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:
Faisal Amir 2023-11-07 21:27:11 +07:00 committed by GitHub
parent fc3352b75c
commit 2394c13065
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
267 changed files with 4407 additions and 6491 deletions

View File

@ -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

View File

@ -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",

View File

@ -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...
}); });

View File

@ -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...
}); });

View File

@ -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
View File

@ -0,0 +1,5 @@
.next/
node_modules/
dist/
*.hbs
*.mdx

8
uikit/.prettierrc Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
module.exports = {
plugins: {
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
"postcss-import": {},
},
};

View 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 }

View 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
View 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 }

View 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;
}
}

View 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 }

View 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
View 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,
}

View 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
View 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,
}

View 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
View 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
View 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 }

View 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
View 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
View 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,
}

View 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;
}
}

View 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 }

View 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;
}
}

View 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 }

View 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;
}
}

View 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 }

View 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;
}
}

View 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,
}

View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
declare module '*.scss' {
const content: Record<string, string>;
export default content;
}

144
web/.eslintrc.js Normal file
View 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',
},
],
},
}

View File

@ -1,6 +0,0 @@
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-img-element": "off"
}
}

View File

@ -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"]
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>
)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>
// );
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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