✨enhancement: support base layout responsive UI (#5472)
* ✨enhancement: support base layout responsive UI * Update web-app/src/containers/LeftPanel.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update web-app/src/containers/ThreadList.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ✨enhancement: responsive assistant screen (#5502) * ✨enhancement: support base layout responsive UI * Update web-app/src/containers/LeftPanel.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update web-app/src/containers/ThreadList.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ✨enhancement: responsive assistant screen * Update web-app/src/containers/dialogs/AddEditAssistant.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ✨enhancement: sort assistant * Update web-app/src/routes/assistant.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ✨enhancement: responsive hub screen (#5507) * ✨enhancement: support base layout responsive UI * Update web-app/src/containers/LeftPanel.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update web-app/src/containers/ThreadList.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ✨enhancement: responsive assistant screen * Update web-app/src/containers/dialogs/AddEditAssistant.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ✨enhancement: sort assistant * Update web-app/src/routes/assistant.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ✨enhancement: responsive hub screen * 🧹cleanup: multiple key and useless for hub translation --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
parent
f46d45e786
commit
9bbf9a590c
@ -17,6 +17,8 @@
|
|||||||
"label": "main",
|
"label": "main",
|
||||||
"title": "Jan",
|
"title": "Jan",
|
||||||
"width": 1024,
|
"width": 1024,
|
||||||
|
"minWidth": 375,
|
||||||
|
"minHeight": 667,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
|
|||||||
@ -67,7 +67,7 @@ function DialogContent({
|
|||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
aria-describedby={ariaDescribedBy}
|
aria-describedby={ariaDescribedBy}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-main-view max-h-[calc(100%-48px)] overflow-auto border-main-view-fg/10 text-main-view-fg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
'bg-main-view max-h-[calc(100%-80px)] overflow-auto border-main-view-fg/10 text-main-view-fg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import {
|
|||||||
import { useThreads } from '@/hooks/useThreads'
|
import { useThreads } from '@/hooks/useThreads'
|
||||||
|
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState, useEffect, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@ -40,6 +40,8 @@ import {
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { DownloadManagement } from '@/containers/DownloadManegement'
|
import { DownloadManagement } from '@/containers/DownloadManegement'
|
||||||
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
|
import { useClickOutside } from '@/hooks/useClickOutside'
|
||||||
|
|
||||||
const mainMenus = [
|
const mainMenus = [
|
||||||
{
|
{
|
||||||
@ -70,6 +72,68 @@ const LeftPanel = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
|
||||||
|
const isSmallScreen = useSmallScreen()
|
||||||
|
const prevScreenSizeRef = useRef<boolean | null>(null)
|
||||||
|
const isInitialMountRef = useRef(true)
|
||||||
|
const panelRef = useRef<HTMLElement>(null)
|
||||||
|
const searchContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const searchContainerMacRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Use click outside hook for panel with debugging
|
||||||
|
useClickOutside(
|
||||||
|
() => {
|
||||||
|
if (isSmallScreen && open) {
|
||||||
|
setLeftPanel(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
panelRef.current,
|
||||||
|
searchContainerRef.current,
|
||||||
|
searchContainerMacRef.current,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-collapse panel only when window is resized
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
const currentIsSmallScreen = window.innerWidth <= 768
|
||||||
|
|
||||||
|
// Skip on initial mount
|
||||||
|
if (isInitialMountRef.current) {
|
||||||
|
isInitialMountRef.current = false
|
||||||
|
prevScreenSizeRef.current = currentIsSmallScreen
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only trigger if the screen size actually changed
|
||||||
|
if (
|
||||||
|
prevScreenSizeRef.current !== null &&
|
||||||
|
prevScreenSizeRef.current !== currentIsSmallScreen
|
||||||
|
) {
|
||||||
|
if (currentIsSmallScreen) {
|
||||||
|
setLeftPanel(false)
|
||||||
|
} else {
|
||||||
|
setLeftPanel(true)
|
||||||
|
}
|
||||||
|
prevScreenSizeRef.current = currentIsSmallScreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resize listener
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
// Initialize the previous screen size on mount
|
||||||
|
if (isInitialMountRef.current) {
|
||||||
|
prevScreenSizeRef.current = window.innerWidth <= 768
|
||||||
|
isInitialMountRef.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
}, [setLeftPanel])
|
||||||
|
|
||||||
const currentPath = useRouterState({
|
const currentPath = useRouterState({
|
||||||
select: (state) => state.location.pathname,
|
select: (state) => state.location.pathname,
|
||||||
})
|
})
|
||||||
@ -91,50 +155,63 @@ const LeftPanel = () => {
|
|||||||
return filteredThreads.filter((t) => !t.isFavorite)
|
return filteredThreads.filter((t) => !t.isFavorite)
|
||||||
}, [filteredThreads])
|
}, [filteredThreads])
|
||||||
|
|
||||||
return (
|
// Disable body scroll when panel is open on small screens
|
||||||
<aside
|
useEffect(() => {
|
||||||
className={cn(
|
if (isSmallScreen && open) {
|
||||||
'w-48 shrink-0 rounded-lg m-1.5 mr-0 text-left-panel-fg',
|
document.body.style.overflow = 'hidden'
|
||||||
open
|
} else {
|
||||||
? 'opacity-100 visibility-visible'
|
document.body.style.overflow = ''
|
||||||
: 'w-0 absolute -top-100 -left-100 visibility-hidden'
|
}
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="relative h-10">
|
|
||||||
<button
|
|
||||||
className="absolute top-1/2 right-0 -translate-y-1/2 z-20"
|
|
||||||
onClick={() => setLeftPanel(!open)}
|
|
||||||
>
|
|
||||||
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out">
|
|
||||||
<IconLayoutSidebar size={18} className="text-left-panel-fg" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{!IS_MACOS && (
|
|
||||||
<div className="relative top-1.5 mb-4 mx-1 mt-1 w-[calc(100%-32px)] z-50">
|
|
||||||
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={t('common:search')}
|
|
||||||
className="w-full pl-7 pr-8 py-1 bg-left-panel-fg/10 rounded-sm text-left-panel-fg focus:outline-none focus:ring-1 focus:ring-left-panel-fg/10"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
{searchTerm && (
|
|
||||||
<button
|
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-left-panel-fg/70 hover:text-left-panel-fg"
|
|
||||||
onClick={() => setSearchTerm('')}
|
|
||||||
>
|
|
||||||
<IconX size={14} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col justify-between h-[calc(100%-42px)] mt-0">
|
return () => {
|
||||||
<div className="flex flex-col justify-between h-full">
|
document.body.style.overflow = ''
|
||||||
{IS_MACOS && (
|
}
|
||||||
<div className="relative mb-4 mx-1 mt-1">
|
}, [isSmallScreen, open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Backdrop overlay for small screens */}
|
||||||
|
{isSmallScreen && open && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur z-30"
|
||||||
|
onClick={(e) => {
|
||||||
|
// Don't close if clicking on search container or if currently searching
|
||||||
|
if (
|
||||||
|
searchContainerRef.current?.contains(e.target as Node) ||
|
||||||
|
searchContainerMacRef.current?.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLeftPanel(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<aside
|
||||||
|
ref={panelRef}
|
||||||
|
className={cn(
|
||||||
|
'w-48 shrink-0 rounded-lg m-1.5 mr-0 text-left-panel-fg overflow-hidden',
|
||||||
|
isSmallScreen &&
|
||||||
|
'fixed h-[calc(100%-16px)] bg-main-view z-40 rounded-sm border border-left-panel-fg/10 m-2 px-1',
|
||||||
|
open
|
||||||
|
? 'opacity-100 visibility-visible'
|
||||||
|
: 'w-0 absolute -top-100 -left-100 visibility-hidden'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative h-10">
|
||||||
|
<button
|
||||||
|
className="absolute top-1/2 right-0 -translate-y-1/2 z-20"
|
||||||
|
onClick={() => setLeftPanel(!open)}
|
||||||
|
>
|
||||||
|
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10">
|
||||||
|
<IconLayoutSidebar size={18} className="text-left-panel-fg" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{!IS_MACOS && (
|
||||||
|
<div
|
||||||
|
ref={searchContainerRef}
|
||||||
|
className="relative top-1.5 mb-4 mx-1 mt-1 w-[calc(100%-32px)] z-50"
|
||||||
|
data-ignore-outside-clicks
|
||||||
|
>
|
||||||
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -146,176 +223,226 @@ const LeftPanel = () => {
|
|||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<button
|
<button
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-left-panel-fg/70 hover:text-left-panel-fg"
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-left-panel-fg/70 hover:text-left-panel-fg"
|
||||||
onClick={() => setSearchTerm('')}
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation() // prevent bubbling
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<IconX size={14} />
|
<IconX size={14} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col w-full h-full overflow-hidden">
|
</div>
|
||||||
<div className="h-full overflow-y-auto overflow-x-hidden">
|
|
||||||
{favoritedThreads.length > 0 && (
|
<div className="flex flex-col justify-between overflow-hidden mt-0 !h-[calc(100%-42px)]">
|
||||||
<>
|
<div className="flex flex-col !h-[calc(100%-140px)]">
|
||||||
|
{IS_MACOS && (
|
||||||
|
<div
|
||||||
|
ref={searchContainerMacRef}
|
||||||
|
className="relative mb-4 mx-1 mt-1"
|
||||||
|
data-ignore-outside-clicks
|
||||||
|
>
|
||||||
|
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('common:search')}
|
||||||
|
className="w-full pl-7 pr-8 py-1 bg-left-panel-fg/10 rounded-sm text-left-panel-fg focus:outline-none focus:ring-1 focus:ring-left-panel-fg/10"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
data-ignore-outside-clicks
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-left-panel-fg/70 hover:text-left-panel-fg"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation() // prevent bubbling
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col w-full overflow-y-auto overflow-x-hidden">
|
||||||
|
<div className="h-full w-full overflow-y-auto">
|
||||||
|
{favoritedThreads.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold sticky top-0">
|
||||||
|
{t('common:favorites')}
|
||||||
|
</span>
|
||||||
|
<div className="relative">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10">
|
||||||
|
<IconDots
|
||||||
|
size={18}
|
||||||
|
className="text-left-panel-fg/60"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="bottom" align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
unstarAllThreads()
|
||||||
|
toast.success(
|
||||||
|
t('common:toast.allThreadsUnfavorited.title'),
|
||||||
|
{
|
||||||
|
id: 'unfav-all-threads',
|
||||||
|
description: t(
|
||||||
|
'common:toast.allThreadsUnfavorited.description'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconStar size={16} />
|
||||||
|
<span>{t('common:unstarAll')}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col mb-4">
|
||||||
|
<ThreadList
|
||||||
|
threads={favoritedThreads}
|
||||||
|
isFavoriteSection={true}
|
||||||
|
/>
|
||||||
|
{favoritedThreads.length === 0 && (
|
||||||
|
<p className="text-xs text-left-panel-fg/50 px-1 font-semibold">
|
||||||
|
{t('chat.status.empty', { ns: 'chat' })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{unFavoritedThreads.length > 0 && (
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold sticky top-0">
|
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold">
|
||||||
{t('common:favorites')}
|
{t('common:recents')}
|
||||||
</span>
|
</span>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<DropdownMenu>
|
<Dialog>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<button className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10">
|
<DropdownMenuTrigger asChild>
|
||||||
<IconDots
|
<button
|
||||||
size={18}
|
className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10"
|
||||||
className="text-left-panel-fg/60"
|
onClick={(e) => {
|
||||||
/>
|
e.preventDefault()
|
||||||
</button>
|
e.stopPropagation()
|
||||||
</DropdownMenuTrigger>
|
}}
|
||||||
<DropdownMenuContent side="bottom" align="end">
|
>
|
||||||
<DropdownMenuItem
|
<IconDots
|
||||||
onClick={() => {
|
size={18}
|
||||||
unstarAllThreads()
|
className="text-left-panel-fg/60"
|
||||||
toast.success(t('common:toast.allThreadsUnfavorited.title'), {
|
/>
|
||||||
id: 'unfav-all-threads',
|
</button>
|
||||||
description: t('common:toast.allThreadsUnfavorited.description'),
|
</DropdownMenuTrigger>
|
||||||
})
|
<DropdownMenuContent side="bottom" align="end">
|
||||||
}}
|
<DialogTrigger asChild>
|
||||||
>
|
<DropdownMenuItem
|
||||||
<IconStar size={16} />
|
onSelect={(e) => e.preventDefault()}
|
||||||
<span>{t('common:unstarAll')}</span>
|
>
|
||||||
</DropdownMenuItem>
|
<IconTrash size={16} />
|
||||||
</DropdownMenuContent>
|
<span>{t('common:deleteAll')}</span>
|
||||||
</DropdownMenu>
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t('common:dialogs.deleteAllThreads.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t(
|
||||||
|
'common:dialogs.deleteAllThreads.description'
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogFooter className="mt-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="hover:no-underline"
|
||||||
|
>
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
deleteAllThreads()
|
||||||
|
toast.success(
|
||||||
|
t(
|
||||||
|
'common:toast.deleteAllThreads.title'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: 'delete-all-thread',
|
||||||
|
description: t(
|
||||||
|
'common:toast.deleteAllThreads.description'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate({ to: route.home })
|
||||||
|
}, 0)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common:deleteAll')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col mb-4">
|
)}
|
||||||
<ThreadList
|
|
||||||
threads={favoritedThreads}
|
|
||||||
isFavoriteSection={true}
|
|
||||||
/>
|
|
||||||
{favoritedThreads.length === 0 && (
|
|
||||||
<p className="text-xs text-left-panel-fg/50 px-1 font-semibold">
|
|
||||||
{t('chat.status.empty', { ns: 'chat' })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{unFavoritedThreads.length > 0 && (
|
{filteredThreads.length === 0 && searchTerm.length > 0 && (
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold">
|
|
||||||
{t('common:recents')}
|
|
||||||
</span>
|
|
||||||
<div className="relative">
|
|
||||||
<Dialog>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconDots
|
|
||||||
size={18}
|
|
||||||
className="text-left-panel-fg/60"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent side="bottom" align="end">
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
<span>{t('common:deleteAll')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{t('common:dialogs.deleteAllThreads.title')}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t(
|
|
||||||
'common:dialogs.deleteAllThreads.description'
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
|
||||||
<DialogFooter className="mt-2">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="hover:no-underline"
|
|
||||||
>
|
|
||||||
{t('common:cancel')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
deleteAllThreads()
|
|
||||||
toast.success(t('common:toast.deleteAllThreads.title'), {
|
|
||||||
id: 'delete-all-thread',
|
|
||||||
description: t('common:toast.deleteAllThreads.description'),
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
navigate({ to: route.home })
|
|
||||||
}, 0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('common:deleteAll')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{filteredThreads.length === 0 && searchTerm.length > 0 && (
|
|
||||||
<div className="px-1 mt-2">
|
|
||||||
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
|
||||||
<IconSearch size={18} />
|
|
||||||
<h6 className="font-medium text-base">
|
|
||||||
{t('common:noResultsFound')}
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
|
||||||
{t('common:noResultsFoundDesc')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{Object.keys(threads).length === 0 && !searchTerm && (
|
|
||||||
<>
|
|
||||||
<div className="px-1 mt-2">
|
<div className="px-1 mt-2">
|
||||||
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
||||||
<IconMessageFilled size={18} />
|
<IconSearch size={18} />
|
||||||
<h6 className="font-medium text-base">
|
<h6 className="font-medium text-base">
|
||||||
{t('common:noThreadsYet')}
|
{t('common:noResultsFound')}
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
||||||
{t('common:noThreadsYetDesc')}
|
{t('common:noResultsFoundDesc')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col">
|
{Object.keys(threads).length === 0 && !searchTerm && (
|
||||||
<ThreadList threads={unFavoritedThreads} />
|
<>
|
||||||
|
<div className="px-1 mt-2">
|
||||||
|
<div className="flex items-center gap-1 text-left-panel-fg/80">
|
||||||
|
<IconMessageFilled size={18} />
|
||||||
|
<h6 className="font-medium text-base">
|
||||||
|
{t('common:noThreadsYet')}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
|
||||||
|
{t('common:noThreadsYetDesc')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<ThreadList threads={unFavoritedThreads} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 py-1 mt-2">
|
<div className="space-y-1 shrink-0 py-1 mt-2">
|
||||||
{mainMenus.map((menu) => {
|
{mainMenus.map((menu) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
currentPath.includes(route.settings.index) &&
|
currentPath.includes(route.settings.index) &&
|
||||||
@ -324,6 +451,7 @@ const LeftPanel = () => {
|
|||||||
<Link
|
<Link
|
||||||
key={menu.title}
|
key={menu.title}
|
||||||
to={menu.route}
|
to={menu.route}
|
||||||
|
onClick={() => isSmallScreen && setLeftPanel(false)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded',
|
'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded',
|
||||||
isActive
|
isActive
|
||||||
@ -341,8 +469,8 @@ const LeftPanel = () => {
|
|||||||
</div>
|
</div>
|
||||||
<DownloadManagement />
|
<DownloadManagement />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
</aside>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,8 +20,10 @@ import {
|
|||||||
IconStar,
|
IconStar,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { useThreads } from '@/hooks/useThreads'
|
import { useThreads } from '@/hooks/useThreads'
|
||||||
|
import { useLeftPanel } from '@/hooks/useLeftPanel'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -55,6 +57,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
isDragging,
|
isDragging,
|
||||||
} = useSortable({ id: thread.id, disabled: true })
|
} = useSortable({ id: thread.id, disabled: true })
|
||||||
|
|
||||||
|
const isSmallScreen = useSmallScreen()
|
||||||
|
const { setLeftPanel } = useLeftPanel()
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
@ -75,7 +80,11 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
navigate({ to: route.threadsDetail, params: { threadId: thread.id } })
|
// Only close panel and navigate if the thread is not already active
|
||||||
|
if (!isActive) {
|
||||||
|
if (isSmallScreen) setLeftPanel(false)
|
||||||
|
navigate({ to: route.threadsDetail, params: { threadId: thread.id } })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +94,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
|
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
|
||||||
}, [thread.title])
|
}, [thread.title])
|
||||||
|
|
||||||
const [title, setTitle] = useState(plainTitleForRename || t('common:newThread'))
|
const [title, setTitle] = useState(
|
||||||
|
plainTitleForRename || t('common:newThread')
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -185,7 +196,10 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
setOpenDropdown(false)
|
setOpenDropdown(false)
|
||||||
toast.success(t('common:toast.renameThread.title'), {
|
toast.success(t('common:toast.renameThread.title'), {
|
||||||
id: 'rename-thread',
|
id: 'rename-thread',
|
||||||
description: t('common:toast.renameThread.description', { title }),
|
description: t(
|
||||||
|
'common:toast.renameThread.description',
|
||||||
|
{ title }
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -231,7 +245,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
setOpenDropdown(false)
|
setOpenDropdown(false)
|
||||||
toast.success(t('common:toast.deleteThread.title'), {
|
toast.success(t('common:toast.deleteThread.title'), {
|
||||||
id: 'delete-thread',
|
id: 'delete-thread',
|
||||||
description: t('common:toast.deleteThread.description'),
|
description: t(
|
||||||
|
'common:toast.deleteThread.description'
|
||||||
|
),
|
||||||
})
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate({ to: route.home })
|
navigate({ to: route.home })
|
||||||
|
|||||||
@ -378,73 +378,27 @@ export default function AddEditAssistant({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{paramsKeys.map((key, index) => (
|
{paramsKeys.map((key, index) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-4">
|
||||||
<Input
|
<div
|
||||||
value={key}
|
key={index}
|
||||||
onChange={(e) =>
|
className="flex items-center flex-col sm:flex-row w-full gap-2"
|
||||||
handleParameterChange(index, e.target.value, 'key')
|
>
|
||||||
}
|
<Input
|
||||||
placeholder={t('assistants:key')}
|
value={key}
|
||||||
className="w-24"
|
onChange={(e) =>
|
||||||
/>
|
handleParameterChange(index, e.target.value, 'key')
|
||||||
|
}
|
||||||
|
placeholder={t('assistants:key')}
|
||||||
|
className="w-full sm:w-24"
|
||||||
|
/>
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<div className="relative w-30">
|
|
||||||
<Input
|
|
||||||
value={
|
|
||||||
paramsTypes[index].charAt(0).toUpperCase() +
|
|
||||||
paramsTypes[index].slice(1)
|
|
||||||
}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<IconChevronDown
|
|
||||||
size={14}
|
|
||||||
className="text-main-view-fg/50 absolute right-2 top-1/2 -translate-y-1/2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="w-32" align="start">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
handleParameterChange(index, 'string', 'type')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('assistants:stringValue')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
handleParameterChange(index, 'number', 'type')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('assistants:numberValue')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
handleParameterChange(index, 'boolean', 'type')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('assistants:booleanValue')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
handleParameterChange(index, 'json', 'type')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('assistants:jsonValue')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
{paramsTypes[index] === 'boolean' ? (
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="relative flex-1">
|
<div className="relative w-full sm:w-30">
|
||||||
<Input
|
<Input
|
||||||
value={
|
value={
|
||||||
paramsValues[index]
|
paramsTypes[index].charAt(0).toUpperCase() +
|
||||||
? t('assistants:trueValue')
|
paramsTypes[index].slice(1)
|
||||||
: t('assistants:falseValue')
|
|
||||||
}
|
}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
@ -454,48 +408,98 @@ export default function AddEditAssistant({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-24" align="start">
|
<DropdownMenuContent className="w-32" align="start">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleParameterChange(index, true, 'value')
|
handleParameterChange(index, 'string', 'type')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('assistants:trueValue')}
|
{t('assistants:stringValue')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleParameterChange(index, false, 'value')
|
handleParameterChange(index, 'number', 'type')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('assistants:falseValue')}
|
{t('assistants:numberValue')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handleParameterChange(index, 'boolean', 'type')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('assistants:booleanValue')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handleParameterChange(index, 'json', 'type')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('assistants:jsonValue')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : paramsTypes[index] === 'json' ? (
|
|
||||||
<Input
|
|
||||||
value={
|
|
||||||
typeof paramsValues[index] === 'object'
|
|
||||||
? JSON.stringify(paramsValues[index], null, 2)
|
|
||||||
: paramsValues[index]?.toString() || ''
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleParameterChange(index, e.target.value, 'value')
|
|
||||||
}
|
|
||||||
placeholder={t('assistants:jsonValuePlaceholder')}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
value={paramsValues[index]?.toString() || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleParameterChange(index, e.target.value, 'value')
|
|
||||||
}
|
|
||||||
type={paramsTypes[index] === 'number' ? 'number' : 'text'}
|
|
||||||
placeholder={t('assistants:value')}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{paramsTypes[index] === 'boolean' ? (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<div className="relative sm:flex-1 w-full">
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
paramsValues[index]
|
||||||
|
? t('assistants:trueValue')
|
||||||
|
: t('assistants:falseValue')
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<IconChevronDown
|
||||||
|
size={14}
|
||||||
|
className="text-main-view-fg/50 absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-24" align="start">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handleParameterChange(index, true, 'value')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('assistants:trueValue')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handleParameterChange(index, false, 'value')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('assistants:falseValue')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : paramsTypes[index] === 'json' ? (
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
typeof paramsValues[index] === 'object'
|
||||||
|
? JSON.stringify(paramsValues[index], null, 2)
|
||||||
|
: paramsValues[index]?.toString() || ''
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleParameterChange(index, e.target.value, 'value')
|
||||||
|
}
|
||||||
|
placeholder={t('assistants:jsonValuePlaceholder')}
|
||||||
|
className="sm:flex-1 h-[36px] w-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={paramsValues[index]?.toString() || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleParameterChange(index, e.target.value, 'value')
|
||||||
|
}
|
||||||
|
type={paramsTypes[index] === 'number' ? 'number' : 'text'}
|
||||||
|
placeholder={t('assistants:value')}
|
||||||
|
className="sm:flex-1 h-[36px] w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
onClick={() => handleRemoveParameter(index)}
|
onClick={() => handleRemoveParameter(index)}
|
||||||
|
|||||||
42
web-app/src/hooks/useClickOutside.ts
Normal file
42
web-app/src/hooks/useClickOutside.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
const DEFAULT_EVENTS = ['mousedown', 'touchstart']
|
||||||
|
|
||||||
|
export function useClickOutside<T extends HTMLElement = any>(
|
||||||
|
handler: () => void,
|
||||||
|
events?: string[] | null,
|
||||||
|
nodes?: (HTMLElement | null)[]
|
||||||
|
) {
|
||||||
|
const ref = useRef<T>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (event: any) => {
|
||||||
|
const { target } = event ?? {}
|
||||||
|
if (Array.isArray(nodes)) {
|
||||||
|
const shouldIgnore =
|
||||||
|
target?.hasAttribute('data-ignore-outside-clicks') ||
|
||||||
|
(!document.body.contains(target) && target.tagName !== 'HTML')
|
||||||
|
const shouldTrigger = nodes.every(
|
||||||
|
(node) => !!node && !event.composedPath().includes(node)
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
|
shouldTrigger && !shouldIgnore && handler()
|
||||||
|
} else if (ref.current && !ref.current.contains(target)) {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
;(events || DEFAULT_EVENTS).forEach((fn) =>
|
||||||
|
document.addEventListener(fn, listener)
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
;(events || DEFAULT_EVENTS).forEach((fn) =>
|
||||||
|
document.removeEventListener(fn, listener)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [ref, handler, nodes, events])
|
||||||
|
|
||||||
|
return ref
|
||||||
|
}
|
||||||
90
web-app/src/hooks/useMediaQuery.ts
Normal file
90
web-app/src/hooks/useMediaQuery.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
export interface UseMediaQueryOptions {
|
||||||
|
getInitialValueInEffect: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaQueryCallback = (event: { matches: boolean; media: string }) => void
|
||||||
|
|
||||||
|
// Zustand store for small screen state
|
||||||
|
type SmallScreenState = {
|
||||||
|
isSmallScreen: boolean
|
||||||
|
setIsSmallScreen: (isSmall: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSmallScreenStore = create<SmallScreenState>((set) => ({
|
||||||
|
isSmallScreen: false,
|
||||||
|
setIsSmallScreen: (isSmall) => set({ isSmallScreen: isSmall }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Older versions of Safari (shipped withCatalina and before) do not support addEventListener on matchMedia
|
||||||
|
* https://stackoverflow.com/questions/56466261/matchmedia-addlistener-marked-as-deprecated-addeventlistener-equivalent
|
||||||
|
* */
|
||||||
|
function attachMediaListener(
|
||||||
|
query: MediaQueryList,
|
||||||
|
callback: MediaQueryCallback
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
query.addEventListener('change', callback)
|
||||||
|
return () => query.removeEventListener('change', callback)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
// eslint-disable @typescript-eslint/no-deprecated
|
||||||
|
query.addListener(callback)
|
||||||
|
return () => query.removeListener(callback)
|
||||||
|
// eslint-enable @typescript-eslint/no-deprecated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialValue(query: string, initialValue?: boolean) {
|
||||||
|
if (typeof initialValue === 'boolean') {
|
||||||
|
return initialValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && 'matchMedia' in window) {
|
||||||
|
return window.matchMedia(query).matches
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMediaQuery(
|
||||||
|
query: string,
|
||||||
|
initialValue?: boolean,
|
||||||
|
{ getInitialValueInEffect }: UseMediaQueryOptions = {
|
||||||
|
getInitialValueInEffect: true,
|
||||||
|
}
|
||||||
|
): boolean {
|
||||||
|
const [matches, setMatches] = useState(
|
||||||
|
getInitialValueInEffect ? initialValue : getInitialValue(query)
|
||||||
|
)
|
||||||
|
const queryRef = useRef<MediaQueryList>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ('matchMedia' in window) {
|
||||||
|
queryRef.current = window.matchMedia(query)
|
||||||
|
setMatches(queryRef.current.matches)
|
||||||
|
return attachMediaListener(queryRef.current, (event) =>
|
||||||
|
setMatches(event.matches)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
return matches || false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific hook for small screen detection with state management
|
||||||
|
export const useSmallScreen = (): boolean => {
|
||||||
|
const { isSmallScreen, setIsSmallScreen } = useSmallScreenStore()
|
||||||
|
const mediaQuery = useMediaQuery('(max-width: 768px)')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsSmallScreen(mediaQuery)
|
||||||
|
}, [mediaQuery, setIsSmallScreen])
|
||||||
|
|
||||||
|
return isSmallScreen
|
||||||
|
}
|
||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "Use this model",
|
"useModel": "Use this model",
|
||||||
"downloadModel": "Download model",
|
"downloadModel": "Download model",
|
||||||
"searchPlaceholder": "Search for models on Hugging Face...",
|
"searchPlaceholder": "Search for models on Hugging Face...",
|
||||||
"editTheme": "Edit Theme",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "Recommended Model",
|
"recommendedModelTitle": "Recommended Model",
|
||||||
"recommendedModelContent": "Browse and download powerful AI models from various providers, all in one place. We suggest starting with Jan-Nano - a model optimized for function calling, tool integration, and research capabilities. It's ideal for building interactive AI agents.",
|
"recommendedModelContent": "Browse and download powerful AI models from various providers, all in one place. We suggest starting with Jan-Nano - a model optimized for function calling, tool integration, and research capabilities. It's ideal for building interactive AI agents.",
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "Gunakan model ini",
|
"useModel": "Gunakan model ini",
|
||||||
"downloadModel": "Unduh model",
|
"downloadModel": "Unduh model",
|
||||||
"searchPlaceholder": "Cari model di Hugging Face...",
|
"searchPlaceholder": "Cari model di Hugging Face...",
|
||||||
"editTheme": "Edit Tema",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "Model yang Direkomendasikan",
|
"recommendedModelTitle": "Model yang Direkomendasikan",
|
||||||
"recommendedModelContent": "Jelajahi dan unduh model AI yang kuat dari berbagai penyedia, semuanya di satu tempat. Kami sarankan memulai dengan Jan-Nano - model yang dioptimalkan untuk pemanggilan fungsi, integrasi alat, dan kemampuan penelitian. Ini ideal untuk membangun agen AI interaktif.",
|
"recommendedModelContent": "Jelajahi dan unduh model AI yang kuat dari berbagai penyedia, semuanya di satu tempat. Kami sarankan memulai dengan Jan-Nano - model yang dioptimalkan untuk pemanggilan fungsi, integrasi alat, dan kemampuan penelitian. Ini ideal untuk membangun agen AI interaktif.",
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "Sử dụng mô hình này",
|
"useModel": "Sử dụng mô hình này",
|
||||||
"downloadModel": "Tải xuống mô hình",
|
"downloadModel": "Tải xuống mô hình",
|
||||||
"searchPlaceholder": "Tìm kiếm các mô hình trên Hugging Face...",
|
"searchPlaceholder": "Tìm kiếm các mô hình trên Hugging Face...",
|
||||||
"editTheme": "Chỉnh sửa chủ đề",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "Mô hình được đề xuất",
|
"recommendedModelTitle": "Mô hình được đề xuất",
|
||||||
"recommendedModelContent": "Duyệt và tải xuống các mô hình AI mạnh mẽ từ nhiều nhà cung cấp khác nhau, tất cả ở cùng một nơi. Chúng tôi khuyên bạn nên bắt đầu với Jan-Nano - một mô hình được tối ưu hóa cho các khả năng gọi hàm, tích hợp công cụ và nghiên cứu. Nó lý tưởng để xây dựng các tác nhân AI tương tác.",
|
"recommendedModelContent": "Duyệt và tải xuống các mô hình AI mạnh mẽ từ nhiều nhà cung cấp khác nhau, tất cả ở cùng một nơi. Chúng tôi khuyên bạn nên bắt đầu với Jan-Nano - một mô hình được tối ưu hóa cho các khả năng gọi hàm, tích hợp công cụ và nghiên cứu. Nó lý tưởng để xây dựng các tác nhân AI tương tác.",
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "使用此模型",
|
"useModel": "使用此模型",
|
||||||
"downloadModel": "下载模型",
|
"downloadModel": "下载模型",
|
||||||
"searchPlaceholder": "在 Hugging Face 上搜索模型...",
|
"searchPlaceholder": "在 Hugging Face 上搜索模型...",
|
||||||
"editTheme": "编辑主题",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "推荐模型",
|
"recommendedModelTitle": "推荐模型",
|
||||||
"recommendedModelContent": "在一个地方浏览和下载来自不同提供商的强大 AI 模型。我们建议从 Jan-Nano 开始 - 这是一个针对函数调用、工具集成和研究功能进行优化的模型。它非常适合构建交互式 AI 代理。",
|
"recommendedModelContent": "在一个地方浏览和下载来自不同提供商的强大 AI 模型。我们建议从 Jan-Nano 开始 - 这是一个针对函数调用、工具集成和研究功能进行优化的模型。它非常适合构建交互式 AI 代理。",
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"useModel": "使用此模型",
|
"useModel": "使用此模型",
|
||||||
"downloadModel": "下載模型",
|
"downloadModel": "下載模型",
|
||||||
"searchPlaceholder": "在 Hugging Face 上搜尋模型...",
|
"searchPlaceholder": "在 Hugging Face 上搜尋模型...",
|
||||||
"editTheme": "編輯主題",
|
|
||||||
"joyride": {
|
"joyride": {
|
||||||
"recommendedModelTitle": "推薦模型",
|
"recommendedModelTitle": "推薦模型",
|
||||||
"recommendedModelContent": "在一個地方瀏覽和下載來自不同提供商的強大 AI 模型。我們建議從 Jan-Nano 開始 - 這是一個針對函數調用、工具整合和研究功能進行優化的模型。它非常適合構建互動式 AI 代理。",
|
"recommendedModelContent": "在一個地方瀏覽和下載來自不同提供商的強大 AI 模型。我們建議從 Jan-Nano 開始 - 這是一個針對函數調用、工具整合和研究功能進行優化的模型。它非常適合構建互動式 AI 代理。",
|
||||||
|
|||||||
@ -43,8 +43,8 @@ const AppLayout = () => {
|
|||||||
{/* Main content panel */}
|
{/* Main content panel */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-full flex w-full p-1',
|
'h-full flex w-full p-1 ',
|
||||||
isLeftPanelOpen && 'w-[calc(100%-198px)]'
|
isLeftPanelOpen && 'w-full md:w-[calc(100%-198px)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full rounded-lg overflow-hidden">
|
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full rounded-lg overflow-hidden">
|
||||||
|
|||||||
@ -62,57 +62,60 @@ function Assistant() {
|
|||||||
<span>{t('assistants:title')}</span>
|
<span>{t('assistants:title')}</span>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="h-full p-4 overflow-y-auto">
|
<div className="h-full p-4 overflow-y-auto">
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
{assistants.map((assistant) => (
|
{assistants
|
||||||
<div
|
.slice().sort((a, b) => a.created_at - b.created_at)
|
||||||
className="bg-main-view-fg/3 p-3 rounded-md"
|
.map((assistant) => (
|
||||||
key={assistant.id}
|
<div
|
||||||
>
|
className="bg-main-view-fg/3 p-3 rounded-md"
|
||||||
<div className="flex items-center justify-between gap-2">
|
key={assistant.id}
|
||||||
<h3 className="text-base font-medium text-main-view-fg/80">
|
>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center justify-between gap-2">
|
||||||
{assistant?.avatar && (
|
<h3 className="text-base font-medium text-main-view-fg/80">
|
||||||
<span className="shrink-0 w-4 h-4 relative flex items-center justify-center">
|
<div className="flex items-center gap-1">
|
||||||
<AvatarEmoji
|
{assistant?.avatar && (
|
||||||
avatar={assistant?.avatar}
|
<span className="shrink-0 w-4 h-4 relative flex items-center justify-center">
|
||||||
imageClassName="object-cover"
|
<AvatarEmoji
|
||||||
textClassName="text-sm"
|
avatar={assistant?.avatar}
|
||||||
/>
|
imageClassName="object-cover"
|
||||||
</span>
|
textClassName="text-sm"
|
||||||
)}
|
/>
|
||||||
<span className="line-clamp-1">{assistant.name}</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
</h3>
|
<span className="line-clamp-1">{assistant.name}</span>
|
||||||
<div className="flex items-center gap-0.5">
|
</div>
|
||||||
<div
|
</h3>
|
||||||
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
<div className="flex items-center gap-0.5">
|
||||||
title={t('assistants:editAssistant')}
|
<div
|
||||||
onClick={() => {
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
setEditingKey(assistant.id)
|
title={t('assistants:editAssistant')}
|
||||||
setOpen(true)
|
onClick={() => {
|
||||||
}}
|
setEditingKey(assistant.id)
|
||||||
>
|
setOpen(true)
|
||||||
<IconPencil size={18} className="text-main-view-fg/50" />
|
}}
|
||||||
</div>
|
>
|
||||||
<div
|
<IconPencil size={18} className="text-main-view-fg/50" />
|
||||||
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
</div>
|
||||||
title={t('assistants:deleteAssistant')}
|
<div
|
||||||
onClick={() => handleDelete(assistant.id)}
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
>
|
title={t('assistants:deleteAssistant')}
|
||||||
<IconTrash size={18} className="text-main-view-fg/50" />
|
onClick={() => handleDelete(assistant.id)}
|
||||||
|
>
|
||||||
|
<IconTrash size={18} className="text-main-view-fg/50" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
className="text-main-view-fg/50 mt-1 line-clamp-2"
|
||||||
|
title={assistant.description}
|
||||||
|
>
|
||||||
|
{assistant.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p
|
))}
|
||||||
className="text-main-view-fg/50 mt-1 line-clamp-2"
|
|
||||||
title={assistant.description}
|
|
||||||
>
|
|
||||||
{assistant.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div
|
<div
|
||||||
className="bg-main-view p-3 rounded-md border border-dashed border-main-view-fg/10 flex items-center justify-center cursor-pointer hover:bg-main-view-fg/1 transition-all duration-200 ease-in-out"
|
className="bg-main-view p-3 min-h-[88px] rounded-md border border-dashed border-main-view-fg/10 flex items-center justify-center cursor-pointer hover:bg-main-view-fg/1 transition-all duration-200 ease-in-out"
|
||||||
key="new-assistant"
|
key="new-assistant"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingKey(null)
|
setEditingKey(null)
|
||||||
|
|||||||
@ -358,6 +358,46 @@ function Hub() {
|
|||||||
// Check if we're on the last step
|
// Check if we're on the last step
|
||||||
const isLastStep = currentStepIndex === steps.length - 1
|
const isLastStep = currentStepIndex === steps.length - 1
|
||||||
|
|
||||||
|
const renderFilter = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<span className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium">
|
||||||
|
{
|
||||||
|
sortOptions.find((option) => option.value === sortSelected)
|
||||||
|
?.name
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="bottom" align="end">
|
||||||
|
{sortOptions.map((option) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer my-0.5',
|
||||||
|
sortSelected === option.value && 'bg-main-view-fg/5'
|
||||||
|
)}
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => setSortSelected(option.value)}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={showOnlyDownloaded}
|
||||||
|
onCheckedChange={setShowOnlyDownloaded}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-main-view-fg/70 font-medium whitespace-nowrap">
|
||||||
|
{t('hub:downloaded')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Joyride
|
<Joyride
|
||||||
@ -391,9 +431,12 @@ function Hub() {
|
|||||||
<div className="pr-4 py-3 h-10 w-full flex items-center justify-between relative z-20">
|
<div className="pr-4 py-3 h-10 w-full flex items-center justify-between relative z-20">
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<Loader className="size-4 animate-spin text-main-view-fg/60" />
|
<Loader className="shrink-0 size-4 animate-spin text-main-view-fg/60" />
|
||||||
) : (
|
) : (
|
||||||
<IconSearch className="text-main-view-fg/60" size={14} />
|
<IconSearch
|
||||||
|
className="shrink-0 text-main-view-fg/60"
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
placeholder={t('hub:searchPlaceholder')}
|
placeholder={t('hub:searchPlaceholder')}
|
||||||
@ -402,49 +445,13 @@ function Hub() {
|
|||||||
className="w-full focus:outline-none"
|
className="w-full focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="sm:flex items-center gap-2 shrink-0 hidden">
|
||||||
<DropdownMenu>
|
{renderFilter()}
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<span
|
|
||||||
title={t('hub:editTheme')}
|
|
||||||
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
sortOptions.find(
|
|
||||||
(option) => option.value === sortSelected
|
|
||||||
)?.name
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent side="bottom" align="end">
|
|
||||||
{sortOptions.map((option) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className={cn(
|
|
||||||
'cursor-pointer my-0.5',
|
|
||||||
sortSelected === option.value && 'bg-main-view-fg/5'
|
|
||||||
)}
|
|
||||||
key={option.value}
|
|
||||||
onClick={() => setSortSelected(option.value)}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={showOnlyDownloaded}
|
|
||||||
onCheckedChange={setShowOnlyDownloaded}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-main-view-fg/70 font-medium whitespace-nowrap">
|
|
||||||
{t('hub:downloaded')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="p-4 w-full h-[calc(100%-32px)] !overflow-y-auto first-step-setup-local-provider">
|
<div className="p-4 w-full h-[calc(100%-32px)] !overflow-y-auto first-step-setup-local-provider">
|
||||||
<div className="flex flex-col h-full justify-between gap-4 gap-y-3 w-4/5 mx-auto">
|
<div className="flex flex-col h-full justify-between gap-4 gap-y-3 w-full md:w-4/5 mx-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<div className="text-center text-muted-foreground">
|
<div className="text-center text-muted-foreground">
|
||||||
@ -459,6 +466,9 @@ function Hub() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pb-2 mb-2 gap-2 ">
|
<div className="flex flex-col pb-2 mb-2 gap-2 ">
|
||||||
|
<div className="flex items-center gap-2 justify-end sm:hidden">
|
||||||
|
{renderFilter()}
|
||||||
|
</div>
|
||||||
{filteredModels.map((model) => (
|
{filteredModels.map((model) => (
|
||||||
<div key={model.id}>
|
<div key={model.id}>
|
||||||
<Card
|
<Card
|
||||||
@ -472,11 +482,14 @@ function Hub() {
|
|||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-main-view-fg font-medium text-base capitalize truncate',
|
'text-main-view-fg font-medium text-base capitalize truncate max-w-38 sm:max-w-none',
|
||||||
isRecommendedModel(model.metadata?.id)
|
isRecommendedModel(model.metadata?.id)
|
||||||
? 'hub-model-card-step'
|
? 'hub-model-card-step'
|
||||||
: ''
|
: ''
|
||||||
)}
|
)}
|
||||||
|
title={
|
||||||
|
extractModelName(model.metadata?.id) || ''
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{extractModelName(model.metadata?.id) || ''}
|
{extractModelName(model.metadata?.id) || ''}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@ -53,8 +53,8 @@ function Index() {
|
|||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
<DropdownAssistant />
|
<DropdownAssistant />
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center">
|
<div className="h-full px-4 md:px-8 overflow-y-auto flex flex-col gap-2 justify-center">
|
||||||
<div className="w-4/6 mx-auto">
|
<div className="w-full md:w-4/6 mx-auto">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="font-editorialnew text-main-view-fg text-4xl">
|
<h1 className="font-editorialnew text-main-view-fg text-4xl">
|
||||||
{t('chat:welcome')}
|
{t('chat:welcome')}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { useAssistant } from '@/hooks/useAssistant'
|
|||||||
import { useAppearance } from '@/hooks/useAppearance'
|
import { useAppearance } from '@/hooks/useAppearance'
|
||||||
import { useOutOfContextPromiseModal } from '@/containers/dialogs/OutOfContextDialog'
|
import { useOutOfContextPromiseModal } from '@/containers/dialogs/OutOfContextDialog'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
|
|
||||||
// as route.threadsDetail
|
// as route.threadsDetail
|
||||||
export const Route = createFileRoute('/threads/$threadId')({
|
export const Route = createFileRoute('/threads/$threadId')({
|
||||||
@ -38,6 +39,7 @@ function ThreadDetail() {
|
|||||||
const { setMessages } = useMessages()
|
const { setMessages } = useMessages()
|
||||||
const { streamingContent } = useAppState()
|
const { streamingContent } = useAppState()
|
||||||
const { appMainViewBgColor, chatWidth } = useAppearance()
|
const { appMainViewBgColor, chatWidth } = useAppearance()
|
||||||
|
const isSmallScreen = useSmallScreen()
|
||||||
|
|
||||||
const { messages } = useMessages(
|
const { messages } = useMessages(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
@ -218,7 +220,8 @@ function ThreadDetail() {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-4/6 mx-auto flex max-w-full flex-col grow',
|
'w-4/6 mx-auto flex max-w-full flex-col grow',
|
||||||
chatWidth === 'compact' ? 'w-4/6' : 'w-full'
|
chatWidth === 'compact' ? 'w-full md:w-4/6' : 'w-full',
|
||||||
|
isSmallScreen && 'w-full'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{messages &&
|
{messages &&
|
||||||
@ -256,8 +259,9 @@ function ThreadDetail() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
' mx-auto pt-2 pb-3 shrink-0 relative',
|
'mx-auto pt-2 pb-3 shrink-0 relative px-2',
|
||||||
chatWidth === 'compact' ? 'w-4/6' : 'w-full px-3'
|
chatWidth === 'compact' ? 'w-full md:w-4/6' : 'w-full',
|
||||||
|
isSmallScreen && 'w-full'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user