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;
/**
* The format of the model.
*/
format: string;
/**
* 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',
danger: 'badge-danger',
outline: 'badge-outline',
pink: 'badge-pink',
},
},
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;
&-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 {
@ -14,7 +18,7 @@
}
&-danger {
@apply bg-danger text-danger-foreground hover:bg-danger/80 border-transparent;
@apply border-transparent bg-red-100 text-red-700;
}
&-outline {

View File

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

View File

@ -1,5 +1,5 @@
.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 disabled:pointer-events-none disabled:opacity-50;
@ -7,6 +7,10 @@
@apply bg-primary hover:bg-primary/90 text-white;
}
&-secondary-blue {
@apply bg-blue-200 text-blue-900 hover:bg-blue-500/80;
}
&-danger {
@apply bg-danger text-danger-foreground hover:bg-danger/90;
}
@ -62,6 +66,9 @@
&.btn-secondary {
@apply bg-secondary hover:bg-secondary/80;
}
&.btn-secondary-blue {
@apply bg-blue-200 text-blue-900 hover:bg-blue-200/80;
}
&.btn-danger {
@apply bg-danger hover:bg-danger/90;
}

View File

@ -1,5 +1,5 @@
.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 {
@apply h-4 w-4 opacity-50;
@ -18,7 +18,7 @@
}
&-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 {

View File

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

View File

@ -12,6 +12,7 @@ import {
ModalHeader,
Button,
ModalTitle,
Progress,
} from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai'
@ -21,7 +22,6 @@ import { useDownloadState } from '@/hooks/useDownloadState'
import { formatDownloadPercentage } from '@/utils/converter'
import { extensionManager } from '@/extension'
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
type Props = {
model: Model
@ -46,7 +46,20 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
{cancelText}
</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>
<ModalContent>

View File

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

View File

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

View File

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

View File

@ -1,6 +1,19 @@
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'
@ -12,12 +25,29 @@ import Loader from '@/containers/Loader'
import { useGetConfiguredModels } from '@/hooks/useGetConfiguredModels'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import ExploreModelList from './ExploreModelList'
const ExploreModelsScreen = () => {
const { loading, models } = useGetConfiguredModels()
const [searchValue, setsearchValue] = useState('')
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 ..." />
return (
@ -52,23 +82,53 @@ const ExploreModelsScreen = () => {
onClick={() => setTabActive('Model')}
>
<Code2Icon size={20} className="text-muted-foreground" />
<span>Model</span>
<span className="font-semibold">Model</span>
</div>
<Tooltip>
<TooltipTrigger>
<div
className={twMerge(
'pointer-events-none flex cursor-pointer items-center space-x-2 px-3 py-2',
'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>Assistant</span>
<span className="font-semibold">Assistant</span>
</div>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={10}>
<span className="font-bold">Coming Soon</span>
<TooltipArrow />
</TooltipContent>
</Tooltip>
</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 className="mt-6">
<ExploreModelList models={models} />
<ExploreModelList models={filteredModels} />
</div>
</div>
</ScrollArea>