Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files. The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
588 lines
18 KiB
TypeScript
Vendored
588 lines
18 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import React, { useState, useMemo, useCallback } from 'react'
|
|
import { ChevronsUpDown, Check, CpuIcon, LayersIcon } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { cn } from '@/lib/utils'
|
|
import { useIsMobile } from '@/hooks/use-mobile'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from '@/components/ui/command'
|
|
import {
|
|
Drawer,
|
|
DrawerContent,
|
|
DrawerHeader,
|
|
DrawerTitle,
|
|
DrawerTrigger,
|
|
} from '@/components/ui/drawer'
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@/components/ui/popover'
|
|
|
|
interface ModelOption {
|
|
label: string
|
|
value: string
|
|
category?: string
|
|
description?: string
|
|
}
|
|
|
|
interface GroupOption {
|
|
label: string
|
|
value: string
|
|
ratio?: number
|
|
desc?: string
|
|
description?: string
|
|
}
|
|
|
|
interface ModelSelectorProps {
|
|
selectedModel: string
|
|
models: ModelOption[]
|
|
onModelChange: (value: string) => void
|
|
className?: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
interface GroupSelectorProps {
|
|
selectedGroup: string
|
|
groups: GroupOption[]
|
|
onGroupChange: (value: string) => void
|
|
className?: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
const ModelTriggerButton = React.forwardRef<
|
|
React.ComponentRef<typeof Button>,
|
|
React.ComponentPropsWithoutRef<typeof Button> & {
|
|
currentLabel: string
|
|
triggerClassName?: string
|
|
isDisabled?: boolean
|
|
}
|
|
>(({ currentLabel, triggerClassName, isDisabled, ...props }, ref) => (
|
|
<Button
|
|
ref={ref}
|
|
variant='outline'
|
|
role='combobox'
|
|
size='sm'
|
|
disabled={isDisabled}
|
|
className={cn(
|
|
'flex h-8 items-center gap-2 border px-3 font-medium',
|
|
'justify-center p-0 sm:w-auto sm:justify-start sm:px-3',
|
|
'w-8',
|
|
'bg-background text-foreground',
|
|
'hover:bg-accent transition-colors',
|
|
'focus:!ring-0 focus:!outline-none',
|
|
'shadow-none',
|
|
triggerClassName
|
|
)}
|
|
{...props}
|
|
>
|
|
<CpuIcon className='text-muted-foreground block size-4 sm:hidden' />
|
|
<span className='text-muted-foreground sm:text-foreground hidden truncate text-xs sm:block'>
|
|
{currentLabel}
|
|
</span>
|
|
<ChevronsUpDown className='text-muted-foreground hidden h-4 w-4 opacity-50 sm:block' />
|
|
</Button>
|
|
))
|
|
|
|
ModelTriggerButton.displayName = 'ModelTriggerButton'
|
|
|
|
const GroupTriggerButton = React.forwardRef<
|
|
React.ComponentRef<typeof Button>,
|
|
React.ComponentPropsWithoutRef<typeof Button> & {
|
|
currentLabel: string
|
|
triggerClassName?: string
|
|
isDisabled?: boolean
|
|
}
|
|
>(({ currentLabel, triggerClassName, isDisabled, ...props }, ref) => (
|
|
<Button
|
|
ref={ref}
|
|
variant='outline'
|
|
role='combobox'
|
|
size='sm'
|
|
disabled={isDisabled}
|
|
className={cn(
|
|
'flex h-8 items-center gap-2 border px-3 font-medium',
|
|
'justify-center p-0 sm:w-auto sm:justify-start sm:px-3',
|
|
'w-8',
|
|
'bg-background text-foreground',
|
|
'hover:bg-accent transition-colors',
|
|
'focus:!ring-0 focus:!outline-none',
|
|
'shadow-none',
|
|
triggerClassName
|
|
)}
|
|
{...props}
|
|
>
|
|
<LayersIcon className='text-muted-foreground block size-4 sm:hidden' />
|
|
<span className='text-muted-foreground sm:text-foreground hidden truncate text-xs sm:block'>
|
|
{currentLabel}
|
|
</span>
|
|
<ChevronsUpDown className='text-muted-foreground hidden h-4 w-4 opacity-50 sm:block' />
|
|
</Button>
|
|
))
|
|
|
|
GroupTriggerButton.displayName = 'GroupTriggerButton'
|
|
|
|
/**
|
|
* Model Selector Component
|
|
* Styled following Scira's form-component design patterns
|
|
*/
|
|
export const ModelSelector: React.FC<ModelSelectorProps> = React.memo(
|
|
({ selectedModel, models, onModelChange, className, disabled = false }) => {
|
|
const { t } = useTranslation()
|
|
const [open, setOpen] = useState(false)
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const isMobile = useIsMobile()
|
|
|
|
const currentModel = useMemo(
|
|
() => models.find((m) => m.value === selectedModel),
|
|
[models, selectedModel]
|
|
)
|
|
|
|
// Group models by category
|
|
const groupedModels = useMemo(
|
|
() =>
|
|
models.reduce(
|
|
(acc, model) => {
|
|
const category = model.category || t('Other')
|
|
if (!acc[category]) {
|
|
acc[category] = []
|
|
}
|
|
acc[category].push(model)
|
|
return acc
|
|
},
|
|
{} as Record<string, ModelOption[]>
|
|
),
|
|
[models, t]
|
|
)
|
|
|
|
// Filter models by search query
|
|
const filteredModels = useMemo(() => {
|
|
if (!searchQuery.trim()) return groupedModels
|
|
|
|
const query = searchQuery.toLowerCase()
|
|
const filtered: Record<string, ModelOption[]> = {}
|
|
|
|
Object.entries(groupedModels).forEach(([category, categoryModels]) => {
|
|
const matches = categoryModels.filter(
|
|
(m) =>
|
|
m.label.toLowerCase().includes(query) ||
|
|
m.value.toLowerCase().includes(query) ||
|
|
m.description?.toLowerCase().includes(query)
|
|
)
|
|
if (matches.length > 0) {
|
|
filtered[category] = matches
|
|
}
|
|
})
|
|
|
|
return filtered
|
|
}, [groupedModels, searchQuery])
|
|
|
|
const handleModelChange = useCallback(
|
|
(value: string) => {
|
|
onModelChange(value)
|
|
setOpen(false)
|
|
setSearchQuery('')
|
|
},
|
|
[onModelChange]
|
|
)
|
|
|
|
// Shared command content
|
|
const renderModelCommandContent = () => (
|
|
<Command
|
|
className={cn(
|
|
isMobile
|
|
? 'h-full flex-1 rounded-lg border-0 bg-transparent'
|
|
: 'rounded-lg'
|
|
)}
|
|
filter={() => 1}
|
|
shouldFilter={false}
|
|
>
|
|
{!isMobile && (
|
|
<CommandInput
|
|
placeholder={t('Search models...')}
|
|
className='h-9'
|
|
value={searchQuery}
|
|
onValueChange={setSearchQuery}
|
|
/>
|
|
)}
|
|
<CommandEmpty>{t('No model found.')}</CommandEmpty>
|
|
<CommandList
|
|
className={isMobile ? '!max-h-full flex-1 p-2' : 'max-h-[300px]'}
|
|
>
|
|
{Object.keys(filteredModels).length === 0 ? (
|
|
<div className='text-muted-foreground px-3 py-6 text-xs'>
|
|
{t('No model found.')}
|
|
</div>
|
|
) : (
|
|
Object.entries(filteredModels).map(
|
|
([category, categoryModels], categoryIndex) => (
|
|
<CommandGroup key={category}>
|
|
{categoryIndex > 0 && (
|
|
<div className='border-border my-1 border-t' />
|
|
)}
|
|
<div
|
|
className={cn(
|
|
'text-muted-foreground px-2 py-1 font-medium',
|
|
isMobile ? 'text-xs' : 'text-[10px]'
|
|
)}
|
|
>
|
|
{t('{{category}} Models', { category })}
|
|
</div>
|
|
{categoryModels.map((model) => (
|
|
<CommandItem
|
|
key={model.value}
|
|
value={model.value}
|
|
onSelect={handleModelChange}
|
|
className={cn(
|
|
'mb-0.5 flex items-center justify-between rounded-lg px-2 py-1.5 text-xs',
|
|
'transition-all duration-200',
|
|
'hover:bg-accent',
|
|
'data-[selected=true]:bg-accent'
|
|
)}
|
|
>
|
|
<div className='flex min-w-0 flex-1 items-center gap-1'>
|
|
<div
|
|
className={cn(
|
|
'truncate font-medium',
|
|
isMobile ? 'text-sm' : 'text-[11px]'
|
|
)}
|
|
>
|
|
<span className='inline'>{model.label}</span>
|
|
</div>
|
|
<Check
|
|
className={cn(
|
|
'h-4 w-4 flex-shrink-0',
|
|
selectedModel === model.value
|
|
? 'opacity-100'
|
|
: 'opacity-0'
|
|
)}
|
|
/>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
)
|
|
)
|
|
)}
|
|
</CommandList>
|
|
</Command>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{isMobile ? (
|
|
<Drawer open={open} onOpenChange={setOpen}>
|
|
<DrawerTrigger asChild>
|
|
<ModelTriggerButton
|
|
currentLabel={currentModel?.label || t('Model')}
|
|
triggerClassName={className}
|
|
isDisabled={disabled}
|
|
aria-expanded={open}
|
|
/>
|
|
</DrawerTrigger>
|
|
<DrawerContent className='flex max-h-[80vh] min-h-[60vh] flex-col'>
|
|
<DrawerHeader className='flex-shrink-0 pb-4'>
|
|
<DrawerTitle className='flex items-center gap-2 text-left text-lg font-medium'>
|
|
{t('Select Model')}
|
|
</DrawerTitle>
|
|
</DrawerHeader>
|
|
<div className='flex min-h-0 flex-1 flex-col'>
|
|
{renderModelCommandContent()}
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
) : (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger
|
|
render={
|
|
<ModelTriggerButton
|
|
currentLabel={currentModel?.label || t('Model')}
|
|
triggerClassName={className}
|
|
isDisabled={disabled}
|
|
aria-expanded={open}
|
|
/>
|
|
}
|
|
/>
|
|
<PopoverContent
|
|
className='bg-popover z-40 w-[90vw] max-w-[20em] rounded-lg border p-0 !shadow-none sm:w-[20em]'
|
|
align='start'
|
|
side='bottom'
|
|
sideOffset={4}
|
|
collisionPadding={8}
|
|
>
|
|
{renderModelCommandContent()}
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
)
|
|
|
|
ModelSelector.displayName = 'ModelSelector'
|
|
|
|
/**
|
|
* Group Selector Component
|
|
* Styled following Scira's form-component design patterns
|
|
*/
|
|
export const GroupSelector: React.FC<GroupSelectorProps> = React.memo(
|
|
({ selectedGroup, groups, onGroupChange, className, disabled = false }) => {
|
|
const { t } = useTranslation()
|
|
const [open, setOpen] = useState(false)
|
|
const isMobile = useIsMobile()
|
|
|
|
const currentGroup = useMemo(
|
|
() => groups.find((g) => g.value === selectedGroup),
|
|
[groups, selectedGroup]
|
|
)
|
|
|
|
const handleGroupChange = useCallback(
|
|
(value: string) => {
|
|
onGroupChange(value)
|
|
setOpen(false)
|
|
},
|
|
[onGroupChange]
|
|
)
|
|
|
|
// Shared command content
|
|
const renderGroupCommandContent = () => (
|
|
<Command
|
|
className={cn(
|
|
isMobile
|
|
? 'h-full flex-1 rounded-lg border-0 bg-transparent'
|
|
: 'rounded-lg'
|
|
)}
|
|
filter={(value, search) => {
|
|
const group = groups.find((g) => g.value === value)
|
|
if (!group || !search) return 1
|
|
|
|
const searchTerm = search.toLowerCase()
|
|
const searchableFields = [
|
|
group.label,
|
|
group.description || '',
|
|
group.value,
|
|
]
|
|
.join(' ')
|
|
.toLowerCase()
|
|
|
|
return searchableFields.includes(searchTerm) ? 1 : 0
|
|
}}
|
|
>
|
|
<CommandInput placeholder={t('Search groups...')} className='h-9' />
|
|
<CommandEmpty>{t('No group found.')}</CommandEmpty>
|
|
<CommandList
|
|
className={isMobile ? '!max-h-full flex-1 p-2' : 'max-h-[240px]'}
|
|
>
|
|
<CommandGroup>
|
|
<div className='text-muted-foreground px-2 py-1 text-[10px] font-medium'>
|
|
{t('Model Group')}
|
|
</div>
|
|
{groups.map((group) => (
|
|
<CommandItem
|
|
key={group.value}
|
|
value={group.value}
|
|
onSelect={handleGroupChange}
|
|
className={cn(
|
|
'mb-0.5 flex items-center justify-between rounded-lg px-2 py-2 text-xs',
|
|
'transition-all duration-200',
|
|
'hover:bg-accent',
|
|
'data-[selected=true]:bg-accent'
|
|
)}
|
|
>
|
|
<div className='flex min-w-0 flex-1 items-center gap-2 pr-4'>
|
|
<div className='flex min-w-0 flex-1 flex-col'>
|
|
<span className='text-foreground truncate text-[11px] font-medium'>
|
|
{group.label}
|
|
</span>
|
|
{(group.desc || group.description) && (
|
|
<div className='text-muted-foreground truncate text-[9px] leading-tight'>
|
|
{group.desc || group.description}
|
|
{group.ratio && (
|
|
<>
|
|
{' · '}
|
|
{t('Ratio: {{value}}', { value: group.ratio })}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Check
|
|
className={cn(
|
|
'ml-auto h-4 w-4',
|
|
selectedGroup === group.value ? 'opacity-100' : 'opacity-0'
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{isMobile ? (
|
|
<Drawer open={open} onOpenChange={setOpen}>
|
|
<DrawerTrigger asChild>
|
|
<GroupTriggerButton
|
|
currentLabel={currentGroup?.label || t('Group')}
|
|
triggerClassName={className}
|
|
isDisabled={disabled}
|
|
aria-expanded={open}
|
|
/>
|
|
</DrawerTrigger>
|
|
<DrawerContent className='max-h-[80vh]'>
|
|
<DrawerHeader className='pb-4 text-left'>
|
|
<DrawerTitle>{t('Choose Group')}</DrawerTitle>
|
|
</DrawerHeader>
|
|
<div className='max-h-[calc(80vh-100px)] overflow-y-auto px-4 pb-6'>
|
|
<div className='space-y-2'>
|
|
{groups.map((group) => (
|
|
<Button
|
|
key={group.value}
|
|
variant='outline'
|
|
onClick={() => handleGroupChange(group.value)}
|
|
className={cn(
|
|
'flex h-auto w-full items-center justify-between rounded-lg p-4 text-left whitespace-normal',
|
|
'border-border hover:bg-accent',
|
|
selectedGroup === group.value
|
|
? 'bg-accent border-primary/20'
|
|
: 'bg-background'
|
|
)}
|
|
>
|
|
<div className='flex min-w-0 flex-1 items-center gap-3'>
|
|
<div className='flex min-w-0 flex-1 flex-col'>
|
|
<span className='text-foreground text-sm font-medium'>
|
|
{group.label}
|
|
</span>
|
|
{(group.desc || group.description) && (
|
|
<div className='text-muted-foreground mt-0.5 text-xs'>
|
|
{group.desc || group.description}
|
|
{group.ratio && (
|
|
<>
|
|
{' · '}
|
|
{t('Ratio: {{value}}', {
|
|
value: group.ratio,
|
|
})}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Check
|
|
className={cn(
|
|
'ml-3 h-5 w-5 shrink-0',
|
|
selectedGroup === group.value
|
|
? 'opacity-100'
|
|
: 'opacity-0'
|
|
)}
|
|
/>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
) : (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger
|
|
render={
|
|
<GroupTriggerButton
|
|
currentLabel={currentGroup?.label || t('Group')}
|
|
triggerClassName={className}
|
|
isDisabled={disabled}
|
|
aria-expanded={open}
|
|
/>
|
|
}
|
|
/>
|
|
<PopoverContent
|
|
className='bg-popover z-50 w-[90vw] max-w-[14em] rounded-lg border p-0 !shadow-none sm:w-[14em]'
|
|
align='start'
|
|
side='bottom'
|
|
sideOffset={4}
|
|
collisionPadding={8}
|
|
>
|
|
{renderGroupCommandContent()}
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
)
|
|
|
|
GroupSelector.displayName = 'GroupSelector'
|
|
|
|
// Export combined selector component
|
|
export interface ModelGroupSelectorProps {
|
|
// Model props
|
|
selectedModel: string
|
|
models: ModelOption[]
|
|
onModelChange: (value: string) => void
|
|
// Group props
|
|
selectedGroup: string
|
|
groups: GroupOption[]
|
|
onGroupChange: (value: string) => void
|
|
// Common props
|
|
className?: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
/**
|
|
* Combined Model and Group Selector Component
|
|
* Provides both model and group selection in a unified interface
|
|
*/
|
|
export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
|
|
selectedModel,
|
|
models,
|
|
onModelChange,
|
|
selectedGroup,
|
|
groups,
|
|
onGroupChange,
|
|
className,
|
|
disabled = false,
|
|
}) => {
|
|
return (
|
|
<div className={cn('flex items-center gap-2', className)}>
|
|
<GroupSelector
|
|
selectedGroup={selectedGroup}
|
|
groups={groups}
|
|
onGroupChange={onGroupChange}
|
|
disabled={disabled}
|
|
/>
|
|
<ModelSelector
|
|
selectedModel={selectedModel}
|
|
models={models}
|
|
onModelChange={onModelChange}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|