Relayout hub screen

This commit is contained in:
Faisal Amir 2023-12-04 18:38:06 +07:00
parent 5d5139ad20
commit 47c15fd550
12 changed files with 230 additions and 101 deletions

View File

@ -183,6 +183,11 @@ export interface Model {
*/ */
version: number; version: number;
/**
* The format of the model.
*/
format: string;
/** /**
* The model download source. It can be an external url or a local filepath. * The model download source. It can be an external url or a local filepath.
*/ */

View File

@ -10,6 +10,7 @@ const badgeVariants = cva('badge', {
secondary: 'badge-secondary', secondary: 'badge-secondary',
danger: 'badge-danger', danger: 'badge-danger',
outline: 'badge-outline', outline: 'badge-outline',
pink: 'badge-pink',
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@ -2,7 +2,11 @@
@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; @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 { &-primary {
@apply bg-primary text-primary-foreground hover:bg-primary/80 border-transparent; @apply border-transparent bg-blue-100 text-blue-600;
}
&-pink {
@apply border-transparent bg-pink-100 text-pink-700;
} }
&-success { &-success {
@ -14,7 +18,7 @@
} }
&-danger { &-danger {
@apply bg-danger text-danger-foreground hover:bg-danger/80 border-transparent; @apply border-transparent bg-red-100 text-red-700;
} }
&-outline { &-outline {

View File

@ -13,6 +13,7 @@ const buttonVariants = cva('btn', {
danger: 'btn-danger', danger: 'btn-danger',
outline: 'btn-outline', outline: 'btn-outline',
secondary: 'btn-secondary', secondary: 'btn-secondary',
secondaryBlue: 'btn-secondary-blue',
ghost: 'btn-ghost', ghost: 'btn-ghost',
success: 'btn-success', success: 'btn-success',
}, },

View File

@ -1,5 +1,5 @@
.btn { .btn {
@apply inline-flex items-center justify-center whitespace-nowrap rounded-md font-semibold transition-colors; @apply inline-flex items-center justify-center whitespace-nowrap rounded-lg font-semibold transition-colors;
@apply focus-visible:ring-ring cursor-pointer focus-visible:outline-none focus-visible:ring-1; @apply focus-visible:ring-ring cursor-pointer focus-visible:outline-none focus-visible:ring-1;
@apply disabled:pointer-events-none disabled:opacity-50; @apply disabled:pointer-events-none disabled:opacity-50;
@ -7,6 +7,10 @@
@apply bg-primary hover:bg-primary/90 text-white; @apply bg-primary hover:bg-primary/90 text-white;
} }
&-secondary-blue {
@apply bg-blue-200 text-blue-900 hover:bg-blue-500/80;
}
&-danger { &-danger {
@apply bg-danger text-danger-foreground hover:bg-danger/90; @apply bg-danger text-danger-foreground hover:bg-danger/90;
} }
@ -62,6 +66,9 @@
&.btn-secondary { &.btn-secondary {
@apply bg-secondary hover:bg-secondary/80; @apply bg-secondary hover:bg-secondary/80;
} }
&.btn-secondary-blue {
@apply bg-blue-200 text-blue-900 hover:bg-blue-200/80;
}
&.btn-danger { &.btn-danger {
@apply bg-danger hover:bg-danger/90; @apply bg-danger hover:bg-danger/90;
} }

View File

@ -1,5 +1,5 @@
.select { .select {
@apply ring-offset-background placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1; @apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1;
&-caret { &-caret {
@apply h-4 w-4 opacity-50; @apply h-4 w-4 opacity-50;
@ -18,7 +18,7 @@
} }
&-item { &-item {
@apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50; @apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
} }
&-trigger-viewport { &-trigger-viewport {

View File

@ -48,8 +48,8 @@ export default function CardSidebar({
> >
<ChevronDownIcon <ChevronDownIcon
className={twMerge( className={twMerge(
'h-5 w-5 flex-none rotate-180 text-gray-400', 'h-5 w-5 flex-none text-gray-400',
show && 'rotate-0' show && 'rotate-180'
)} )}
/> />
<span className="font-bold">{title}</span> <span className="font-bold">{title}</span>

View File

@ -12,6 +12,7 @@ import {
ModalHeader, ModalHeader,
Button, Button,
ModalTitle, ModalTitle,
Progress,
} from '@janhq/uikit' } from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai' import { atom, useAtomValue } from 'jotai'
@ -21,7 +22,6 @@ import { useDownloadState } from '@/hooks/useDownloadState'
import { formatDownloadPercentage } from '@/utils/converter' import { formatDownloadPercentage } from '@/utils/converter'
import { extensionManager } from '@/extension' import { extensionManager } from '@/extension'
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
type Props = { type Props = {
model: Model model: Model
@ -46,7 +46,20 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
{cancelText} {cancelText}
</Button> </Button>
) : ( ) : (
<Button>{cancelText}</Button> <Button themes="secondaryBlue">
<div className="flex items-center space-x-2">
<span className="inline-block">Cancel</span>
<Progress
className="inline-block h-2 w-[80px] bg-blue-100"
value={
formatDownloadPercentage(downloadState?.percent, {
hidePercentage: true,
}) as number
}
/>
<span>{formatDownloadPercentage(downloadState.percent)}</span>
</div>
</Button>
)} )}
</ModalTrigger> </ModalTrigger>
<ModalContent> <ModalContent>

View File

@ -1,6 +1,6 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { forwardRef } from 'react' import { forwardRef, useState } from 'react'
import { Model } from '@janhq/core' import { Model } from '@janhq/core'
import { Badge } from '@janhq/uikit' import { Badge } from '@janhq/uikit'
@ -12,58 +12,78 @@ type Props = {
} }
const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => { const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
const [open, setOpen] = useState('')
const handleToggle = () => {
if (open === model.id) {
setOpen('')
} else {
setOpen(model.id)
}
}
return ( return (
<div <div
ref={ref} ref={ref}
className="mb-6 flex flex-col overflow-hidden rounded-xl border border-border bg-background/60" className="mb-6 flex flex-col overflow-hidden rounded-xl border border-border bg-background/60"
> >
<ExploreModelItemHeader model={model} /> <ExploreModelItemHeader
<div className="flex flex-col p-4"> model={model}
<div className="mb-4 flex flex-col gap-1"> onClick={handleToggle}
<span className="font-semibold">About</span> open={open}
<p>{model.description}</p> />
</div> {open === model.id && (
<div className="flex">
<div className="mb-4 flex space-x-6 border-b border-border pb-4"> <div className="flex w-full flex-col border-t border-border p-4 ">
<div> <div className="mb-6 flex flex-col gap-1">
<span className="font-semibold">Author</span> <span className="font-semibold">About</span>
<p className="mt-1 font-medium">{model.metadata.author}</p> <p className="text-muted-foreground">
{model.description || '-'}
</p>
</div>
<div className="flex space-x-10">
<div>
<span className="font-semibold text-muted-foreground">
Author
</span>
<p className="mt-2 font-medium">{model.metadata.author}</p>
</div>
<div>
<span className="mb-1 font-semibold text-muted-foreground">
Model ID
</span>
<p className="mt-2 font-medium">{model.id}</p>
</div>
<div>
<span className="mb-1 font-semibold text-muted-foreground">
Tags
</span>
<div className="mt-2 flex space-x-2">
{model.metadata.tags.map((tag, i) => (
<Badge key={i} themes={i === 0 ? 'primary' : 'pink'}>
{tag}
</Badge>
))}
</div>
</div>
</div>
</div> </div>
<div> <div className="w-48 flex-shrink-0 border-l border-t border-border p-4">
<span className="mb-1 font-semibold">Compatibility</span> <div>
<div className="mt-1 flex gap-2"> <span className="font-semibold text-muted-foreground">
{/* <Badge Format
themes="secondary" </span>
className="line-clamp-1 lg:line-clamp-none" <p className="mt-2 font-medium uppercase">{model.format}</p>
title={`${toGigabytes( </div>
model.metadata.maxRamRequired // TODO: check this <div className="mt-4">
)} RAM required`} <span className="font-semibold text-muted-foreground">
> Compatibility
{toGigabytes(model.metadata.maxRamRequired)} RAM required </span>
</Badge> */} <p className="mt-2 font-medium">-</p>
</div> </div>
</div> </div>
</div> </div>
)}
<div className="grid grid-cols-3 items-center gap-4">
<div>
<span className="font-semibold">Version</span>
<div className="mt-2 flex space-x-2">
<Badge themes="outline">v{model.version}</Badge>
</div>
</div>
<div>
<span className="font-semibold">Tags</span>
<div className="mt-2 flex space-x-2">
{model.metadata.tags.map((tag, i) => (
<Badge key={i} themes="outline">
{tag}
</Badge>
))}
</div>
</div>
</div>
</div>
</div> </div>
) )
}) })

View File

@ -1,16 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo } from 'react'
import { Model } from '@janhq/core' import { Model } from '@janhq/core'
import { Badge, Button } from '@janhq/uikit' import { Badge, Button } from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai' import { atom, useAtomValue } from 'jotai'
import { ChevronDownIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import ModalCancelDownload from '@/containers/ModalCancelDownload' import ModalCancelDownload from '@/containers/ModalCancelDownload'
import { MainViewState } from '@/constants/screens' import { MainViewState } from '@/constants/screens'
import { ModelPerformance, TagType } from '@/constants/tagType' // import { ModelPerformance, TagType } from '@/constants/tagType'
import useDownloadModel from '@/hooks/useDownloadModel' import useDownloadModel from '@/hooks/useDownloadModel'
import { useDownloadState } from '@/hooks/useDownloadState' import { useDownloadState } from '@/hooks/useDownloadState'
@ -21,18 +25,20 @@ import { toGigabytes } from '@/utils/converter'
type Props = { type Props = {
model: Model model: Model
onClick: () => void
open: string
} }
const ExploreModelItemHeader: React.FC<Props> = ({ model }) => { const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
console.log(model)
const { downloadModel } = useDownloadModel() const { downloadModel } = useDownloadModel()
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const { modelDownloadStateAtom, downloadStates } = useDownloadState() const { modelDownloadStateAtom, downloadStates } = useDownloadState()
const [title, setTitle] = useState<string>('Recommended') // const [title, setTitle] = useState<string>('Recommended')
// const [performanceTag, setPerformanceTag] = useState<TagType>(
// ModelPerformance.PerformancePositive
// )
const [performanceTag, setPerformanceTag] = useState<TagType>(
ModelPerformance.PerformancePositive
)
const downloadAtom = useMemo( const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.id]), () => atom((get) => get(modelDownloadStateAtom)[model.id]),
[model.id] [model.id]
@ -48,18 +54,14 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model }) => {
const isDownloaded = downloadedModels.find((md) => md.id === model.id) != null const isDownloaded = downloadedModels.find((md) => md.id === model.id) != null
let downloadButton = ( let downloadButton = (
<Button onClick={() => onDownloadClick()}> <Button onClick={() => onDownloadClick()}>Download</Button>
{model.metadata.size
? `Download (${toGigabytes(model.metadata.size)})`
: 'Download'}
</Button>
) )
if (isDownloaded) { if (isDownloaded) {
downloadButton = ( downloadButton = (
<Button <Button
themes="success" themes="success"
className="min-w-[80px]" className="min-w-[98px]"
onClick={() => { onClick={() => {
setMainViewState(MainViewState.MyModels) setMainViewState(MainViewState.MyModels)
}} }}
@ -73,30 +75,42 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model }) => {
downloadButton = <ModalCancelDownload model={model} /> downloadButton = <ModalCancelDownload model={model} />
} }
const renderBadge = (performance: TagType) => { // const renderBadge = (performance: TagType) => {
switch (performance) { // switch (performance) {
case ModelPerformance.PerformancePositive: // case ModelPerformance.PerformancePositive:
return <Badge themes="success">{title}</Badge> // return <Badge themes="success">{title}</Badge>
case ModelPerformance.PerformanceNeutral: // case ModelPerformance.PerformanceNeutral:
return <Badge themes="secondary">{title}</Badge> // return <Badge themes="secondary">{title}</Badge>
case ModelPerformance.PerformanceNegative: // case ModelPerformance.PerformanceNegative:
return <Badge themes="danger">{title}</Badge> // return <Badge themes="danger">{title}</Badge>
default: // default:
break // break
} // }
} // }
return ( 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 cursor-pointer items-center justify-between rounded-t-md bg-background/50 px-4 py-4"
onClick={onClick}
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium">{model.name}</span> <span className="font-bold">{model.name}</span>
<Badge>{model.metadata.tags[0]}</Badge>
</div> </div>
<div className="space-x-2"> <div className="inline-flex items-center space-x-2">
{performanceTag && renderBadge(performanceTag)} <span className="font-semibold text-muted-foreground">
{toGigabytes(model.metadata.size)}
</span>
{downloadButton} {downloadButton}
<ChevronDownIcon
className={twMerge(
'h-5 w-5 flex-none text-gray-400',
open === model.id && 'rotate-180'
)}
/>
</div> </div>
</div> </div>
) )

View File

@ -6,10 +6,14 @@ type Props = {
models: Model[] models: Model[]
} }
const ExploreModelList: React.FC<Props> = ({ models }) => ( const ExploreModelList: React.FC<Props> = ({ models }) => {
<div className="relative h-full w-full flex-shrink-0"> return (
{models?.map((model) => <ExploreModelItem key={model.id} model={model} />)} <div className="relative h-full w-full flex-shrink-0">
</div> {models
) ?.sort((a, b) => a.metadata.size - b.metadata.size)
?.map((model) => <ExploreModelItem key={model.id} model={model} />)}
</div>
)
}
export default ExploreModelList export default ExploreModelList

View File

@ -1,6 +1,19 @@
import { useState } from 'react' import { useState } from 'react'
import { Input, ScrollArea } from '@janhq/uikit' import {
Input,
ScrollArea,
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipArrow,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectGroup,
SelectItem,
} from '@janhq/uikit'
import { SearchIcon } from 'lucide-react' import { SearchIcon } from 'lucide-react'
@ -12,12 +25,29 @@ import Loader from '@/containers/Loader'
import { useGetConfiguredModels } from '@/hooks/useGetConfiguredModels' import { useGetConfiguredModels } from '@/hooks/useGetConfiguredModels'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import ExploreModelList from './ExploreModelList' import ExploreModelList from './ExploreModelList'
const ExploreModelsScreen = () => { const ExploreModelsScreen = () => {
const { loading, models } = useGetConfiguredModels() const { loading, models } = useGetConfiguredModels()
const [searchValue, setsearchValue] = useState('') const [searchValue, setsearchValue] = useState('')
const [tabActive, setTabActive] = useState('Model') const [tabActive, setTabActive] = useState('Model')
const { downloadedModels } = useGetDownloadedModels()
const [sortSelected, setSortSelected] = useState('All Model')
const sortMenu = ['All Model', 'Downloaded']
const filteredModels = models.filter((x) => {
if (sortSelected === 'Downloaded') {
return (
x.name.toLowerCase().includes(searchValue.toLowerCase()) &&
downloadedModels.some((y) => y.id === x.id)
)
} else {
return x.name.toLowerCase().includes(searchValue.toLowerCase())
}
})
if (loading) return <Loader description="loading ..." /> if (loading) return <Loader description="loading ..." />
return ( return (
@ -52,23 +82,53 @@ const ExploreModelsScreen = () => {
onClick={() => setTabActive('Model')} onClick={() => setTabActive('Model')}
> >
<Code2Icon size={20} className="text-muted-foreground" /> <Code2Icon size={20} className="text-muted-foreground" />
<span>Model</span> <span className="font-semibold">Model</span>
</div>
<div
className={twMerge(
'pointer-events-none flex cursor-pointer items-center space-x-2 px-3 py-2',
tabActive === 'Assistant' && 'bg-secondary'
)}
onClick={() => setTabActive('Assistant')}
>
<UserIcon size={20} className="text-muted-foreground" />
<span>Assistant</span>
</div> </div>
<Tooltip>
<TooltipTrigger>
<div
className={twMerge(
'pointer-events-none flex cursor-pointer items-center space-x-2 px-3 py-2 text-muted-foreground',
tabActive === 'Assistant' && 'bg-secondary'
)}
onClick={() => setTabActive('Assistant')}
>
<UserIcon size={20} className="text-muted-foreground" />
<span className="font-semibold">Assistant</span>
</div>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={10}>
<span className="font-bold">Coming Soon</span>
<TooltipArrow />
</TooltipContent>
</Tooltip>
</div> </div>
<Select
value={sortSelected}
onValueChange={(value) => {
setSortSelected(value)
}}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Sort By"></SelectValue>
</SelectTrigger>
<SelectContent className="right-0 block w-full min-w-[200px] pr-0">
<SelectGroup>
{sortMenu.map((x, i) => {
return (
<SelectItem key={i} value={x}>
<span className="line-clamp-1 block">{x}</span>
</SelectItem>
)
})}
</SelectGroup>
</SelectContent>
</Select>
</div> </div>
<div className="mt-6"> <div className="mt-6">
<ExploreModelList models={models} /> <ExploreModelList models={filteredModels} />
</div> </div>
</div> </div>
</ScrollArea> </ScrollArea>