Relayout hub screen
This commit is contained in:
parent
5d5139ad20
commit
47c15fd550
@ -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.
|
||||
*/
|
||||
|
||||
@ -10,6 +10,7 @@ const badgeVariants = cva('badge', {
|
||||
secondary: 'badge-secondary',
|
||||
danger: 'badge-danger',
|
||||
outline: 'badge-outline',
|
||||
pink: 'badge-pink',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user