ui: standalone UIKit and refactor (#557)
* Eslint import order * Initial Uikit * Rename file with camelCase * Remove unused code * Remove unused code * Set position traficlight mac * Grouping Ribbon, Topbar and Bottombar as layout * Added image brand * Moving feature toggle into context folder * Fix active state of setting menu * Cleanup downloadModel atom helper * Cleanup useGetConfigureModel * Added wave animation * Create useMainViewState intead of import helper atom * Remove unused code * Take a back switch ui * Toggle using switch component * Add dynamic primary color * Cleanup import * Added uikit scroll area * Add best practice form * Added toaster container * Fix loader container * Add hooks useDownloadState * Added tooltip on ribbon menu * Added case user multiple download model * Adjust input style with bigger ring * Restyle my model screen * Replace useStartStop model with useActiveModel * Import icon using Icon name * Fix missing login loading start and stop model * WIP integrate with cmdk * Move layout search bar on middle of app * Added function cancel download * Cleanup model explore * Cleanup unused code * Move app version in bototmbar or footer * WIP chat screen * WIP chat screen * Cleanup style and remove unsed code * Added command for showing downloaded model * Fix missing keyframe loader dot animation * Conditional loader of plugin setting * WIP history list message * chore: rebase main * Adding script ui into root package * Fix different version react hooks form * Add close toaster * Added status model active or not on list of command * Conditional showing info if user don't have a model * Disabled toolbar chat when user not yet have convo * chore: fix state * fix: get resource atom * Fix conditional bottom bar * fix: model download state * Fix font * Improve icon my model * Add toaster delete chat * Remove test classname * Fix scroll chat body * Fix scrolling chat body * chore: add message update * Add uikit into depedencies on root package * Update chat flow * Fix hot reload ui changes * Increate background color chat screen light mode * Added visual conversation active state * Added build:uikit on gh actions * chore: attempt to fix CI * fix: deps * fix: tests * chore: attempt to fix CI --------- Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
parent
fc3352b75c
commit
2394c13065
@ -4,29 +4,31 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'electron/**'
|
- "electron/**"
|
||||||
- .github/workflows/jan-electron-linter-and-test.yml
|
- .github/workflows/jan-electron-linter-and-test.yml
|
||||||
- 'web/**'
|
- "web/**"
|
||||||
- 'package.json'
|
- "uikit/**"
|
||||||
- 'node_modules/**'
|
- "package.json"
|
||||||
- 'yarn.lock'
|
- "node_modules/**"
|
||||||
|
- "yarn.lock"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'electron/**'
|
- "electron/**"
|
||||||
- .github/workflows/linter-and-test.yml
|
- .github/workflows/linter-and-test.yml
|
||||||
- 'web/**'
|
- "web/**"
|
||||||
- 'package.json'
|
- "uikit/**"
|
||||||
- 'node_modules/**'
|
- "package.json"
|
||||||
- 'yarn.lock'
|
- "node_modules/**"
|
||||||
|
- "yarn.lock"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-on-macos:
|
test-on-macos:
|
||||||
runs-on: [self-hosted, macOS, macos-desktop]
|
runs-on: [self-hosted, macOS, macos-desktop]
|
||||||
steps:
|
steps:
|
||||||
- name: 'Cleanup build folder'
|
- name: "Cleanup build folder"
|
||||||
run: |
|
run: |
|
||||||
ls -la ./
|
ls -la ./
|
||||||
rm -rf ./* || true
|
rm -rf ./* || true
|
||||||
@ -41,6 +43,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Build uikit
|
||||||
|
run: |
|
||||||
|
cd uikit
|
||||||
|
yarn install
|
||||||
|
yarn build
|
||||||
|
|
||||||
- name: Linter and test
|
- name: Linter and test
|
||||||
run: |
|
run: |
|
||||||
yarn config set network-timeout 300000
|
yarn config set network-timeout 300000
|
||||||
@ -73,6 +81,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Build uikit
|
||||||
|
run: |
|
||||||
|
cd uikit
|
||||||
|
yarn install
|
||||||
|
yarn build
|
||||||
|
|
||||||
- name: Linter and test
|
- name: Linter and test
|
||||||
run: |
|
run: |
|
||||||
yarn config set network-timeout 300000
|
yarn config set network-timeout 300000
|
||||||
@ -85,7 +99,7 @@ jobs:
|
|||||||
test-on-ubuntu:
|
test-on-ubuntu:
|
||||||
runs-on: [self-hosted, Linux, ubuntu-desktop]
|
runs-on: [self-hosted, Linux, ubuntu-desktop]
|
||||||
steps:
|
steps:
|
||||||
- name: 'Cleanup build folder'
|
- name: "Cleanup build folder"
|
||||||
run: |
|
run: |
|
||||||
ls -la ./
|
ls -la ./
|
||||||
rm -rf ./* || true
|
rm -rf ./* || true
|
||||||
@ -100,6 +114,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Build uikit
|
||||||
|
run: |
|
||||||
|
cd uikit
|
||||||
|
yarn install
|
||||||
|
yarn build
|
||||||
|
|
||||||
- name: Linter and test
|
- name: Linter and test
|
||||||
run: |
|
run: |
|
||||||
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
|
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
|
||||||
@ -109,4 +129,4 @@ jobs:
|
|||||||
yarn install
|
yarn install
|
||||||
yarn build:plugins
|
yarn build:plugins
|
||||||
yarn build:test-linux
|
yarn build:test-linux
|
||||||
yarn test
|
yarn test
|
||||||
|
|||||||
@ -53,11 +53,12 @@ app.on("quit", () => {
|
|||||||
function createMainWindow() {
|
function createMainWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
|
minWidth: 800,
|
||||||
height: 800,
|
height: 800,
|
||||||
show: false,
|
show: false,
|
||||||
trafficLightPosition: {
|
trafficLightPosition: {
|
||||||
x: 16,
|
x: 10,
|
||||||
y: 10,
|
y: 15,
|
||||||
},
|
},
|
||||||
titleBarStyle: "hidden",
|
titleBarStyle: "hidden",
|
||||||
vibrancy: "sidebar",
|
vibrancy: "sidebar",
|
||||||
|
|||||||
@ -36,12 +36,6 @@ test.afterAll(async () => {
|
|||||||
|
|
||||||
test("explores models", async () => {
|
test("explores models", async () => {
|
||||||
await page.getByTestId("Explore Models").first().click();
|
await page.getByTestId("Explore Models").first().click();
|
||||||
const header = await page
|
await page.getByTestId("testid-explore-models").isVisible();
|
||||||
.getByRole("heading")
|
|
||||||
.filter({ hasText: "Explore Models" })
|
|
||||||
.first()
|
|
||||||
.isDisabled();
|
|
||||||
expect(header).toBe(false);
|
|
||||||
|
|
||||||
// More test cases here...
|
// More test cases here...
|
||||||
});
|
});
|
||||||
|
|||||||
@ -36,6 +36,6 @@ test.afterAll(async () => {
|
|||||||
|
|
||||||
test("shows my models", async () => {
|
test("shows my models", async () => {
|
||||||
await page.getByTestId("My Models").first().click();
|
await page.getByTestId("My Models").first().click();
|
||||||
await page.getByTestId("testid-mymodels-header").isVisible();
|
await page.getByTestId("testid-my-models").isVisible();
|
||||||
// More test cases here...
|
// More test cases here...
|
||||||
});
|
});
|
||||||
|
|||||||
19
package.json
19
package.json
@ -3,11 +3,17 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
|
"uikit",
|
||||||
|
"core",
|
||||||
"electron",
|
"electron",
|
||||||
"web",
|
"web",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"nohoist": [
|
"nohoist": [
|
||||||
|
"uikit",
|
||||||
|
"uikit/*",
|
||||||
|
"core",
|
||||||
|
"core/*",
|
||||||
"electron",
|
"electron",
|
||||||
"electron/**",
|
"electron/**",
|
||||||
"web",
|
"web",
|
||||||
@ -23,13 +29,15 @@
|
|||||||
"dev:web": "yarn workspace jan-web dev",
|
"dev:web": "yarn workspace jan-web dev",
|
||||||
"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\"",
|
||||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||||
|
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
|
||||||
|
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
|
||||||
"build:core": "cd core && yarn install && yarn run build",
|
"build:core": "cd core && yarn install && yarn run build",
|
||||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "yarn workspace jan build",
|
"build:electron": "yarn workspace jan build",
|
||||||
"build:electron:test": "yarn workspace jan build:test",
|
"build:electron:test": "yarn workspace jan build:test",
|
||||||
"build:pull-plugins": "rimraf ./electron/core/pre-install/*.tgz && cd ./electron/core/pre-install && npm pack @janhq/inference-plugin @janhq/monitoring-plugin",
|
"build:pull-plugins": "rimraf ./electron/core/pre-install/*.tgz && cd ./electron/core/pre-install && npm pack @janhq/inference-plugin @janhq/monitoring-plugin",
|
||||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:dev && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:dev && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
||||||
"build:plugins-web": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-plugin && npm install && npm run build:deps && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/conversational-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
|
"build:plugins-web": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:deps && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
|
||||||
"build": "yarn build:web && yarn build:electron",
|
"build": "yarn build:web && yarn build:electron",
|
||||||
"build:test": "yarn build:web && yarn build:electron:test",
|
"build:test": "yarn build:web && yarn build:electron:test",
|
||||||
"build:test-darwin": "yarn build:web && yarn workspace jan build:test-darwin",
|
"build:test-darwin": "yarn build:web && yarn workspace jan build:test-darwin",
|
||||||
@ -42,7 +50,7 @@
|
|||||||
"build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin",
|
"build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin",
|
||||||
"build:publish-win32": "yarn build:web && yarn workspace jan build:publish-win32",
|
"build:publish-win32": "yarn build:web && yarn workspace jan build:publish-win32",
|
||||||
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux",
|
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux",
|
||||||
"build:web-plugins": "yarn build:web && yarn build:plugins-web && mkdir -p \"./web/out/plugins/conversational-plugin\" && cp \"./plugins/conversational-plugin/dist/index.js\" \"./web/out/plugins/conversational-plugin\" && mkdir -p \"./web/out/plugins/inference-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/out/plugins/inference-plugin\" && mkdir -p \"./web/out/plugins/model-plugin\" && cp \"./plugins/model-plugin/dist/index.js\" \"./web/out/plugins/model-plugin\" && mkdir -p \"./web/out/plugins/monitoring-plugin\" && cp \"./plugins/monitoring-plugin/dist/index.js\" \"./web/out/plugins/monitoring-plugin\"",
|
"build:web-plugins": "yarn build:web && yarn build:plugins-web && mkdir -p \"./web/out/plugins/conversational-json\" && cp \"./plugins/conversational-json/dist/index.js\" \"./web/out/plugins/conversational-json\" && mkdir -p \"./web/out/plugins/inference-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/out/plugins/inference-plugin\" && mkdir -p \"./web/out/plugins/model-plugin\" && cp \"./plugins/model-plugin/dist/index.js\" \"./web/out/plugins/model-plugin\" && mkdir -p \"./web/out/plugins/monitoring-plugin\" && cp \"./plugins/monitoring-plugin/dist/index.js\" \"./web/out/plugins/monitoring-plugin\"",
|
||||||
"server:prod": "yarn workspace server build && yarn build:web-plugins && cpx \"web/out/**\" \"server/build/renderer/\" && mkdir -p ./server/build/@janhq && cp -r ./plugins/* ./server/build/@janhq",
|
"server:prod": "yarn workspace server build && yarn build:web-plugins && cpx \"web/out/**\" \"server/build/renderer/\" && mkdir -p ./server/build/@janhq && cp -r ./plugins/* ./server/build/@janhq",
|
||||||
"start:server": "yarn server:prod && node server/build/main.js"
|
"start:server": "yarn server:prod && node server/build/main.js"
|
||||||
},
|
},
|
||||||
@ -52,8 +60,5 @@
|
|||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"wait-on": "^7.0.1"
|
"wait-on": "^7.0.1"
|
||||||
},
|
},
|
||||||
"version": "0.0.0",
|
"version": "0.0.0"
|
||||||
"dependencies": {
|
|
||||||
"@janhq/core": "file:core"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
uikit/.prettierignore
Normal file
5
uikit/.prettierignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.next/
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.hbs
|
||||||
|
*.mdx
|
||||||
8
uikit/.prettierrc
Normal file
8
uikit/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
53
uikit/package.json
Normal file
53
uikit/package.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "@janhq/uikit",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist/**"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build:styles": "postcss src/main.scss -o dist/index.css --use postcss-import",
|
||||||
|
"build:react": "tsup src/index.{ts,tsx} --format cjs,esm --dts --external react react-dom --minify terser --splitting --sourcemap",
|
||||||
|
"dev:react": "tsup src/index.{ts,tsx} --format cjs,esm --watch --dts",
|
||||||
|
"dev:styles": "postcss src/main.scss -o dist/index.css -u postcss-import -w",
|
||||||
|
"build": "yarn build:styles && yarn build:react",
|
||||||
|
"dev": "concurrently --kill-others \"yarn dev:styles\" \"yarn dev:react\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
|
"@radix-ui/react-context": "^1.0.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"cmdk": "^0.2.0",
|
||||||
|
"lucide-react": "^0.292.0",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.47.0",
|
||||||
|
"scss": "^0.2.4",
|
||||||
|
"tailwindcss": "^3.3.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"postcss-cli": "^10.1.0",
|
||||||
|
"postcss-import": "^15.1.0",
|
||||||
|
"prejss-cli": "^0.3.3",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||||
|
"tailwind-merge": "^2.0.0",
|
||||||
|
"terser": "^5.24.0",
|
||||||
|
"tsup": "^7.2.0",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
uikit/postcss.config.js
Normal file
8
uikit/postcss.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"tailwindcss/nesting": {},
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
"postcss-import": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
43
uikit/src/avatar/index.tsx
Normal file
43
uikit/src/avatar/index.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { forwardRef, ElementRef, ComponentPropsWithoutRef } from 'react'
|
||||||
|
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const Avatar = forwardRef<
|
||||||
|
ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
|
ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('avatar', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const AvatarImage = forwardRef<
|
||||||
|
ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
|
ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('avatar-image', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||||
|
|
||||||
|
const AvatarFallback = forwardRef<
|
||||||
|
ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
|
ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('avatar-fallback', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
11
uikit/src/avatar/styles.scss
Normal file
11
uikit/src/avatar/styles.scss
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.avatar {
|
||||||
|
@apply relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full;
|
||||||
|
|
||||||
|
&-image {
|
||||||
|
@apply aspect-square h-full w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-fallback {
|
||||||
|
@apply bg-muted flex h-full w-full items-center justify-center rounded-full font-bold uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
uikit/src/badge/index.tsx
Normal file
30
uikit/src/badge/index.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const badgeVariants = cva('badge', {
|
||||||
|
variants: {
|
||||||
|
themes: {
|
||||||
|
primary: 'badge-primary',
|
||||||
|
success: 'badge-success',
|
||||||
|
secondary: 'badge-secondary',
|
||||||
|
danger: 'badge-danger',
|
||||||
|
outline: 'badge-outline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
themes: 'primary',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, themes, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={twMerge(badgeVariants({ themes }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
23
uikit/src/badge/styles.scss
Normal file
23
uikit/src/badge/styles.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.badge {
|
||||||
|
@apply focus:ring-ring border-border inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2;
|
||||||
|
|
||||||
|
&-primary {
|
||||||
|
@apply bg-primary text-primary-foreground hover:bg-primary/80 border-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-success {
|
||||||
|
@apply border-transparent bg-green-500 text-green-900 hover:bg-green-500/80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-secondary {
|
||||||
|
@apply bg-secondary text-secondary-foreground hover:bg-secondary/80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-danger {
|
||||||
|
@apply bg-danger text-danger-foreground hover:bg-danger/80 border-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-outline {
|
||||||
|
@apply text-foreground border-border border;
|
||||||
|
}
|
||||||
|
}
|
||||||
98
uikit/src/button/index.tsx
Normal file
98
uikit/src/button/index.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { forwardRef, ButtonHTMLAttributes } from 'react'
|
||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const buttonVariants = cva('btn', {
|
||||||
|
variants: {
|
||||||
|
themes: {
|
||||||
|
primary: 'btn-primary',
|
||||||
|
danger: 'btn-danger',
|
||||||
|
outline: 'btn-outline',
|
||||||
|
secondary: 'btn-secondary',
|
||||||
|
ghost: 'btn-ghost',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
sm: 'btn-sm',
|
||||||
|
md: 'btn-md',
|
||||||
|
lg: 'btn-lg',
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
true: 'w-full',
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
true: 'btn-loading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
themes: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
themes,
|
||||||
|
size,
|
||||||
|
block,
|
||||||
|
loading,
|
||||||
|
asChild = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const Comp = asChild ? Slot : 'button'
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={twMerge(
|
||||||
|
buttonVariants({ themes, size, block, loading, className })
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
role="status"
|
||||||
|
className="btn-loading-circle"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = 'Button'
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
64
uikit/src/button/styles.scss
Normal file
64
uikit/src/button/styles.scss
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
.btn {
|
||||||
|
@apply inline-flex items-center justify-center whitespace-nowrap rounded-md font-semibold transition-colors;
|
||||||
|
@apply focus-visible:ring-ring cursor-pointer focus-visible:outline-none focus-visible:ring-1;
|
||||||
|
@apply disabled:pointer-events-none disabled:opacity-50;
|
||||||
|
|
||||||
|
&-primary {
|
||||||
|
@apply bg-primary hover:bg-primary/90 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-danger {
|
||||||
|
@apply bg-danger text-danger-foreground hover:bg-danger/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-outline {
|
||||||
|
@apply border-input hover:bg-accent hover:text-accent-foreground border bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-secondary {
|
||||||
|
@apply bg-secondary text-secondary-foreground hover:bg-secondary/80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-ghost {
|
||||||
|
@apply hover:bg-accent hover:text-accent-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-sm {
|
||||||
|
@apply h-7 rounded-md px-3 text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-md {
|
||||||
|
@apply h-9 px-4 py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-lg {
|
||||||
|
@apply h-10 rounded-md px-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-loading {
|
||||||
|
@apply pointer-events-none opacity-50;
|
||||||
|
&-circle {
|
||||||
|
@apply mr-2 h-4 animate-spin opacity-50;
|
||||||
|
> circle {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
> path {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[type='button'],
|
||||||
|
[type='reset'],
|
||||||
|
[type='submit'] {
|
||||||
|
&.btn-primary {
|
||||||
|
@apply bg-primary hover:bg-primary/90;
|
||||||
|
}
|
||||||
|
&.btn-secondary {
|
||||||
|
@apply bg-secondary hover:bg-secondary/80;
|
||||||
|
}
|
||||||
|
&.btn-danger {
|
||||||
|
@apply bg-danger hover:bg-danger/90;
|
||||||
|
}
|
||||||
|
}
|
||||||
132
uikit/src/command/index.tsx
Normal file
132
uikit/src/command/index.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import { DialogProps } from '@radix-ui/react-dialog'
|
||||||
|
import { Command as CommandPrimitive } from 'cmdk'
|
||||||
|
import { Search } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Modal, ModalContent } from '../modal'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const Command = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('command', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Command.displayName = CommandPrimitive.displayName
|
||||||
|
|
||||||
|
interface CommandModalProps extends DialogProps {}
|
||||||
|
|
||||||
|
const CommandModal = ({ children, ...props }: CommandModalProps) => {
|
||||||
|
return (
|
||||||
|
<Modal {...props}>
|
||||||
|
<ModalContent className="command-modal-content">
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandInput = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="command-input-wrapper" cmdk-input-wrapper="">
|
||||||
|
<Search className="command-search-icon" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('command-input', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||||
|
|
||||||
|
const CommandList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('command-list', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandList.displayName = CommandPrimitive.List.displayName
|
||||||
|
|
||||||
|
const CommandEmpty = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||||
|
>((props, ref) => (
|
||||||
|
<CommandPrimitive.Empty ref={ref} className="command-empty" {...props} />
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||||
|
|
||||||
|
const CommandGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('command-group', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||||
|
|
||||||
|
const CommandSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('bg-border -mx-1 h-px', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const CommandItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('command-list-item', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const CommandShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return <span className={twMerge('command-sc', className)} {...props} />
|
||||||
|
}
|
||||||
|
CommandShortcut.displayName = 'CommandShortcut'
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandModal,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
45
uikit/src/command/styles.scss
Normal file
45
uikit/src/command/styles.scss
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.command {
|
||||||
|
@apply bg-background/80 text-muted-foreground flex h-full w-full flex-col overflow-hidden rounded-md text-left;
|
||||||
|
|
||||||
|
&-modal-content {
|
||||||
|
@apply overflow-hidden p-0;
|
||||||
|
> .modal-close {
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input-wrapper {
|
||||||
|
@apply border-border flex items-center border-b px-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input {
|
||||||
|
@apply placeholder:text-muted-foreground flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-search-icon {
|
||||||
|
@apply mr-2 h-4 w-4 shrink-0 opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-list {
|
||||||
|
@apply max-h-[300px] overflow-y-auto overflow-x-hidden py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-list-item {
|
||||||
|
@apply text-foreground aria-selected:bg-accent relative flex cursor-pointer select-none items-center rounded-md px-2 py-2 text-sm outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-empty {
|
||||||
|
@apply py-6 text-center text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-group {
|
||||||
|
@apply text-muted-foreground overflow-hidden p-1 px-2 py-1.5 text-xs font-medium;
|
||||||
|
> [cmdk-group-heading] {
|
||||||
|
@apply mb-2 pl-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-sc {
|
||||||
|
@apply text-muted-foreground ml-auto text-xs tracking-widest;
|
||||||
|
}
|
||||||
|
}
|
||||||
175
uikit/src/form/index.tsx
Normal file
175
uikit/src/form/index.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
ControllerProps,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
} from 'react-hook-form'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const Form = FormProvider
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext)
|
||||||
|
const itemContext = React.useContext(FormItemContext)
|
||||||
|
const { getFieldState, formState } = useFormContext()
|
||||||
|
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState)
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error('useFormField should be used within <FormField>')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const id = React.useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div ref={ref} className={twMerge(className)} {...props} />
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormItem.displayName = 'FormItem'
|
||||||
|
|
||||||
|
const FormLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('form-label', className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormLabel.displayName = 'FormLabel'
|
||||||
|
|
||||||
|
const FormControl = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Slot>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
|
>(({ ...props }, ref) => {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
ref={ref}
|
||||||
|
id={formItemId}
|
||||||
|
className={error && 'form-input-error'}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormControl.displayName = 'FormControl'
|
||||||
|
|
||||||
|
const FormDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={twMerge('form-description', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormDescription.displayName = 'FormDescription'
|
||||||
|
|
||||||
|
const FormMessage = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
const { error, formMessageId } = useFormField()
|
||||||
|
const body = error ? String(error?.message) : children
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formMessageId}
|
||||||
|
className={twMerge('form-message', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
FormMessage.displayName = 'FormMessage'
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
}
|
||||||
21
uikit/src/form/styles.scss
Normal file
21
uikit/src/form/styles.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.form {
|
||||||
|
&-item {
|
||||||
|
@apply space-y-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input-error {
|
||||||
|
@apply border-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
@apply mb-2 inline-block cursor-pointer font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-description {
|
||||||
|
@apply text-muted-foreground text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-message {
|
||||||
|
@apply text-danger mt-2 text-xs font-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
uikit/src/index.ts
Normal file
11
uikit/src/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export * from './avatar'
|
||||||
|
export * from './switch'
|
||||||
|
export * from './button'
|
||||||
|
export * from './scroll-area'
|
||||||
|
export * from './form'
|
||||||
|
export * from './input'
|
||||||
|
export * from './progress'
|
||||||
|
export * from './badge'
|
||||||
|
export * from './tooltip'
|
||||||
|
export * from './modal'
|
||||||
|
export * from './command'
|
||||||
21
uikit/src/input/index.tsx
Normal file
21
uikit/src/input/index.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { forwardRef } from 'react'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={twMerge('input test', className)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = 'Input'
|
||||||
|
|
||||||
|
export { Input }
|
||||||
6
uikit/src/input/styles.scss
Normal file
6
uikit/src/input/styles.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.input {
|
||||||
|
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-md border bg-transparent px-3 py-1 transition-colors;
|
||||||
|
@apply disabled:cursor-not-allowed disabled:opacity-50;
|
||||||
|
@apply focus-visible:ring-secondary focus-visible:outline-none focus-visible:ring-1;
|
||||||
|
@apply file:border-0 file:bg-transparent file:font-medium;
|
||||||
|
}
|
||||||
116
uikit/src/main.scss
Normal file
116
uikit/src/main.scss
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
|
@import './avatar/styles.scss';
|
||||||
|
@import './switch/styles.scss';
|
||||||
|
@import './button/styles.scss';
|
||||||
|
@import './scroll-area/styles.scss';
|
||||||
|
@import './form/styles.scss';
|
||||||
|
@import './input/styles.scss';
|
||||||
|
@import './progress/styles.scss';
|
||||||
|
@import './badge/styles.scss';
|
||||||
|
@import './tooltip/styles.scss';
|
||||||
|
@import './modal/styles.scss';
|
||||||
|
@import './command/styles.scss';
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--muted: 60 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
|
||||||
|
--accent: 60 4.8% 95.9%;
|
||||||
|
--accent-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--danger: 346.8 77.2% 49.8%;
|
||||||
|
--danger-foreground: 355.7 100% 97.3%;
|
||||||
|
|
||||||
|
--border: 20 5.9% 90%;
|
||||||
|
--input: 20 5.9% 90%;
|
||||||
|
--ring: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
.primary-blue {
|
||||||
|
--primary: 221 83% 53%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 24 9.8% 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-green {
|
||||||
|
--primary: 142.1 76.2% 36.3%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-purple {
|
||||||
|
--primary: 262.1 83.3% 57.8%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
|
||||||
|
--secondary: 220 14.3% 95.9%;
|
||||||
|
--secondary-foreground: 220.9 39.3% 11%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--muted: 12 6.5% 15.1%;
|
||||||
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
|
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--danger: 346.8 77.2% 49.8%;
|
||||||
|
--danger-foreground: 355.7 100% 97.3%;
|
||||||
|
|
||||||
|
--border: 12 6.5% 15.1%;
|
||||||
|
--input: 12 6.5% 15.1%;
|
||||||
|
--ring: 35.5 91.7% 32.9%;
|
||||||
|
|
||||||
|
.primary-blue {
|
||||||
|
--primary: 221 83% 53%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 12 6.5% 15.1%;
|
||||||
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-green {
|
||||||
|
--primary: 142.1 70.6% 45.3%;
|
||||||
|
--primary-foreground: 144.9 80.4% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-purple {
|
||||||
|
--primary: 263.4 70% 50.4%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
|
||||||
|
--secondary: 215 27.9% 16.9%;
|
||||||
|
--secondary-foreground: 210 20% 98%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(p) {
|
||||||
|
@apply text-muted-foreground;
|
||||||
|
}
|
||||||
99
uikit/src/modal/index.tsx
Normal file
99
uikit/src/modal/index.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as ModalPrimitive from '@radix-ui/react-dialog'
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const Modal = ModalPrimitive.Root
|
||||||
|
|
||||||
|
const ModalTrigger = ModalPrimitive.Trigger
|
||||||
|
|
||||||
|
const ModalPortal = ModalPrimitive.Portal
|
||||||
|
|
||||||
|
const ModalClose = ModalPrimitive.Close
|
||||||
|
|
||||||
|
const ModalOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ModalPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ModalPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ModalPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge(' modal-backdrop', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ModalOverlay.displayName = ModalPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const ModalContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ModalPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ModalPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ModalPortal>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge(' modal-content', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ModalPrimitive.Close className="modal-close">
|
||||||
|
<X size={20} />
|
||||||
|
</ModalPrimitive.Close>
|
||||||
|
</ModalPrimitive.Content>
|
||||||
|
</ModalPortal>
|
||||||
|
))
|
||||||
|
ModalContent.displayName = ModalPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const ModalHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={twMerge('modal-header', className)} {...props} />
|
||||||
|
)
|
||||||
|
ModalHeader.displayName = 'ModalHeader'
|
||||||
|
|
||||||
|
const ModalFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={twMerge('modal-footer', className)} {...props} />
|
||||||
|
)
|
||||||
|
ModalFooter.displayName = 'ModalFooter'
|
||||||
|
|
||||||
|
const ModalTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ModalPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ModalPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ModalPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('modal-title', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ModalTitle.displayName = ModalPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const ModalDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ModalPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ModalPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ModalPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('modal-description', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ModalDescription.displayName = ModalPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Modal,
|
||||||
|
ModalPortal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalClose,
|
||||||
|
ModalTrigger,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
ModalTitle,
|
||||||
|
ModalDescription,
|
||||||
|
}
|
||||||
32
uikit/src/modal/styles.scss
Normal file
32
uikit/src/modal/styles.scss
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
.modal {
|
||||||
|
&-backdrop {
|
||||||
|
@apply bg-background/80 fixed inset-0 z-50 backdrop-blur-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
@apply bg-background border-border fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-4 shadow-lg duration-200 sm:rounded-lg md:w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-close {
|
||||||
|
@apply absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none;
|
||||||
|
> svg {
|
||||||
|
@apply text-muted-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
@apply flex flex-col space-y-1.5 text-center sm:text-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-footer {
|
||||||
|
@apply flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
@apply text-lg font-semibold leading-none tracking-tight;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-description {
|
||||||
|
@apply text-muted-foreground text-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
uikit/src/progress/index.tsx
Normal file
24
uikit/src/progress/index.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as ProgressPrimitive from '@radix-ui/react-progress'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const Progress = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
|
>(({ className, value, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('progress', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className="progress-indicator"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
))
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Progress }
|
||||||
7
uikit/src/progress/styles.scss
Normal file
7
uikit/src/progress/styles.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.progress {
|
||||||
|
@apply bg-secondary relative h-4 w-full overflow-hidden rounded-full;
|
||||||
|
|
||||||
|
&-indicator {
|
||||||
|
@apply bg-primary h-full w-full flex-1 transition-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
uikit/src/scroll-area/index.tsx
Normal file
51
uikit/src/scroll-area/index.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { forwardRef, ElementRef, ComponentPropsWithoutRef } from 'react'
|
||||||
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const ScrollArea = forwardRef<
|
||||||
|
ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge('scroll-area', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="scroll-area-viewport">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
))
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ScrollBar = forwardRef<
|
||||||
|
ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
|
ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={twMerge(
|
||||||
|
'scroll-bar',
|
||||||
|
orientation === 'vertical' && 'scroll-bar-vertical',
|
||||||
|
orientation === 'horizontal' && 'scroll-bar-vertical ',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
className={twMerge(
|
||||||
|
'scroll-bar-thumb',
|
||||||
|
orientation === 'vertical' && 'flex-1'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
))
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
||||||
23
uikit/src/scroll-area/styles.scss
Normal file
23
uikit/src/scroll-area/styles.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.scroll-area {
|
||||||
|
@apply relative overflow-hidden;
|
||||||
|
|
||||||
|
&-viewport {
|
||||||
|
@apply h-full w-full rounded-[inherit];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-bar {
|
||||||
|
@apply flex touch-none select-none transition-colors;
|
||||||
|
|
||||||
|
&-vertical {
|
||||||
|
@apply h-full w-2.5 border-l border-l-transparent p-[1px];
|
||||||
|
}
|
||||||
|
|
||||||
|
&-horizontal {
|
||||||
|
@apply h-2.5 flex-col border-t border-t-transparent p-[1px];
|
||||||
|
}
|
||||||
|
|
||||||
|
&-thumb {
|
||||||
|
@apply bg-border relative z-50 w-[10px] rounded-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
uikit/src/switch/index.tsx
Normal file
22
uikit/src/switch/index.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as SwitchPrimitives from '@radix-ui/react-switch'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
import { forwardRef, ElementRef, ComponentPropsWithoutRef } from 'react'
|
||||||
|
|
||||||
|
const Switch = forwardRef<
|
||||||
|
ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={twMerge('switch peer', className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb className={twMerge('switch-toggle')} />
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
10
uikit/src/switch/styles.scss
Normal file
10
uikit/src/switch/styles.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.switch {
|
||||||
|
@apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent;
|
||||||
|
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
|
||||||
|
@apply data-[state=checked]:bg-primary data-[state=unchecked]:bg-input;
|
||||||
|
@apply disabled:cursor-not-allowed disabled:opacity-50;
|
||||||
|
|
||||||
|
&-toggle {
|
||||||
|
@apply bg-background pointer-events-none block h-4 w-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
uikit/src/tooltip/index.tsx
Normal file
40
uikit/src/tooltip/index.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={twMerge('tooltip', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const TooltipArrow = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className }, ref) => (
|
||||||
|
<TooltipPrimitive.Arrow className={twMerge('tooltip-arrow', className)} />
|
||||||
|
))
|
||||||
|
TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Tooltip,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipArrow,
|
||||||
|
}
|
||||||
6
uikit/src/tooltip/styles.scss
Normal file
6
uikit/src/tooltip/styles.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.tooltip {
|
||||||
|
@apply dark:bg-input dark:text-foreground z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md;
|
||||||
|
&-arrow {
|
||||||
|
@apply dark:fill-input fill-gray-950;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
uikit/tailwind.config.js
Normal file
32
uikit/tailwind.config.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{js,jsx,ts,tsx,scss,css}'],
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'background': 'hsl(var(--background))',
|
||||||
|
'foreground': 'hsl(var(--foreground))',
|
||||||
|
|
||||||
|
'card': 'hsl(var(--card))',
|
||||||
|
'card-foreground': 'hsl(var(--card-foreground))',
|
||||||
|
|
||||||
|
'primary': 'hsl(var(--primary))',
|
||||||
|
'primary-foreground': 'hsl(var(--primary-foreground))',
|
||||||
|
|
||||||
|
'secondary': 'hsl(var(--secondary))',
|
||||||
|
'secondary-foreground': 'hsl(var(--secondary-foreground))',
|
||||||
|
|
||||||
|
'muted': 'hsl(var(--muted))',
|
||||||
|
'muted-foreground': 'hsl(var(--muted-foreground))',
|
||||||
|
|
||||||
|
'accent': 'hsl(var(--accent))',
|
||||||
|
'accent-foreground': 'hsl(var(--accent-foreground))',
|
||||||
|
|
||||||
|
'danger': 'hsl(var(--danger))',
|
||||||
|
'danger-foreground': 'hsl(var(--danger-foreground))',
|
||||||
|
|
||||||
|
'border': 'hsl(var(--border))',
|
||||||
|
'input': 'hsl(var(--input))',
|
||||||
|
'ring': 'hsl(var(--ring))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
24
uikit/tsconfig.json
Normal file
24
uikit/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "React Library",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["dom", "ES2015"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "es6",
|
||||||
|
"composite": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"inlineSources": false,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"preserveWatchOutput": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
4
uikit/types/declaration.d.ts
vendored
Normal file
4
uikit/types/declaration.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.scss' {
|
||||||
|
const content: Record<string, string>;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
144
web/.eslintrc.js
Normal file
144
web/.eslintrc.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
|
||||||
|
ignorePatterns: [
|
||||||
|
'build',
|
||||||
|
'dist',
|
||||||
|
'node_modules',
|
||||||
|
'renderer',
|
||||||
|
'.next',
|
||||||
|
'_next',
|
||||||
|
'*.md',
|
||||||
|
'out',
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
'next/core-web-vitals',
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:import/typescript',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'prettier',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
'eslint-config-next/core-web-vitals',
|
||||||
|
],
|
||||||
|
globals: {
|
||||||
|
React: true,
|
||||||
|
JSX: true,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@next/next/no-server-import-in-page': 'off',
|
||||||
|
|
||||||
|
'@typescript-eslint/naming-convention': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
selector: 'default',
|
||||||
|
format: ['camelCase', 'PascalCase'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'variableLike',
|
||||||
|
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
|
||||||
|
leadingUnderscore: 'allow',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'property',
|
||||||
|
format: ['camelCase', 'snake_case', 'PascalCase', 'UPPER_CASE'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'memberLike',
|
||||||
|
format: ['camelCase', 'PascalCase'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'typeLike',
|
||||||
|
format: ['PascalCase'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@next/next/no-img-element': 'off',
|
||||||
|
'@next/next/no-html-link-for-pages': 'off',
|
||||||
|
'react/display-name': 'off',
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn'],
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'alphabetize': { order: 'asc' },
|
||||||
|
'groups': ['builtin', 'external', 'parent', 'sibling', 'index'],
|
||||||
|
'pathGroups': [
|
||||||
|
{
|
||||||
|
pattern: 'react*',
|
||||||
|
group: 'external',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'next*',
|
||||||
|
group: 'external',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'next/*',
|
||||||
|
group: 'external',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/assets/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/components/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/containers/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/context/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/constants/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/hooks/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/services/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/screens/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/utils/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '@/styles/**',
|
||||||
|
group: 'parent',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'pathGroupsExcludedImportTypes': ['react'],
|
||||||
|
'newlines-between': 'always-and-inside-groups',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "next/core-web-vitals",
|
|
||||||
"rules": {
|
|
||||||
"@next/next/no-img-element": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,6 +3,6 @@
|
|||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"quoteProps": "consistent",
|
"quoteProps": "consistent",
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"endOfLine": "lf",
|
"endOfLine": "auto",
|
||||||
"plugins": ["prettier-plugin-tailwindcss"]
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import React from 'react'
|
|
||||||
import ModelTable from '../ModelTable'
|
|
||||||
import { activeModelAtom } from '@helpers/atoms/Model.atom'
|
|
||||||
|
|
||||||
const ActiveModelTable: React.FC = () => {
|
|
||||||
const activeModel = useAtomValue(activeModelAtom)
|
|
||||||
|
|
||||||
if (!activeModel) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pl-[63px] pr-[89px]">
|
|
||||||
<h3 className="mb-[13px] text-xl leading-[25px]">Active Model(s)</h3>
|
|
||||||
<ModelTable models={[activeModel]} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ActiveModelTable
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import DownloadModelContent from '../DownloadModelContent'
|
|
||||||
import ModelDownloadButton from '../ModelDownloadButton'
|
|
||||||
import ModelDownloadingButton from '../ModelDownloadingButton'
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
|
|
||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
model: Model
|
|
||||||
isRecommend: boolean
|
|
||||||
required?: string
|
|
||||||
onDownloadClick?: (model: Model) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const AvailableModelCard: React.FC<Props> = ({
|
|
||||||
model,
|
|
||||||
isRecommend,
|
|
||||||
required,
|
|
||||||
onDownloadClick,
|
|
||||||
}) => {
|
|
||||||
const downloadState = useAtomValue(modelDownloadStateAtom)
|
|
||||||
|
|
||||||
let isDownloading = false
|
|
||||||
let total = 0
|
|
||||||
let transferred = 0
|
|
||||||
|
|
||||||
if (model._id && downloadState[model._id]) {
|
|
||||||
isDownloading =
|
|
||||||
downloadState[model._id].error == null &&
|
|
||||||
downloadState[model._id].percent < 1
|
|
||||||
|
|
||||||
if (isDownloading) {
|
|
||||||
total = downloadState[model._id].size.total
|
|
||||||
transferred = downloadState[model._id].size.transferred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadButton = isDownloading ? (
|
|
||||||
<div className="flex w-1/5 items-start justify-end">
|
|
||||||
<ModelDownloadingButton total={total} value={transferred} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex w-1/5 items-center justify-end">
|
|
||||||
<ModelDownloadButton callback={() => onDownloadClick?.(model)} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border border-gray-200">
|
|
||||||
<div className="flex justify-between gap-2.5 px-3 py-4">
|
|
||||||
<DownloadModelContent
|
|
||||||
required={required}
|
|
||||||
author={model.author}
|
|
||||||
description={model.shortDescription}
|
|
||||||
isRecommend={isRecommend}
|
|
||||||
name={model.name}
|
|
||||||
type={'LLM'}
|
|
||||||
/>
|
|
||||||
{downloadButton}
|
|
||||||
</div>
|
|
||||||
{/* <ViewModelDetailButton callback={handleViewDetails} /> */}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AvailableModelCard
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import SecondaryButton from '../SecondaryButton'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
allowEdit?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const Avatar: React.FC<Props> = ({ allowEdit = false }) => (
|
|
||||||
<div className="mx-auto flex flex-col gap-5">
|
|
||||||
<span className="mx-auto inline-block h-10 w-10 overflow-hidden rounded-full bg-gray-100">
|
|
||||||
<svg
|
|
||||||
className="mx-auto h-full w-full text-gray-300"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
{allowEdit ?? <SecondaryButton title={'Edit picture'} />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default Avatar
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
import { ChevronLeftIcon } from '@heroicons/react/24/outline'
|
|
||||||
import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
|
|
||||||
const BasicPromptButton: React.FC = () => {
|
|
||||||
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowingAdvancedPrompt(false)}
|
|
||||||
className="mx-2 mb-[10px] mt-3 flex flex-none items-center gap-1 text-xs leading-[18px] text-[#6B7280]"
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon width={20} height={20} />
|
|
||||||
<span className="text-xs font-semibold text-gray-500">BASIC PROMPT</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(BasicPromptButton)
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
|
|
||||||
import {
|
|
||||||
MainViewState,
|
|
||||||
setMainViewStateAtom,
|
|
||||||
} from '@helpers/atoms/MainView.atom'
|
|
||||||
import useCreateConversation from '@hooks/useCreateConversation'
|
|
||||||
import useDeleteBot from '@hooks/useDeleteBot'
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
import React from 'react'
|
|
||||||
import PrimaryButton from '../PrimaryButton'
|
|
||||||
import ExpandableHeader from '../ExpandableHeader'
|
|
||||||
|
|
||||||
const BotInfo: React.FC = () => {
|
|
||||||
const { deleteBot } = useDeleteBot()
|
|
||||||
const { createConvoByBot } = useCreateConversation()
|
|
||||||
const setMainView = useSetAtom(setMainViewStateAtom)
|
|
||||||
const botInfo = useAtomValue(activeBotAtom)
|
|
||||||
if (!botInfo) return null
|
|
||||||
|
|
||||||
const onNewChatClicked = () => {
|
|
||||||
if (!botInfo) {
|
|
||||||
alert('No bot selected')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createConvoByBot(botInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDeleteBotClick = async () => {
|
|
||||||
// TODO: display confirmation diaglog
|
|
||||||
const result = await deleteBot(botInfo._id)
|
|
||||||
if (result === 'success') {
|
|
||||||
setMainView(MainViewState.Welcome)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-1 my-1 flex flex-col gap-2">
|
|
||||||
<ExpandableHeader title="BOT INFO" />
|
|
||||||
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className="mb-2">{botInfo.name}</label>
|
|
||||||
<span className="text-muted-foreground">{botInfo.description}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full flex-col space-y-2">
|
|
||||||
<PrimaryButton onClick={onNewChatClicked} title="New chat" />
|
|
||||||
<PrimaryButton
|
|
||||||
title="Delete bot"
|
|
||||||
onClick={onDeleteBotClick}
|
|
||||||
className="bg-red-500 hover:bg-red-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BotInfo
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
import React from 'react'
|
|
||||||
import Avatar from '../Avatar'
|
|
||||||
import PrimaryButton from '../PrimaryButton'
|
|
||||||
import useCreateConversation from '@hooks/useCreateConversation'
|
|
||||||
import useDeleteBot from '@hooks/useDeleteBot'
|
|
||||||
import {
|
|
||||||
setMainViewStateAtom,
|
|
||||||
MainViewState,
|
|
||||||
} from '@helpers/atoms/MainView.atom'
|
|
||||||
|
|
||||||
const BotInfoContainer: React.FC = () => {
|
|
||||||
const activeBot = useAtomValue(activeBotAtom)
|
|
||||||
const setMainView = useSetAtom(setMainViewStateAtom)
|
|
||||||
const { deleteBot } = useDeleteBot()
|
|
||||||
const { createConvoByBot } = useCreateConversation()
|
|
||||||
|
|
||||||
const onNewChatClicked = () => {
|
|
||||||
if (!activeBot) {
|
|
||||||
alert('No bot selected')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createConvoByBot(activeBot)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDeleteBotClick = async () => {
|
|
||||||
if (!activeBot) {
|
|
||||||
alert('No bot selected')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: display confirmation diaglog
|
|
||||||
const result = await deleteBot(activeBot._id)
|
|
||||||
if (result === 'success') {
|
|
||||||
setMainView(MainViewState.Welcome)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!activeBot) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full pt-4">
|
|
||||||
<div className="mx-auto flex w-[672px] min-w-max flex-col gap-4">
|
|
||||||
<Avatar />
|
|
||||||
<h1 className="text-center text-2xl font-bold">{activeBot?.name}</h1>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<PrimaryButton
|
|
||||||
fullWidth
|
|
||||||
title="New chat"
|
|
||||||
onClick={onNewChatClicked}
|
|
||||||
/>
|
|
||||||
<PrimaryButton
|
|
||||||
fullWidth
|
|
||||||
className="bg-red-500 hover:bg-red-400"
|
|
||||||
title="Delete bot"
|
|
||||||
onClick={onDeleteBotClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p>{activeBot?.description}</p>
|
|
||||||
<p>System prompt</p>
|
|
||||||
<p>{activeBot?.systemPrompt}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BotInfoContainer
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
|
|
||||||
import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
import useGetBots from '@hooks/useGetBots'
|
|
||||||
import { useAtom, useSetAtom } from 'jotai'
|
|
||||||
import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import Avatar from '../Avatar'
|
|
||||||
import {
|
|
||||||
MainViewState,
|
|
||||||
setMainViewStateAtom,
|
|
||||||
} from '@helpers/atoms/MainView.atom'
|
|
||||||
|
|
||||||
const BotListContainer: React.FC = () => {
|
|
||||||
const [open, setOpen] = useAtom(showingBotListModalAtom)
|
|
||||||
const setMainView = useSetAtom(setMainViewStateAtom)
|
|
||||||
const [activeBot, setActiveBot] = useAtom(activeBotAtom)
|
|
||||||
const [bots, setBots] = useState<Bot[]>([])
|
|
||||||
const { getAllBots } = useGetBots()
|
|
||||||
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
getAllBots().then((res) => {
|
|
||||||
setBots(res)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [open])
|
|
||||||
|
|
||||||
const onBotSelected = (bot: Bot) => {
|
|
||||||
if (bot._id !== activeBot?._id) {
|
|
||||||
setMainView(MainViewState.BotInfo)
|
|
||||||
setActiveBot(bot)
|
|
||||||
setRightSideBarVisibility(true)
|
|
||||||
}
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-background/50 border-border overflow-hidden border sm:rounded-md">
|
|
||||||
<ul role="list" className="divide-y divide-gray-200">
|
|
||||||
{bots.map((bot, i) => (
|
|
||||||
<li
|
|
||||||
role="button"
|
|
||||||
key={i}
|
|
||||||
className="flex items-center gap-4 p-4 hover:bg-hover-light sm:px-6"
|
|
||||||
onClick={() => onBotSelected(bot)}
|
|
||||||
>
|
|
||||||
<Avatar />
|
|
||||||
<div className="flex flex-1 flex-col">
|
|
||||||
<p className="line-clamp-1">{bot.name}</p>
|
|
||||||
<p className="text-muted-foreground mt-1 line-clamp-1 text-ellipsis">
|
|
||||||
{bot._id}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BotListContainer
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
|
||||||
import { useAtom } from 'jotai'
|
|
||||||
import React, { Fragment } from 'react'
|
|
||||||
import BotListContainer from '../BotListContainer'
|
|
||||||
|
|
||||||
const BotListModal: React.FC = () => {
|
|
||||||
const [open, setOpen] = useAtom(showingBotListModalAtom)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={open} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-10" onClose={setOpen}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 z-40 h-full bg-gray-950/90 transition-opacity dark:backdrop-blur-sm" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-50 w-screen overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="border-border bg-background/90 relative transform overflow-hidden rounded-lg border px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
|
||||||
<h1 className="mb-4 font-bold">Your bots</h1>
|
|
||||||
<BotListContainer />
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BotListModal
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import Image from 'next/image'
|
|
||||||
|
|
||||||
const BotPreview: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[235px] flex-col gap-2 overflow-hidden rounded-lg border border-gray-400 pb-2">
|
|
||||||
<div className="flex items-center justify-center bg-gray-400 p-2">
|
|
||||||
<Image
|
|
||||||
className="rounded-md"
|
|
||||||
src={
|
|
||||||
'https://i.pinimg.com/564x/52/b1/6f/52b16f96f52221d48bea716795ccc89a.jpg'
|
|
||||||
}
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1 px-1 text-xs text-gray-400">
|
|
||||||
<div className="mx-1 flex-grow border-b border-gray-400"></div>
|
|
||||||
Context cleared
|
|
||||||
<div className="mx-1 flex-grow border-b border-gray-400"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BotPreview
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import ExpandableHeader from '../ExpandableHeader'
|
|
||||||
import { useDebouncedCallback } from 'use-debounce'
|
|
||||||
import useUpdateBot from '@hooks/useUpdateBot'
|
|
||||||
import { formatTwoDigits } from '@utils/converter'
|
|
||||||
|
|
||||||
const delayBeforeUpdateInMs = 1000
|
|
||||||
|
|
||||||
const BotSetting: React.FC = () => {
|
|
||||||
const activeBot = useAtomValue(activeBotAtom)
|
|
||||||
const [temperature, setTemperature] = useState(
|
|
||||||
activeBot?.customTemperature ?? 0
|
|
||||||
)
|
|
||||||
|
|
||||||
const [maxTokens, setMaxTokens] = useState(activeBot?.maxTokens ?? 0)
|
|
||||||
const [frequencyPenalty, setFrequencyPenalty] = useState(
|
|
||||||
activeBot?.frequencyPenalty ?? 0
|
|
||||||
)
|
|
||||||
const [presencePenalty, setPresencePenalty] = useState(
|
|
||||||
activeBot?.presencePenalty ?? 0
|
|
||||||
)
|
|
||||||
|
|
||||||
const { updateBot } = useUpdateBot()
|
|
||||||
|
|
||||||
const debouncedTemperature = useDebouncedCallback((value) => {
|
|
||||||
if (!activeBot) return
|
|
||||||
if (activeBot.customTemperature === value) return
|
|
||||||
updateBot(activeBot, { customTemperature: value })
|
|
||||||
}, delayBeforeUpdateInMs)
|
|
||||||
|
|
||||||
const debouncedMaxToken = useDebouncedCallback((value) => {
|
|
||||||
if (!activeBot) return
|
|
||||||
if (activeBot.maxTokens === value) return
|
|
||||||
updateBot(activeBot, { maxTokens: value })
|
|
||||||
}, delayBeforeUpdateInMs)
|
|
||||||
|
|
||||||
const debouncedFreqPenalty = useDebouncedCallback((value) => {
|
|
||||||
if (!activeBot) return
|
|
||||||
if (activeBot.frequencyPenalty === value) return
|
|
||||||
updateBot(activeBot, { frequencyPenalty: value })
|
|
||||||
}, delayBeforeUpdateInMs)
|
|
||||||
|
|
||||||
const debouncedPresencePenalty = useDebouncedCallback((value) => {
|
|
||||||
if (!activeBot) return
|
|
||||||
if (activeBot.presencePenalty === value) return
|
|
||||||
updateBot(activeBot, { presencePenalty: value })
|
|
||||||
}, delayBeforeUpdateInMs)
|
|
||||||
|
|
||||||
const debouncedSystemPrompt = useDebouncedCallback((value) => {
|
|
||||||
if (!activeBot) return
|
|
||||||
if (activeBot.systemPrompt === value) return
|
|
||||||
updateBot(activeBot, { systemPrompt: value })
|
|
||||||
}, delayBeforeUpdateInMs)
|
|
||||||
|
|
||||||
if (!activeBot) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="my-3 flex flex-col">
|
|
||||||
<ExpandableHeader title="BOT SETTINGS" />
|
|
||||||
<div className="mx-2 mt-3 flex flex-shrink-0 flex-col gap-4">
|
|
||||||
{/* System prompt */}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="comment" className="block">
|
|
||||||
System prompt
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<textarea
|
|
||||||
rows={4}
|
|
||||||
name="comment"
|
|
||||||
id="comment"
|
|
||||||
className="bg-background/80 text-background-reverse ring-border placeholder:text-muted-foreground focus:ring-accent/50 block w-full resize-none rounded-md border-0 py-1.5 text-xs leading-relaxed shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset"
|
|
||||||
defaultValue={activeBot.systemPrompt}
|
|
||||||
onChange={(e) => debouncedSystemPrompt(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* TODO: clean up this code */}
|
|
||||||
{/* Max temp */}
|
|
||||||
<p>Max tokens</p>
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
className="flex-1"
|
|
||||||
type="range"
|
|
||||||
defaultValue={activeBot.maxTokens ?? 0}
|
|
||||||
min={0}
|
|
||||||
max={4096}
|
|
||||||
step={1}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = Number(e.target.value)
|
|
||||||
setMaxTokens(value)
|
|
||||||
debouncedMaxToken(value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="border-accent rounded-md border px-2 py-1">
|
|
||||||
{formatTwoDigits(maxTokens)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Frequency penalty</p>
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
className="flex-1"
|
|
||||||
type="range"
|
|
||||||
defaultValue={activeBot.frequencyPenalty ?? 0}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = Number(e.target.value)
|
|
||||||
setFrequencyPenalty(value)
|
|
||||||
debouncedFreqPenalty(value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="border-accent rounded-md border px-2 py-1">
|
|
||||||
{formatTwoDigits(frequencyPenalty)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Presence penalty</p>
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
className="flex-1"
|
|
||||||
type="range"
|
|
||||||
defaultValue={activeBot.maxTokens ?? 0}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = Number(e.target.value)
|
|
||||||
setPresencePenalty(value)
|
|
||||||
debouncedPresencePenalty(value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="border-accent rounded-md border px-2 py-1">
|
|
||||||
{formatTwoDigits(presencePenalty)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Custom temp */}
|
|
||||||
<p>Temperature</p>
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
className="flex-1"
|
|
||||||
type="range"
|
|
||||||
id="volume"
|
|
||||||
name="volume"
|
|
||||||
defaultValue={activeBot.customTemperature ?? 0}
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.01"
|
|
||||||
onChange={(e) => {
|
|
||||||
const newTemp = Number(e.target.value)
|
|
||||||
setTemperature(newTemp)
|
|
||||||
debouncedTemperature(Number(e.target.value))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="border-accent rounded-md border px-2 py-1">
|
|
||||||
{formatTwoDigits(temperature)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BotSetting
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import MainHeader from '../MainHeader'
|
|
||||||
import MainView from '../MainView'
|
|
||||||
|
|
||||||
const CenterContainer: React.FC = () => (
|
|
||||||
<div className="flex flex-1 flex-col dark:bg-gray-950/50">
|
|
||||||
<MainHeader />
|
|
||||||
<MainView />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default React.memo(CenterContainer)
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import SimpleControlNetMessage from '../SimpleControlNetMessage'
|
|
||||||
import SimpleImageMessage from '../SimpleImageMessage'
|
|
||||||
import SimpleTextMessage from '../SimpleTextMessage'
|
|
||||||
import { ChatMessage, MessageType } from '@models/ChatMessage'
|
|
||||||
|
|
||||||
|
|
||||||
export default function renderChatMessage({
|
|
||||||
id,
|
|
||||||
messageType,
|
|
||||||
messageSenderType,
|
|
||||||
senderAvatarUrl,
|
|
||||||
senderName,
|
|
||||||
createdAt,
|
|
||||||
imageUrls,
|
|
||||||
text,
|
|
||||||
}: ChatMessage): React.ReactNode {
|
|
||||||
switch (messageType) {
|
|
||||||
case MessageType.ImageWithText:
|
|
||||||
return (
|
|
||||||
<SimpleControlNetMessage
|
|
||||||
key={id}
|
|
||||||
avatarUrl={senderAvatarUrl}
|
|
||||||
senderName={senderName}
|
|
||||||
createdAt={createdAt}
|
|
||||||
imageUrls={imageUrls ?? []}
|
|
||||||
text={text ?? ''}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case MessageType.Image:
|
|
||||||
return (
|
|
||||||
<SimpleImageMessage
|
|
||||||
key={id}
|
|
||||||
avatarUrl={senderAvatarUrl}
|
|
||||||
senderName={senderName}
|
|
||||||
createdAt={createdAt}
|
|
||||||
imageUrls={imageUrls ?? []}
|
|
||||||
text={text}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case MessageType.Text:
|
|
||||||
return (
|
|
||||||
<SimpleTextMessage
|
|
||||||
key={id}
|
|
||||||
avatarUrl={senderAvatarUrl}
|
|
||||||
senderName={senderName}
|
|
||||||
createdAt={createdAt}
|
|
||||||
senderType={messageSenderType}
|
|
||||||
text={text}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
/* eslint-disable react/display-name */
|
|
||||||
import React, { forwardRef } from 'react'
|
|
||||||
import renderChatMessage from '../ChatBody/renderChatMessage'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
message: ChatMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
type Ref = HTMLDivElement
|
|
||||||
|
|
||||||
const ChatItem = forwardRef<Ref, Props>(({ message }, ref) => {
|
|
||||||
const item = renderChatMessage(message)
|
|
||||||
|
|
||||||
const content = ref ? <div ref={ref}>{item}</div> : item
|
|
||||||
|
|
||||||
return content
|
|
||||||
})
|
|
||||||
|
|
||||||
export default ChatItem
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import React, { Fragment } from 'react'
|
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
|
||||||
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
|
|
||||||
import { useAtom } from 'jotai'
|
|
||||||
import { showConfirmDeleteModalAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
|
|
||||||
const ConfirmDeleteModelModal: React.FC = () => {
|
|
||||||
const [show, setShow] = useAtom(showConfirmDeleteModalAtom)
|
|
||||||
|
|
||||||
const onConfirmDelete = () => {}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={show} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-10" onClose={setShow}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
|
||||||
<div className="sm:flex sm:items-start">
|
|
||||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
||||||
<QuestionMarkCircleIcon
|
|
||||||
className="h-6 w-6 text-green-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="text-base font-semibold leading-6 text-gray-900"
|
|
||||||
>
|
|
||||||
Log out
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Are you sure you want to delete this model?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
|
|
||||||
onClick={onConfirmDelete}
|
|
||||||
>
|
|
||||||
Log out
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
|
||||||
onClick={() => setShow(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(ConfirmDeleteModelModal)
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import React, { Fragment } from 'react'
|
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
|
||||||
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
|
|
||||||
import { useAtom } from 'jotai'
|
|
||||||
import useSignOut from '@hooks/useSignOut'
|
|
||||||
import { showConfirmSignOutModalAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
|
|
||||||
const ConfirmSignOutModal: React.FC = () => {
|
|
||||||
const [show, setShow] = useAtom(showConfirmSignOutModalAtom)
|
|
||||||
const { signOut } = useSignOut()
|
|
||||||
|
|
||||||
const onLogOutClick = () => {
|
|
||||||
signOut().then(() => setShow(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={show} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-10" onClose={setShow}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
|
||||||
<div className="sm:flex sm:items-start">
|
|
||||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
||||||
<QuestionMarkCircleIcon
|
|
||||||
className="h-6 w-6 text-green-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="text-base font-semibold leading-6 text-gray-900"
|
|
||||||
>
|
|
||||||
Log out
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Are you want to logout?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
|
|
||||||
onClick={onLogOutClick}
|
|
||||||
>
|
|
||||||
Log out
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
|
||||||
onClick={() => setShow(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(ConfirmSignOutModal)
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import useCreateConversation from '@hooks/useCreateConversation'
|
|
||||||
import { PlayIcon } from '@heroicons/react/24/outline'
|
|
||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
model: Model
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConversationalCard: React.FC<Props> = ({ model }) => {
|
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
|
||||||
|
|
||||||
const { name, avatarUrl, shortDescription } = model
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => requestCreateConvo(model)}
|
|
||||||
className="flex w-52 flex-shrink-0 flex-col justify-between gap-3 rounded-lg bg-white p-4 text-left hover:opacity-20 dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
<div className="box-border flex flex-col gap-2">
|
|
||||||
<Image
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
src={avatarUrl ?? ''}
|
|
||||||
className="rounded-full"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<h2 className="mt-2 line-clamp-1 font-semibold text-gray-900 dark:text-white">
|
|
||||||
{name}
|
|
||||||
</h2>
|
|
||||||
<span className="mt-1 line-clamp-2 font-normal text-gray-600">
|
|
||||||
{shortDescription}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="flex items-center gap-0.5 text-xs leading-5 text-gray-500">
|
|
||||||
<PlayIcon width={16} height={16} />
|
|
||||||
32.2k runs
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(ConversationalCard)
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
import ConversationalCard from '../ConversationalCard'
|
|
||||||
import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
models: Model[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConversationalList: React.FC<Props> = ({ models }) => (
|
|
||||||
<>
|
|
||||||
<div className="mb-2 mt-8 flex items-center gap-3">
|
|
||||||
<ChatBubbleBottomCenterTextIcon width={24} height={24} className="ml-6" />
|
|
||||||
<span className="font-semibold text-gray-900 dark:text-white">
|
|
||||||
Conversational
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="scroll mt-2 flex w-full gap-2 overflow-hidden overflow-x-scroll pl-6">
|
|
||||||
{models?.map((item) => (
|
|
||||||
<ConversationalCard key={item._id} model={item} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ConversationalList
|
|
||||||
@ -1,175 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import TextInputWithTitle from '../TextInputWithTitle'
|
|
||||||
import TextAreaWithTitle from '../TextAreaWithTitle'
|
|
||||||
import DropdownBox from '../DropdownBox'
|
|
||||||
import PrimaryButton from '../PrimaryButton'
|
|
||||||
import ToggleSwitch from '../ToggleSwitch'
|
|
||||||
import CreateBotPromptInput from '../CreateBotPromptInput'
|
|
||||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
|
||||||
import Avatar from '../Avatar'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import DraggableProgressBar from '../DraggableProgressBar'
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
|
|
||||||
import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
|
|
||||||
import {
|
|
||||||
MainViewState,
|
|
||||||
setMainViewStateAtom,
|
|
||||||
} from '@helpers/atoms/MainView.atom'
|
|
||||||
|
|
||||||
const CreateBotContainer: React.FC = () => {
|
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
|
||||||
const setActiveBot = useSetAtom(activeBotAtom)
|
|
||||||
const setMainViewState = useSetAtom(setMainViewStateAtom)
|
|
||||||
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
|
|
||||||
|
|
||||||
const createBot = async (bot: Bot) => {
|
|
||||||
try {
|
|
||||||
// await executeSerial(DataService.CreateBot, bot)
|
|
||||||
} catch (err) {
|
|
||||||
alert(err)
|
|
||||||
console.error(err)
|
|
||||||
} finally {
|
|
||||||
setMainViewState(MainViewState.BotInfo)
|
|
||||||
setActiveBot(bot)
|
|
||||||
setRightSideBarVisibility(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { handleSubmit, control } = useForm<Bot>({
|
|
||||||
defaultValues: {
|
|
||||||
_id: uuidv4(),
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
visibleFromBotProfile: true,
|
|
||||||
systemPrompt: '',
|
|
||||||
welcomeMessage: '',
|
|
||||||
publiclyAccessible: true,
|
|
||||||
suggestReplies: false,
|
|
||||||
renderMarkdownContent: true,
|
|
||||||
customTemperature: 0.7,
|
|
||||||
enableCustomTemperature: false,
|
|
||||||
maxTokens: 2048,
|
|
||||||
frequencyPenalty: 0,
|
|
||||||
presencePenalty: 0,
|
|
||||||
},
|
|
||||||
mode: 'onChange',
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<Bot> = (data) => {
|
|
||||||
console.log('bot', JSON.stringify(data, null, 2))
|
|
||||||
if (!data.modelId) {
|
|
||||||
alert('Please select a model')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const bot: Bot = {
|
|
||||||
...data,
|
|
||||||
customTemperature: Number(data.customTemperature),
|
|
||||||
maxTokens: Number(data.maxTokens),
|
|
||||||
frequencyPenalty: Number(data.frequencyPenalty),
|
|
||||||
presencePenalty: Number(data.presencePenalty),
|
|
||||||
}
|
|
||||||
createBot(bot)
|
|
||||||
}
|
|
||||||
|
|
||||||
let models = downloadedModels.map((model: { _id: any }) => {
|
|
||||||
return model._id
|
|
||||||
})
|
|
||||||
|
|
||||||
models = ['Select a model', ...models]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className="flex h-full w-full flex-col"
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<div className="mx-6 mt-3 flex items-center justify-between gap-3">
|
|
||||||
<span className="text-lg font-bold">Create Bot</span>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<PrimaryButton isSubmit title="Create" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="scroll flex flex-1 flex-col overflow-y-auto pt-4">
|
|
||||||
<div className="mx-auto flex max-w-2xl flex-col gap-4">
|
|
||||||
<Avatar allowEdit />
|
|
||||||
|
|
||||||
<TextInputWithTitle
|
|
||||||
description="Bot name should be unique, 4-20 characters long, and may include alphanumeric characters, dashes or underscores."
|
|
||||||
title="Bot name"
|
|
||||||
id="name"
|
|
||||||
control={control}
|
|
||||||
required={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextAreaWithTitle
|
|
||||||
id="description"
|
|
||||||
title="Bot description"
|
|
||||||
placeholder="Optional"
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-col pb-2">
|
|
||||||
<DropdownBox
|
|
||||||
id="modelId"
|
|
||||||
title="Model"
|
|
||||||
data={models}
|
|
||||||
control={control}
|
|
||||||
required={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CreateBotPromptInput id="systemPrompt" control={control} required />
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-0.5">
|
|
||||||
<label className="block">Bot access</label>
|
|
||||||
<span className="mb-4 mt-1 text-muted-foreground">
|
|
||||||
If this setting is enabled, the bot will be added to your profile
|
|
||||||
and will be publicly accessible. Turning this off will make the
|
|
||||||
bot private.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<ToggleSwitch
|
|
||||||
id="publiclyAccessible"
|
|
||||||
title="Bot publicly accessible"
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
<DraggableProgressBar
|
|
||||||
id="maxTokens"
|
|
||||||
control={control}
|
|
||||||
min={0}
|
|
||||||
max={4096}
|
|
||||||
step={1}
|
|
||||||
/>
|
|
||||||
<p>Custom temperature</p>
|
|
||||||
<DraggableProgressBar
|
|
||||||
id="customTemperature"
|
|
||||||
control={control}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
<p>Frequency penalty</p>
|
|
||||||
<DraggableProgressBar
|
|
||||||
id="frequencyPenalty"
|
|
||||||
control={control}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
<p>Presence penalty</p>
|
|
||||||
<DraggableProgressBar
|
|
||||||
id="presencePenalty"
|
|
||||||
control={control}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateBotContainer
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
import React, { Fragment, useState } from 'react'
|
|
||||||
import ToggleSwitch from '../ToggleSwitch'
|
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
|
|
||||||
import CutomBotTemperature from '../CustomBotTemperature'
|
|
||||||
import DraggableProgressBar from '../DraggableProgressBar'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
control?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateBotInAdvance: React.FC<Props> = ({ control }) => {
|
|
||||||
const [showAdvanced, setShowAdvanced] = useState(true)
|
|
||||||
|
|
||||||
const handleShowAdvanced = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
setShowAdvanced(!showAdvanced)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<button
|
|
||||||
className="mb-2 flex items-center justify-center font-bold"
|
|
||||||
onClick={handleShowAdvanced}
|
|
||||||
>
|
|
||||||
Advanced
|
|
||||||
{showAdvanced ? (
|
|
||||||
<ChevronDownIcon width={16} className="ml-2" />
|
|
||||||
) : (
|
|
||||||
<ChevronUpIcon width={16} className="ml-2" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showAdvanced && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<p className="text-bold">Max tokens</p>
|
|
||||||
<DraggableProgressBar
|
|
||||||
id="maxTokens"
|
|
||||||
control={control}
|
|
||||||
min={0}
|
|
||||||
max={4096}
|
|
||||||
step={1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-bold">Custom temperature</p>
|
|
||||||
<DraggableProgressBar
|
|
||||||
id="customTemperature"
|
|
||||||
control={control}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-bold">Frequency penalty</p>
|
|
||||||
<DraggableProgressBar
|
|
||||||
id="frequencyPenalty"
|
|
||||||
control={control}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-bold">Presence penalty</p>
|
|
||||||
<DraggableProgressBar
|
|
||||||
id="presencePenalty"
|
|
||||||
control={control}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* {showAdvanced && (
|
|
||||||
<Fragment>
|
|
||||||
<ToggleSwitch
|
|
||||||
id="suggestReplies"
|
|
||||||
title="Suggest replies"
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
<ToggleSwitch
|
|
||||||
id="renderMarkdownContent"
|
|
||||||
title="Render markdown content"
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
<CutomBotTemperature control={control} />
|
|
||||||
</Fragment>
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateBotInAdvance
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import React, { Fragment, use } from 'react'
|
|
||||||
import ToggleSwitch from '../ToggleSwitch'
|
|
||||||
import { useController } from 'react-hook-form'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string
|
|
||||||
control?: any
|
|
||||||
required?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => {
|
|
||||||
const { field } = useController({
|
|
||||||
name: id,
|
|
||||||
control: control,
|
|
||||||
rules: { required: required },
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label htmlFor="comment" className="block font-bold ">
|
|
||||||
Prompt
|
|
||||||
</label>
|
|
||||||
<p className="mt-1 font-normal text-gray-400">
|
|
||||||
All conversations with this bot will start with your prompt but it
|
|
||||||
will not be visible to the user in the chat. If you would like the
|
|
||||||
prompt message to be visible to the user, consider using an intro
|
|
||||||
message instead.
|
|
||||||
</p>
|
|
||||||
<ToggleSwitch
|
|
||||||
id="visibleFromBotProfile"
|
|
||||||
title={'Prompt visible from bot profile'}
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
rows={4}
|
|
||||||
className="block w-full resize-none rounded-md border-0 bg-background/80 py-1.5 text-xs leading-relaxed text-background-reverse shadow-sm ring-1 ring-inset ring-border placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-accent/50"
|
|
||||||
placeholder="Talk to me like a pirate"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateBotPromptInput
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import ToggleSwitch from '../ToggleSwitch'
|
|
||||||
import DraggableProgressBar from '../DraggableProgressBar'
|
|
||||||
import { Controller } from 'react-hook-form'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
control?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
const CutomBotTemperature: React.FC<Props> = ({ control }) => (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<ToggleSwitch
|
|
||||||
id="enableCustomTemperature"
|
|
||||||
title="Custom temperature"
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
<div className="mt-1 text-[0.8em] text-gray-500">
|
|
||||||
{`Controls the creativity of the bot's responses. Higher values produce more
|
|
||||||
varied but unpredictable replies, lower values generate more consistent
|
|
||||||
responses.`}
|
|
||||||
</div>
|
|
||||||
<span className="text-gray-900">default: 0.7</span>
|
|
||||||
<Controller
|
|
||||||
name="enableCustomTemperature"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value } }) => {
|
|
||||||
if (!value) return <div />
|
|
||||||
return (
|
|
||||||
<DraggableProgressBar
|
|
||||||
id="customTemperature"
|
|
||||||
control={control}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default CutomBotTemperature
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import DownloadModelTitle from '../DownloadModelTitle'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
author: string
|
|
||||||
description: string
|
|
||||||
isRecommend: boolean
|
|
||||||
name: string
|
|
||||||
type: string
|
|
||||||
required?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DownloadModelContent: React.FC<Props> = ({
|
|
||||||
author,
|
|
||||||
description,
|
|
||||||
isRecommend,
|
|
||||||
name,
|
|
||||||
required,
|
|
||||||
type,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex w-4/5 flex-col gap-2.5">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<h2 className="text-xl font-medium leading-[25px] tracking-[-0.4px] text-gray-900">
|
|
||||||
{name}
|
|
||||||
</h2>
|
|
||||||
<DownloadModelTitle title={type} />
|
|
||||||
<div className="rounded-md bg-purple-100 px-2.5 py-0.5 text-center">
|
|
||||||
<span className="text-xs font-semibold leading-[18px] text-purple-800">
|
|
||||||
{author}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{required && (
|
|
||||||
<div className="rounded-md bg-purple-100 px-2.5 py-0.5 text-center">
|
|
||||||
<span className="text-xs leading-[18px] text-[#11192899]">
|
|
||||||
Required{' '}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs font-semibold leading-[18px] text-gray-900">
|
|
||||||
{required}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs leading-[18px] text-gray-500">{description}</p>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
isRecommend ? 'flex' : 'hidden'
|
|
||||||
} w-fit items-center justify-center gap-2 rounded-full bg-green-50 px-2.5 py-0.5`}
|
|
||||||
>
|
|
||||||
<div className="h-3 w-3 rounded-full bg-green-400"></div>
|
|
||||||
<span className="leading-18px text-xs font-medium text-green-600">
|
|
||||||
Recommend
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DownloadModelContent
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
type Props = {
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DownloadModelTitle: React.FC<Props> = ({ title }) => (
|
|
||||||
<div className="rounded-md bg-purple-100 px-2.5 py-0.5 text-center">
|
|
||||||
<span className="text-xs font-medium leading-[18px] text-purple-800">
|
|
||||||
{title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default DownloadModelTitle
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
import DownloadModelContent from '../DownloadModelContent'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
model: Model
|
|
||||||
isRecommend: boolean
|
|
||||||
required?: string
|
|
||||||
transferred?: number
|
|
||||||
onDeleteClick?: (model: Model) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const DownloadedModelCard: React.FC<Props> = ({
|
|
||||||
model,
|
|
||||||
isRecommend,
|
|
||||||
required,
|
|
||||||
onDeleteClick,
|
|
||||||
}) => (
|
|
||||||
<div className="rounded-lg border border-gray-200">
|
|
||||||
<div className="flex justify-between gap-2.5 px-3 py-4">
|
|
||||||
<DownloadModelContent
|
|
||||||
required={required}
|
|
||||||
author={model.author}
|
|
||||||
description={model.shortDescription}
|
|
||||||
isRecommend={isRecommend}
|
|
||||||
name={model.name}
|
|
||||||
type={'LLM'}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col justify-center">
|
|
||||||
<button onClick={() => onDeleteClick?.(model)}>Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default DownloadedModelCard
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ModelTable from '../ModelTable'
|
|
||||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
|
||||||
|
|
||||||
const DownloadedModelTable: React.FC = () => {
|
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
|
||||||
|
|
||||||
if (!downloadedModels || downloadedModels.length === 0) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-5">
|
|
||||||
{/* <h3 className="mt-[50px] text-xl leading-[25px]">Downloaded Models</h3> */}
|
|
||||||
{/* <div className="w-[568px] py-5">
|
|
||||||
<SearchBar />
|
|
||||||
</div> */}
|
|
||||||
<ModelTable models={downloadedModels} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DownloadedModelTable
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import React, { Fragment } from 'react'
|
|
||||||
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import ModelDownloadingTable from '../ModelDownloadingTable'
|
|
||||||
|
|
||||||
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="mb-4 mt-[50px] text-xl leading-[25px]">
|
|
||||||
Downloading Models
|
|
||||||
</h3>
|
|
||||||
<ModelDownloadingTable downloadStates={downloadStates} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DownloadingModelTable
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { formatTwoDigits } from '@utils/converter'
|
|
||||||
import React from 'react'
|
|
||||||
import { Controller, useController } from 'react-hook-form'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string
|
|
||||||
control: any
|
|
||||||
min: number
|
|
||||||
max: number
|
|
||||||
step: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const DraggableProgressBar: React.FC<Props> = ({
|
|
||||||
id,
|
|
||||||
control,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
step,
|
|
||||||
}) => {
|
|
||||||
const { field } = useController({
|
|
||||||
name: id,
|
|
||||||
control: control,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
{...field}
|
|
||||||
className="flex-1"
|
|
||||||
type="range"
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
step={step}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name={id}
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<span className="rounded-md border border-border px-2 py-1 text-accent">
|
|
||||||
{formatTwoDigits(value)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DraggableProgressBar
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import React, { Fragment } from 'react'
|
|
||||||
import { useController } from 'react-hook-form'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
data: string[]
|
|
||||||
control?: any
|
|
||||||
required?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const DropdownBox: React.FC<Props> = ({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
data,
|
|
||||||
control,
|
|
||||||
required = false,
|
|
||||||
}) => {
|
|
||||||
const { field } = useController({
|
|
||||||
name: id,
|
|
||||||
control: control,
|
|
||||||
rules: { required: required },
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<label className="block font-bold">{title}</label>
|
|
||||||
<select
|
|
||||||
className="bg-background/80 ring-border focus:ring-accent/50 mt-1 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-xs ring-1 ring-inset focus:ring-2 sm:leading-6"
|
|
||||||
{...field}
|
|
||||||
>
|
|
||||||
{data.map((option) => (
|
|
||||||
<option key={option}>{option}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DropdownBox
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import { Fragment, useState } from 'react'
|
|
||||||
import { Menu, Transition } from '@headlessui/react'
|
|
||||||
import { ChevronDownIcon } from '@heroicons/react/24/outline'
|
|
||||||
|
|
||||||
function classNames(...classes: any) {
|
|
||||||
return classes.filter(Boolean).join(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string
|
|
||||||
data: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DropdownsList: React.FC<Props> = ({ data, title }) => {
|
|
||||||
const [checked, setChecked] = useState(data[0])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu as="div" className="relative w-full text-left">
|
|
||||||
<div className="flex flex-col gap-2 pt-2">
|
|
||||||
<h2 className="text-sm text-[#111928]">{title}</h2>
|
|
||||||
<Menu.Button className="inline-flex w-full items-center justify-between gap-x-1.5 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">
|
|
||||||
{checked}
|
|
||||||
<ChevronDownIcon width={12} height={12} />
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-full origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
||||||
<div className="py-1">
|
|
||||||
{data.map((item, index) => (
|
|
||||||
<Menu.Item key={index}>
|
|
||||||
{({ active }) => (
|
|
||||||
<a
|
|
||||||
onClick={() => setChecked(item)}
|
|
||||||
href="#"
|
|
||||||
className={classNames(
|
|
||||||
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
|
||||||
'block px-4 py-2 text-sm'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import SelectModels from '../ModelSelector'
|
|
||||||
import InputToolbar from '../InputToolbar'
|
|
||||||
|
|
||||||
const EmptyChatContainer: React.FC = () => (
|
|
||||||
<div className="flex flex-1 flex-col h-full w-full">
|
|
||||||
<div className="flex flex-1 items-center justify-center">
|
|
||||||
<SelectModels />
|
|
||||||
</div>
|
|
||||||
<InputToolbar />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default EmptyChatContainer
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExpandableHeader: React.FC<Props> = ({ title }) => (
|
|
||||||
<button className="flex items-center justify-between">
|
|
||||||
<h2 className="text-muted-foreground pl-1 font-bold">{title}</h2>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ExpandableHeader
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import HeaderTitle from '../HeaderTitle'
|
|
||||||
import ExploreModelList from '../../../screens/ExploreModels/ExploreModelList'
|
|
||||||
import ExploreModelFilter from '../ExploreModelFilter'
|
|
||||||
|
|
||||||
const ExploreModelContainer: React.FC = () => (
|
|
||||||
<div className="flex h-full w-full flex-1 flex-col px-16 pt-14">
|
|
||||||
<HeaderTitle title="Explore Models" />
|
|
||||||
{/* <SearchBar
|
|
||||||
type={SearchType.Model}
|
|
||||||
placeholder="Owner name like TheBloke, bhlim etc.."
|
|
||||||
/> */}
|
|
||||||
<div className="mt-9 flex flex-1 gap-x-10 overflow-hidden">
|
|
||||||
<ExploreModelFilter />
|
|
||||||
<ExploreModelList />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ExploreModelContainer
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import SearchBar from '../SearchBar'
|
|
||||||
import SimpleCheckbox from '../SimpleCheckbox'
|
|
||||||
import SimpleTag from '../SimpleTag'
|
|
||||||
import { TagType } from '../SimpleTag/TagType'
|
|
||||||
|
|
||||||
const tags = [
|
|
||||||
'Roleplay',
|
|
||||||
'Llama',
|
|
||||||
'Story',
|
|
||||||
'Casual',
|
|
||||||
'Professional',
|
|
||||||
'CodeLlama',
|
|
||||||
'Coding',
|
|
||||||
]
|
|
||||||
const checkboxs = ['GGUF', 'TensorRT', 'Meow', 'JigglyPuff']
|
|
||||||
|
|
||||||
const ExploreModelFilter: React.FC = () => {
|
|
||||||
const enabled = true
|
|
||||||
if (!enabled) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-64">
|
|
||||||
<h2 className="mb-[15px] text-xs font-semibold">Tags</h2>
|
|
||||||
<SearchBar placeholder="Filter by tags" />
|
|
||||||
<div className="mt-[14px] flex flex-wrap gap-[9px]">
|
|
||||||
{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,153 +0,0 @@
|
|||||||
/* eslint-disable react/display-name */
|
|
||||||
|
|
||||||
'use client'
|
|
||||||
|
|
||||||
import ExploreModelItemHeader from '../ExploreModelItemHeader'
|
|
||||||
import { Button } from '@uikit'
|
|
||||||
import ModelVersionList from '../ModelVersionList'
|
|
||||||
import { forwardRef, useEffect, useState } from 'react'
|
|
||||||
import SimpleTag from '../SimpleTag'
|
|
||||||
import {
|
|
||||||
MiscellanousTag,
|
|
||||||
NumOfBit,
|
|
||||||
QuantMethodTag,
|
|
||||||
RamRequired,
|
|
||||||
UsecaseTag,
|
|
||||||
VersionTag,
|
|
||||||
} from '@/_components/SimpleTag/TagType'
|
|
||||||
import { displayDate } from '@utils/datetime'
|
|
||||||
import useGetMostSuitableModelVersion from '@hooks/useGetMostSuitableModelVersion'
|
|
||||||
import { toGigabytes } from '@utils/converter'
|
|
||||||
import { ModelCatalog } from '@janhq/core/lib/types'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
model: ModelCatalog
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
|
||||||
const [show, setShow] = useState(false)
|
|
||||||
|
|
||||||
const { availableVersions } = model
|
|
||||||
const { suitableModel, getMostSuitableModelVersion } =
|
|
||||||
useGetMostSuitableModelVersion()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getMostSuitableModelVersion(availableVersions)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [availableVersions])
|
|
||||||
|
|
||||||
if (!suitableModel) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const { quantMethod, bits, maxRamRequired, usecase } = suitableModel
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="border-border bg-background/60 mb-4 flex flex-col rounded-md border"
|
|
||||||
>
|
|
||||||
<ExploreModelItemHeader
|
|
||||||
suitableModel={suitableModel}
|
|
||||||
exploreModel={model}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col p-4">
|
|
||||||
<div className="mb-4 flex flex-col gap-1">
|
|
||||||
<span className="font-semibold">About</span>
|
|
||||||
<span className="text-muted-foreground leading-relaxed">
|
|
||||||
{model.longDescription}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="font-semibold">Release Date</div>
|
|
||||||
<p className="text-muted-foreground mt-1">
|
|
||||||
{displayDate(model.releaseDate)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="font-semibold">Version</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<SimpleTag
|
|
||||||
title={model.version}
|
|
||||||
type={VersionTag.Version}
|
|
||||||
clickable={false}
|
|
||||||
/>
|
|
||||||
<SimpleTag
|
|
||||||
title={quantMethod}
|
|
||||||
type={QuantMethodTag.Default}
|
|
||||||
clickable={false}
|
|
||||||
/>
|
|
||||||
<SimpleTag
|
|
||||||
title={`${bits} Bits`}
|
|
||||||
type={NumOfBit.Default}
|
|
||||||
clickable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold">Author</div>
|
|
||||||
<p className="text-muted-foreground mt-1">{model.author}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="font-semibold">Compatibility</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<SimpleTag
|
|
||||||
title={usecase}
|
|
||||||
type={UsecaseTag.UsecaseDefault}
|
|
||||||
clickable={false}
|
|
||||||
/>
|
|
||||||
<SimpleTag
|
|
||||||
title={`${toGigabytes(maxRamRequired)} RAM required`}
|
|
||||||
type={RamRequired.RamDefault}
|
|
||||||
clickable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">Tags</div>
|
|
||||||
<div className="mt-1 flex flex-wrap gap-2">
|
|
||||||
{model.tags.map((tag) => (
|
|
||||||
<SimpleTag
|
|
||||||
key={tag}
|
|
||||||
title={tag}
|
|
||||||
type={MiscellanousTag.MiscellanousDefault}
|
|
||||||
clickable={false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{model.availableVersions?.length > 0 && (
|
|
||||||
<div className="border-border bg-background mt-5 w-full rounded-md border p-2">
|
|
||||||
<button onClick={() => setShow(!show)} className="w-full">
|
|
||||||
{!show
|
|
||||||
? '+ Show Available Versions'
|
|
||||||
: '- Collapse Available Versions'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{show && (
|
|
||||||
<ModelVersionList
|
|
||||||
model={model}
|
|
||||||
versions={model.availableVersions}
|
|
||||||
recommendedVersion={suitableModel?._id ?? ''}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default ExploreModelItem
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
import SimpleTag from '../SimpleTag'
|
|
||||||
import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
|
|
||||||
import { useCallback, useEffect, useMemo } from 'react'
|
|
||||||
import useGetPerformanceTag from '@hooks/useGetPerformanceTag'
|
|
||||||
import useDownloadModel from '@hooks/useDownloadModel'
|
|
||||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
|
||||||
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
|
|
||||||
import { atom, useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
import { Button } from '@uikit'
|
|
||||||
import {
|
|
||||||
MainViewState,
|
|
||||||
setMainViewStateAtom,
|
|
||||||
} from '@helpers/atoms/MainView.atom'
|
|
||||||
import ConfirmationModal from '../ConfirmationModal'
|
|
||||||
import { showingCancelDownloadModalAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
suitableModel: ModelVersion
|
|
||||||
exploreModel: ModelCatalog
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExploreModelItemHeader: React.FC<Props> = ({
|
|
||||||
suitableModel,
|
|
||||||
exploreModel,
|
|
||||||
}) => {
|
|
||||||
const { downloadModel } = useDownloadModel()
|
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
|
||||||
const { performanceTag, title, getPerformanceForModel } =
|
|
||||||
useGetPerformanceTag()
|
|
||||||
const downloadAtom = useMemo(
|
|
||||||
() => atom((get) => get(modelDownloadStateAtom)[suitableModel._id]),
|
|
||||||
[suitableModel._id]
|
|
||||||
)
|
|
||||||
const downloadState = useAtomValue(downloadAtom)
|
|
||||||
const setMainViewState = useSetAtom(setMainViewStateAtom)
|
|
||||||
const setShowingCancelDownloadModal = useSetAtom(
|
|
||||||
showingCancelDownloadModalAtom
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getPerformanceForModel(suitableModel)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [suitableModel])
|
|
||||||
|
|
||||||
const onDownloadClick = useCallback(() => {
|
|
||||||
downloadModel(exploreModel, suitableModel)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [exploreModel, suitableModel])
|
|
||||||
|
|
||||||
const isDownloaded =
|
|
||||||
downloadedModels.find((model) => model._id === suitableModel._id) != null
|
|
||||||
|
|
||||||
let downloadButton = (
|
|
||||||
<Button themes="accent" onClick={() => onDownloadClick()}>
|
|
||||||
{suitableModel.size
|
|
||||||
? `Download (${toGigabytes(suitableModel.size)})`
|
|
||||||
: 'Download'}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isDownloaded) {
|
|
||||||
downloadButton = (
|
|
||||||
<Button
|
|
||||||
themes="accent"
|
|
||||||
onClick={() => {
|
|
||||||
setMainViewState(MainViewState.MyModel)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
View Downloaded Model
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloadState != null) {
|
|
||||||
// downloading
|
|
||||||
downloadButton = (
|
|
||||||
<Button
|
|
||||||
themes="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setShowingCancelDownloadModal(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel ({formatDownloadPercentage(downloadState.percent)})
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelDownloadModal =
|
|
||||||
downloadState != null ? (
|
|
||||||
<ConfirmationModal
|
|
||||||
atom={showingCancelDownloadModalAtom}
|
|
||||||
title="Cancel Download"
|
|
||||||
description={`Are you sure you want to cancel the download of ${downloadState?.fileName}?`}
|
|
||||||
onConfirm={() => {
|
|
||||||
window.coreAPI?.abortDownload(downloadState?.fileName)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between rounded-t-md border-b border-border bg-background/50 px-4 py-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>{exploreModel.name}</span>
|
|
||||||
{performanceTag && (
|
|
||||||
<SimpleTag title={title} type={performanceTag} clickable={false} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{downloadButton}
|
|
||||||
{cancelDownloadModal}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExploreModelItemHeader
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { showingMobilePaneAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
import { Bars3Icon } from '@heroicons/react/24/outline'
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const HamburgerButton: React.FC = () => {
|
|
||||||
const setShowingMobilePane = useSetAtom(showingMobilePaneAtom)
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center justify-center self-end rounded-md p-1 text-gray-700 lg:hidden"
|
|
||||||
onClick={() => setShowingMobilePane(true)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(HamburgerButton)
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import UserProfileDropDown from '../UserProfileDropDown'
|
|
||||||
import LoginButton from '../LoginButton'
|
|
||||||
import HamburgerButton from '../HamburgerButton'
|
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<header className="flex border-b-[1px] border-gray-200 p-3 dark:bg-gray-800">
|
|
||||||
<nav className="flex-1 justify-center">
|
|
||||||
<HamburgerButton />
|
|
||||||
</nav>
|
|
||||||
<div className="h-[30px]" />
|
|
||||||
<LoginButton />
|
|
||||||
<UserProfileDropDown />
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { ArrowLeftIcon } from '@heroicons/react/24/outline'
|
|
||||||
|
|
||||||
const HeaderBackButton: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<button className="flex items-center gap-1">
|
|
||||||
<ArrowLeftIcon width={24} height={24} />
|
|
||||||
<span className="text-sm">Back</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(HeaderBackButton)
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const HeaderTitle: React.FC<Props> = ({ title, className }) => (
|
|
||||||
<h2
|
|
||||||
className={`my-5 text-[34px] font-semibold leading-[41px] tracking-[-0.4px] ${className}`}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default React.memo(HeaderTitle)
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import React, { Fragment } from 'react'
|
|
||||||
import HistoryList from '../HistoryList'
|
|
||||||
import LeftHeaderAction from '../LeftHeaderAction'
|
|
||||||
import { leftSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import { Variants, motion } from 'framer-motion'
|
|
||||||
|
|
||||||
const leftSideBarVariants: Variants = {
|
|
||||||
show: {
|
|
||||||
x: 0,
|
|
||||||
width: 320,
|
|
||||||
opacity: 1,
|
|
||||||
transition: { duration: 0.3 },
|
|
||||||
},
|
|
||||||
hide: {
|
|
||||||
x: '-100%',
|
|
||||||
width: 0,
|
|
||||||
opacity: 0,
|
|
||||||
transition: { duration: 0.3 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const LeftContainer: React.FC = () => {
|
|
||||||
const isVisible = useAtomValue(leftSideBarExpandStateAtom)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={false}
|
|
||||||
animate={isVisible ? 'show' : 'hide'}
|
|
||||||
variants={leftSideBarVariants}
|
|
||||||
className="flex w-80 flex-shrink-0 flex-col dark:bg-gray-950/50"
|
|
||||||
>
|
|
||||||
{isVisible && (
|
|
||||||
<Fragment>
|
|
||||||
{/* <LeftHeaderAction /> */}
|
|
||||||
<HistoryList />
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(LeftContainer)
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useContext } from 'react'
|
|
||||||
import SecondaryButton from '../SecondaryButton'
|
|
||||||
import { useSetAtom, useAtomValue } from 'jotai'
|
|
||||||
import {
|
|
||||||
MainViewState,
|
|
||||||
setMainViewStateAtom,
|
|
||||||
} from '@helpers/atoms/MainView.atom'
|
|
||||||
import { MagnifyingGlassIcon, PlusIcon } from '@heroicons/react/24/outline'
|
|
||||||
import useCreateConversation from '@hooks/useCreateConversation'
|
|
||||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
|
||||||
import { Button } from '@uikit'
|
|
||||||
import { activeModelAtom } from '@helpers/atoms/Model.atom'
|
|
||||||
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
|
|
||||||
import {
|
|
||||||
FeatureToggleContext,
|
|
||||||
} from '@helpers/FeatureToggleWrapper'
|
|
||||||
|
|
||||||
const LeftHeaderAction: React.FC = () => {
|
|
||||||
const setMainView = useSetAtom(setMainViewStateAtom)
|
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
|
||||||
const activeModel = useAtomValue(activeModelAtom)
|
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
|
||||||
const setShowModalNoActiveModel = useSetAtom(showingModalNoActiveModel)
|
|
||||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
|
||||||
|
|
||||||
const onExploreClick = () => {
|
|
||||||
setMainView(MainViewState.ExploreModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onNewConversationClick = () => {
|
|
||||||
if (activeModel) {
|
|
||||||
requestCreateConvo(activeModel)
|
|
||||||
} else {
|
|
||||||
setShowModalNoActiveModel(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCreateBotClicked = () => {
|
|
||||||
if (downloadedModels.length === 0) {
|
|
||||||
alert('You need to download at least one model to create a bot.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setMainView(MainViewState.CreateBot)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="sticky top-0 mb-4 bg-background/90 p-4">
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<SecondaryButton
|
|
||||||
title={'Explore'}
|
|
||||||
onClick={onExploreClick}
|
|
||||||
className="w-full flex-1"
|
|
||||||
icon={<MagnifyingGlassIcon width={16} height={16} />}
|
|
||||||
/>
|
|
||||||
{experimentalFeatureEnabed && (
|
|
||||||
<SecondaryButton
|
|
||||||
title={'Create bot'}
|
|
||||||
onClick={onCreateBotClicked}
|
|
||||||
className="w-full flex-1"
|
|
||||||
icon={<PlusIcon width={16} height={16} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={onNewConversationClick}
|
|
||||||
className="mt-2 flex w-full items-center space-x-2"
|
|
||||||
>
|
|
||||||
<PlusIcon width={16} height={16} />
|
|
||||||
<span>New Conversation</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(LeftHeaderAction)
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
import {
|
|
||||||
MainViewState,
|
|
||||||
getMainViewStateAtom,
|
|
||||||
setMainViewStateAtom,
|
|
||||||
} from '@helpers/atoms/MainView.atom'
|
|
||||||
|
|
||||||
import CompactLogo from '../../../containers/Logo/CompactLogo'
|
|
||||||
import {
|
|
||||||
ChatBubbleOvalLeftEllipsisIcon,
|
|
||||||
Cog8ToothIcon,
|
|
||||||
CpuChipIcon,
|
|
||||||
CubeTransparentIcon,
|
|
||||||
Squares2X2Icon,
|
|
||||||
} from '@heroicons/react/24/outline'
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
|
||||||
import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
|
||||||
import useGetBots from '@hooks/useGetBots'
|
|
||||||
|
|
||||||
import { Icons, Toggle } from '@uikit'
|
|
||||||
|
|
||||||
const menu = [
|
|
||||||
// {
|
|
||||||
// name: 'Explore Models',
|
|
||||||
// icon: <CpuChipIcon />,
|
|
||||||
// state: MainViewState.ExploreModel,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
name: 'My Models',
|
|
||||||
icon: <Icons name="layout-grid" />,
|
|
||||||
state: MainViewState.MyModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Settings',
|
|
||||||
icon: <Icons name="settings" />,
|
|
||||||
state: MainViewState.Setting,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const LeftRibbonNav: React.FC = () => {
|
|
||||||
const currentState = useAtomValue(getMainViewStateAtom)
|
|
||||||
const setMainViewState = useSetAtom(setMainViewStateAtom)
|
|
||||||
const setBotListModal = useSetAtom(showingBotListModalAtom)
|
|
||||||
const { downloadedModels } = useGetDownloadedModels()
|
|
||||||
const { getAllBots } = useGetBots()
|
|
||||||
|
|
||||||
const onMenuClick = (mainViewState: MainViewState) => {
|
|
||||||
if (currentState === mainViewState) return
|
|
||||||
setMainViewState(mainViewState)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isConversationView = currentState === MainViewState.Conversation
|
|
||||||
const bgColor = isConversationView ? 'bg-gray-500' : ''
|
|
||||||
|
|
||||||
const onConversationClick = () => {
|
|
||||||
// if (currentState === MainViewState.Conversation) return
|
|
||||||
setMainViewState(MainViewState.Conversation)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBotListClick = async () => {
|
|
||||||
const bots = await getAllBots()
|
|
||||||
if (bots?.length === 0) {
|
|
||||||
alert('You have no bot')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloadedModels.length === 0) {
|
|
||||||
alert('You have no model downloaded')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setBotListModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="flex h-screen flex-shrink-0 flex-col items-center pt-10">
|
|
||||||
<CompactLogo />
|
|
||||||
<div className="flex w-full flex-1 flex-col items-center justify-between">
|
|
||||||
<div className="flex flex-col pt-4">
|
|
||||||
<button onClick={onConversationClick}>
|
|
||||||
<ChatBubbleOvalLeftEllipsisIcon
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
color="text-white"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button onClick={onBotListClick}>
|
|
||||||
<CubeTransparentIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ul className="flex flex-col gap-3 py-8">
|
|
||||||
{menu.map((item) => {
|
|
||||||
const bgColor = currentState === item.state ? 'bg-gray-500' : ''
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
role="button"
|
|
||||||
key={item.name}
|
|
||||||
className="item-center flex gap-x-2"
|
|
||||||
onClick={() => onMenuClick(item.state)}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
<span className="text-xs">{item.name}</span>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/* User avatar */}
|
|
||||||
{/* <div className="pb-5 flex items-center justify-center">
|
|
||||||
<Image src={"/icons/avatar.svg"} width={40} height={40} alt="" />
|
|
||||||
</div> */}
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LeftRibbonNav
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
const LoginButton: React.FC = () => {
|
|
||||||
// const { signInWithKeyCloak } = useSignIn();
|
|
||||||
// const { user, loading } = useGetCurrentUser();
|
|
||||||
|
|
||||||
// if (loading || user) {
|
|
||||||
// return <div />;
|
|
||||||
// }
|
|
||||||
// return (
|
|
||||||
// <div className="hidden lg:block">
|
|
||||||
// <button
|
|
||||||
// onClick={signInWithKeyCloak}
|
|
||||||
// type="button"
|
|
||||||
// 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
|
|
||||||
// </button>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
return <div />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LoginButton
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import ChatBody from '../ChatBody'
|
|
||||||
import InputToolbar from '../InputToolbar'
|
|
||||||
|
|
||||||
const MainChat: React.FC = () => (
|
|
||||||
<div className="flex h-full w-full flex-col">
|
|
||||||
<ChatBody />
|
|
||||||
<InputToolbar />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default MainChat
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import LeftContainer from '../LeftContainer'
|
|
||||||
import LeftRibbonNav from '../LeftRibbonNav'
|
|
||||||
import MonitorBar from '../MonitorBar'
|
|
||||||
import RightContainer from '../RightContainer'
|
|
||||||
import CenterContainer from '../CenterContainer'
|
|
||||||
|
|
||||||
const MainContainer: React.FC = () => (
|
|
||||||
<div className="flex h-screen">
|
|
||||||
<LeftRibbonNav />
|
|
||||||
<div className="flex flex-1 flex-col ">
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
|
||||||
<LeftContainer />
|
|
||||||
<CenterContainer />
|
|
||||||
<RightContainer />
|
|
||||||
</div>
|
|
||||||
<MonitorBar />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default MainContainer
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import Welcome from '../WelcomeContainer'
|
|
||||||
import { Preferences } from '../Preferences'
|
|
||||||
import MyModelContainer from '../MyModelContainer'
|
|
||||||
import ExploreModelContainer from '../ExploreModelContainer'
|
|
||||||
import {
|
|
||||||
MainViewState,
|
|
||||||
getMainViewStateAtom,
|
|
||||||
} from '@helpers/atoms/MainView.atom'
|
|
||||||
import EmptyChatContainer from '../EmptyChatContainer'
|
|
||||||
import MainChat from '../MainChat'
|
|
||||||
import CreateBotContainer from '../CreateBotContainer'
|
|
||||||
|
|
||||||
const MainView: React.FC = () => {
|
|
||||||
const viewState = useAtomValue(getMainViewStateAtom)
|
|
||||||
|
|
||||||
let children = null
|
|
||||||
switch (viewState) {
|
|
||||||
case MainViewState.ConversationEmptyModel:
|
|
||||||
children = <EmptyChatContainer />
|
|
||||||
break
|
|
||||||
case MainViewState.ExploreModel:
|
|
||||||
children = <ExploreModelContainer />
|
|
||||||
break
|
|
||||||
case MainViewState.Setting:
|
|
||||||
children = <Preferences />
|
|
||||||
break
|
|
||||||
case MainViewState.ResourceMonitor:
|
|
||||||
case MainViewState.MyModel:
|
|
||||||
children = <MyModelContainer />
|
|
||||||
break
|
|
||||||
case MainViewState.CreateBot:
|
|
||||||
children = <CreateBotContainer />
|
|
||||||
break
|
|
||||||
case MainViewState.Welcome:
|
|
||||||
children = <Welcome />
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
children = <MainChat />
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="flex-1 overflow-hidden">{children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MainView
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import { Popover, Transition } from '@headlessui/react'
|
|
||||||
import { Fragment } from 'react'
|
|
||||||
// import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
import { showConfirmSignOutModalAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
|
|
||||||
export const MenuHeader: React.FC = () => {
|
|
||||||
const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom)
|
|
||||||
// const { user } = useGetCurrentUser();
|
|
||||||
|
|
||||||
return <div></div>
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Transition
|
|
||||||
// as={Fragment}
|
|
||||||
// enter="transition ease-out duration-200"
|
|
||||||
// enterFrom="opacity-0 translate-y-1"
|
|
||||||
// enterTo="opacity-100 translate-y-0"
|
|
||||||
// leave="transition ease-in duration-150"
|
|
||||||
// leaveFrom="opacity-100 translate-y-0"
|
|
||||||
// leaveTo="opacity-0 translate-y-1"
|
|
||||||
// >
|
|
||||||
// <Popover.Panel className="absolute shadow-profile -right-2 top-full z-10 mt-3 w-[224px] overflow-hidden rounded-[6px] bg-white shadow-lg ring-1 ring-gray-200">
|
|
||||||
// <div className="py-3 px-4 gap-2 flex flex-col">
|
|
||||||
// <h2 className="text-[20px] leading-[25px] tracking-[-0.4px] font-bold text-[#111928]">
|
|
||||||
// {user.displayName}
|
|
||||||
// </h2>
|
|
||||||
// <span className="text-[#6B7280] leading-[17.5px] text-sm">
|
|
||||||
// {user.email}
|
|
||||||
// </span>
|
|
||||||
// </div>
|
|
||||||
// <hr />
|
|
||||||
// <button
|
|
||||||
// onClick={() => setShowConfirmSignOutModal(true)}
|
|
||||||
// className="px-4 py-3 text-sm w-full text-left text-gray-700"
|
|
||||||
// >
|
|
||||||
// Sign Out
|
|
||||||
// </button>
|
|
||||||
// <hr />
|
|
||||||
// <div className="flex gap-2 px-4 py-2 justify-center items-center">
|
|
||||||
// <Link href="/privacy">
|
|
||||||
// <span className="text-[#6B7280] text-xs">Privacy</span>
|
|
||||||
// </Link>
|
|
||||||
// <div className="w-1 h-1 bg-[#D9D9D9] rounded-lg" />
|
|
||||||
// <Link href="/support">
|
|
||||||
// <span className="text-[#6B7280] text-xs">Support</span>
|
|
||||||
// </Link>
|
|
||||||
// </div>
|
|
||||||
// </Popover.Panel>
|
|
||||||
// </Transition>
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import React, { useRef } from 'react'
|
|
||||||
import { Dialog } from '@headlessui/react'
|
|
||||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import { useAtom } from 'jotai'
|
|
||||||
import { showingMobilePaneAtom } from '@helpers/atoms/Modal.atom'
|
|
||||||
|
|
||||||
const MobileMenuPane: React.FC = () => {
|
|
||||||
const [show, setShow] = useAtom(showingMobilePaneAtom)
|
|
||||||
let loginRef = useRef(null)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
open={show}
|
|
||||||
initialFocus={loginRef}
|
|
||||||
onClose={() => setShow(false)}
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 z-10" />
|
|
||||||
<Dialog.Panel className="fixed inset-y-0 right-0 z-10 w-full overflow-y-auto bg-white px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<a href="#" className="-m-1.5 p-1.5">
|
|
||||||
<span className="sr-only">Your Company</span>
|
|
||||||
<Image
|
|
||||||
className="h-8 w-auto"
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
src="icons/app_icon.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="-m-2.5 rounded-md p-2.5 text-gray-700"
|
|
||||||
onClick={() => setShow(false)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close menu</span>
|
|
||||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 flow-root">
|
|
||||||
<div className="-my-6 divide-y divide-gray-500/10">
|
|
||||||
<div className="space-y-2 py-6" />
|
|
||||||
<div className="py-6">
|
|
||||||
<a
|
|
||||||
ref={loginRef}
|
|
||||||
href="#"
|
|
||||||
className="-mx-3 block rounded-lg px-3 py-2.5 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
Log in
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MobileMenuPane
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Button } from '@uikit'
|
|
||||||
import ModelActionMenu from '../ModelActionMenu'
|
|
||||||
|
|
||||||
export enum ModelActionType {
|
|
||||||
Start = 'Start',
|
|
||||||
Stop = 'Stop',
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModelActionStyle = {
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelActionMapper: Record<ModelActionType, ModelActionStyle> = {
|
|
||||||
[ModelActionType.Start]: {
|
|
||||||
title: 'Start',
|
|
||||||
},
|
|
||||||
[ModelActionType.Stop]: {
|
|
||||||
title: 'Stop',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
disabled?: boolean
|
|
||||||
loading?: boolean
|
|
||||||
type: ModelActionType
|
|
||||||
onActionClick: (type: ModelActionType) => void
|
|
||||||
onDeleteClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelActionButton: React.FC<Props> = ({
|
|
||||||
disabled,
|
|
||||||
loading,
|
|
||||||
type,
|
|
||||||
onActionClick,
|
|
||||||
onDeleteClick,
|
|
||||||
}) => {
|
|
||||||
const styles = modelActionMapper[type]
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
onActionClick(type)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<td className="whitespace-nowrap px-3 py-2 text-right">
|
|
||||||
<div className="flex items-center justify-end gap-x-4">
|
|
||||||
<ModelActionMenu onDeleteClick={onDeleteClick} />
|
|
||||||
<Button
|
|
||||||
disabled={disabled}
|
|
||||||
size="sm"
|
|
||||||
themes={styles.title === 'Start' ? 'accent' : 'default'}
|
|
||||||
onClick={() => onClick()}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
{styles.title} Model
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModelActionButton
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { Menu, Transition } from '@headlessui/react'
|
|
||||||
import { EllipsisVerticalIcon } from '@heroicons/react/20/solid'
|
|
||||||
import { Fragment } from 'react'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDeleteClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => (
|
|
||||||
<button className="text-muted-foreground text-xs" onClick={onDeleteClick}>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ModelActionMenu
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
callback: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelDownloadButton: React.FC<Props> = ({ callback }) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="flex items-center gap-2 rounded-lg bg-[#1A56DB] px-3 py-2"
|
|
||||||
onClick={callback}
|
|
||||||
>
|
|
||||||
<ArrowDownTrayIcon width={16} height={16} color="#FFFFFF" />
|
|
||||||
<span className="text-xs font-medium leading-[18px] text-[#fff]">
|
|
||||||
Download
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModelDownloadButton
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { toGigabytes } from '@utils/converter'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
total: number
|
|
||||||
value: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelDownloadingButton: React.FC<Props> = ({ total, value }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<button className="flex gap-2 rounded-lg border border-gray-200 px-3 py-2 text-xs leading-[18px]">
|
|
||||||
Downloading...
|
|
||||||
</button>
|
|
||||||
<div className="rounded bg-gray-200 px-2.5 py-0.5">
|
|
||||||
<span className="text-xs font-medium text-gray-800">
|
|
||||||
{toGigabytes(value)} / {toGigabytes(total)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModelDownloadingButton
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
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:rounded-lg last:border-b-0"
|
|
||||||
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
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ModelTableHeader from '../ModelTableHeader'
|
|
||||||
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 min-w-full rounded-lg border border-gray-200 align-middle shadow-lg">
|
|
||||||
<table className="min-w-full">
|
|
||||||
<thead className="border-b border-gray-200 bg-gray-50">
|
|
||||||
<tr className="rounded-t-lg">
|
|
||||||
{tableHeaders.map((item) => (
|
|
||||||
<ModelTableHeader key={item} title={item} />
|
|
||||||
))}
|
|
||||||
<th scope="col" className="relative w-fit px-6 py-3">
|
|
||||||
<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,15 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelInfoItem: React.FC<Props> = ({ description, name }) => (
|
|
||||||
<div className="flex flex-1 flex-col">
|
|
||||||
<span className="text-sm font-normal text-gray-500">{name}</span>
|
|
||||||
<span className="text-sm font-normal">{description}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default React.memo(ModelInfoItem)
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import React, { useCallback } from 'react'
|
|
||||||
import { ModelStatus, ModelStatusComponent } from '../ModelStatusComponent'
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import ModelActionButton, { ModelActionType } from '../ModelActionButton'
|
|
||||||
import useStartStopModel from '@hooks/useStartStopModel'
|
|
||||||
import useDeleteModel from '@hooks/useDeleteModel'
|
|
||||||
import { activeModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
|
||||||
import { toGigabytes } from '@utils/converter'
|
|
||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
model: Model
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelRow: React.FC<Props> = ({ model }) => {
|
|
||||||
const { startModel, stopModel } = useStartStopModel()
|
|
||||||
const activeModel = useAtomValue(activeModelAtom)
|
|
||||||
const { deleteModel } = useDeleteModel()
|
|
||||||
const { loading, model: currentModelState } = useAtomValue(stateModel)
|
|
||||||
|
|
||||||
let status = ModelStatus.Installed
|
|
||||||
if (activeModel && activeModel._id === model._id) {
|
|
||||||
status = ModelStatus.Active
|
|
||||||
}
|
|
||||||
|
|
||||||
let actionButtonType = ModelActionType.Start
|
|
||||||
if (activeModel && activeModel._id === model._id) {
|
|
||||||
actionButtonType = ModelActionType.Stop
|
|
||||||
}
|
|
||||||
|
|
||||||
const onModelActionClick = (action: ModelActionType) => {
|
|
||||||
if (action === ModelActionType.Start) {
|
|
||||||
startModel(model._id)
|
|
||||||
} else {
|
|
||||||
stopModel(model._id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDeleteClick = useCallback(() => {
|
|
||||||
deleteModel(model)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [model])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className="border-b border-border bg-background/50 last:rounded-lg last:border-b-0">
|
|
||||||
<td className="whitespace-nowrap px-3 font-semibold text-muted-foreground">
|
|
||||||
{model.name}
|
|
||||||
<span className="ml-2 font-semibold">v{model.version}</span>
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap px-3 text-muted-foreground">
|
|
||||||
<div className="flex flex-col justify-start">
|
|
||||||
<span>GGUF</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap px-3 text-muted-foreground">
|
|
||||||
{toGigabytes(model.size)}
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap px-3 text-muted-foreground">
|
|
||||||
<ModelStatusComponent status={status} />
|
|
||||||
</td>
|
|
||||||
<ModelActionButton
|
|
||||||
disabled={loading}
|
|
||||||
loading={currentModelState === model._id ? loading : false}
|
|
||||||
type={actionButtonType}
|
|
||||||
onActionClick={onModelActionClick}
|
|
||||||
onDeleteClick={onDeleteClick}
|
|
||||||
/>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModelRow
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { searchingModelText } from '@helpers/JotaiWrapper'
|
|
||||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
|
|
||||||
import { useSetAtom } from 'jotai'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
const ModelSearchBar: React.FC = () => {
|
|
||||||
const setSearchtext = useSetAtom(searchingModelText)
|
|
||||||
const [text, setText] = useState('')
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchtext(text)
|
|
||||||
}, [text, setSearchtext])
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center py-[27px]">
|
|
||||||
<div className="flex h-[42px] w-[520px] items-center">
|
|
||||||
<input
|
|
||||||
className="h-full flex-1 rounded-bl-lg rounded-tl-lg border border-gray-300 bg-gray-300 px-4 py-3 text-sm leading-[17.5px] outline-none"
|
|
||||||
placeholder="Search model"
|
|
||||||
value={text}
|
|
||||||
onChange={(text) => setText(text.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<button className="flex h-[42px] w-[42px] items-center justify-center rounded-br-lg rounded-tr-lg border border-gray-800 bg-gray-800 p-2">
|
|
||||||
<MagnifyingGlassIcon width={20} height={20} color="#FFFFFF" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModelSearchBar
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
import { Fragment, useEffect } from 'react'
|
|
||||||
import { Listbox, Transition } from '@headlessui/react'
|
|
||||||
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
|
||||||
import { selectedModelAtom } from '@helpers/atoms/Model.atom'
|
|
||||||
import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom'
|
|
||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
|
|
||||||
function classNames(...classes: any) {
|
|
||||||
return classes.filter(Boolean).join(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
const SelectModels: React.FC = () => {
|
|
||||||
const downloadedModels = useAtomValue(downloadedModelAtom)
|
|
||||||
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (downloadedModels && downloadedModels.length > 0) {
|
|
||||||
onModelSelected(downloadedModels[0])
|
|
||||||
}
|
|
||||||
}, [downloadedModels])
|
|
||||||
|
|
||||||
const onModelSelected = (model: Model) => {
|
|
||||||
setSelectedModel(model)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedModel) {
|
|
||||||
return <div>You have not downloaded any model!</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Listbox value={selectedModel} onChange={onModelSelected}>
|
|
||||||
{({ open }) => (
|
|
||||||
<div className="w-[461px]">
|
|
||||||
<Listbox.Label className="block text-sm font-medium leading-6 text-gray-900">
|
|
||||||
Select a Model:
|
|
||||||
</Listbox.Label>
|
|
||||||
<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-blue-500 sm:text-sm sm:leading-6">
|
|
||||||
<span className="flex items-center">
|
|
||||||
<img
|
|
||||||
src={selectedModel.avatarUrl}
|
|
||||||
alt=""
|
|
||||||
className="h-5 w-5 flex-shrink-0 rounded-full"
|
|
||||||
/>
|
|
||||||
<span className="ml-3 block truncate">
|
|
||||||
{selectedModel.name}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">
|
|
||||||
<ChevronUpDownIcon
|
|
||||||
className="h-5 w-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options className="absolute z-10 mt-1 max-h-[188px] w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
|
||||||
{downloadedModels.map((model) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={model._id}
|
|
||||||
className={({ active }) =>
|
|
||||||
classNames(
|
|
||||||
active ? 'bg-blue-600 text-white' : 'text-gray-900',
|
|
||||||
'relative cursor-default select-none py-2 pl-3 pr-9'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={model}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<img
|
|
||||||
src={model.avatarUrl}
|
|
||||||
alt=""
|
|
||||||
className="h-5 w-5 flex-shrink-0 rounded-full"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
selected ? 'font-semibold' : 'font-normal',
|
|
||||||
'ml-3 block truncate'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{model.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
active ? 'text-white' : 'text-blue-600',
|
|
||||||
'absolute inset-y-0 right-0 flex items-center pr-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SelectModels
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
export type ModelStatusType = {
|
|
||||||
title: string
|
|
||||||
textColor: string
|
|
||||||
backgroundColor: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ModelStatus {
|
|
||||||
Installed,
|
|
||||||
Active,
|
|
||||||
RunningInNitro,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ModelStatusMapper: Record<ModelStatus, ModelStatusType> = {
|
|
||||||
[ModelStatus.Installed]: {
|
|
||||||
title: 'Installed',
|
|
||||||
textColor: 'text-black',
|
|
||||||
backgroundColor: 'bg-gray-200 text-gray-600',
|
|
||||||
},
|
|
||||||
[ModelStatus.Active]: {
|
|
||||||
title: 'Active',
|
|
||||||
textColor: 'text-green-800',
|
|
||||||
backgroundColor: 'bg-green-100 dark:bg-green-300 text-green-700',
|
|
||||||
},
|
|
||||||
[ModelStatus.RunningInNitro]: {
|
|
||||||
title: 'Running in Nitro',
|
|
||||||
textColor: 'text-green-800',
|
|
||||||
backgroundColor: 'bg-green-100 dark:bg-green-300 text-green-700',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
status: ModelStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ModelStatusComponent: React.FC<Props> = ({ status }) => {
|
|
||||||
const statusType = ModelStatusMapper[status]
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`w-fit rounded-full px-2.5 py-0.5 text-xs font-medium ${statusType.backgroundColor}`}
|
|
||||||
>
|
|
||||||
{statusType.title}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ModelRow from '../ModelRow'
|
|
||||||
import ModelTableHeader from '../ModelTableHeader'
|
|
||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
models: Model[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableHeaders = ['MODEL', 'FORMAT', 'SIZE', 'STATUS', 'ACTIONS']
|
|
||||||
|
|
||||||
const ModelTable: React.FC<Props> = ({ models }) => (
|
|
||||||
<>
|
|
||||||
<div className="overflow-hidden rounded-lg border border-border align-middle shadow-lg">
|
|
||||||
<table className="min-w-full">
|
|
||||||
<thead className="bg-background">
|
|
||||||
<tr className="rounded-t-lg">
|
|
||||||
{tableHeaders.map((item) => (
|
|
||||||
<ModelTableHeader key={item} title={item} />
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{models?.map((model) => (
|
|
||||||
<ModelRow key={model._id} model={model} />
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div className="relative"></div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default React.memo(ModelTable)
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelTableHeader: React.FC<Props> = ({ title }) => (
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="text-muted-foreground border-border border-b p-3 text-left text-xs font-semibold uppercase first:rounded-tl-lg last:rounded-tr-lg last:text-right"
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</th>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default React.memo(ModelTableHeader)
|
|
||||||
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