feat(playground): add chat parameter settings panel
- expose playground controls for sampling, penalty, token, and seed parameters. - reuse the existing persisted config and parameter-enabled state so requests only send enabled values. - add localized parameter copy and keep the panel scrollable on short screens.
This commit is contained in:
+24
@@ -43,17 +43,33 @@ import {
|
||||
getAttachmentActionNotice,
|
||||
getSearchActionNotice,
|
||||
} from '../../lib'
|
||||
import type { ParameterEnabled, PlaygroundConfig } from '../../types'
|
||||
import { PlaygroundParameterPanel } from './playground-parameter-panel'
|
||||
|
||||
type PlaygroundInputToolsProps = {
|
||||
config: PlaygroundConfig
|
||||
disabled?: boolean
|
||||
hasMessages?: boolean
|
||||
onClearMessages?: () => void
|
||||
onConfigChange: <K extends keyof PlaygroundConfig>(
|
||||
key: K,
|
||||
value: PlaygroundConfig[K]
|
||||
) => void
|
||||
onParameterEnabledChange: (
|
||||
key: keyof ParameterEnabled,
|
||||
value: boolean
|
||||
) => void
|
||||
parameterEnabled: ParameterEnabled
|
||||
}
|
||||
|
||||
export function PlaygroundInputTools({
|
||||
config,
|
||||
disabled,
|
||||
hasMessages = false,
|
||||
onClearMessages,
|
||||
onConfigChange,
|
||||
onParameterEnabledChange,
|
||||
parameterEnabled,
|
||||
}: PlaygroundInputToolsProps) {
|
||||
const { t } = useTranslation()
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false)
|
||||
@@ -133,6 +149,14 @@ export function PlaygroundInputTools({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<PlaygroundParameterPanel
|
||||
config={config}
|
||||
disabled={disabled}
|
||||
onConfigChange={onConfigChange}
|
||||
onParameterEnabledChange={onParameterEnabledChange}
|
||||
parameterEnabled={parameterEnabled}
|
||||
/>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
|
||||
@@ -27,11 +27,17 @@ import {
|
||||
} from '@/components/ai-elements/prompt-input'
|
||||
|
||||
import { getSubmittableInputText } from '../../lib'
|
||||
import type { ModelOption, GroupOption } from '../../types'
|
||||
import type {
|
||||
ModelOption,
|
||||
GroupOption,
|
||||
ParameterEnabled,
|
||||
PlaygroundConfig,
|
||||
} from '../../types'
|
||||
import { PlaygroundInputControls } from './playground-input-controls'
|
||||
import { PlaygroundInputTools } from './playground-input-tools'
|
||||
|
||||
interface PlaygroundInputProps {
|
||||
config: PlaygroundConfig
|
||||
onSubmit: (text: string) => void
|
||||
onStop?: () => void
|
||||
disabled?: boolean
|
||||
@@ -44,10 +50,20 @@ interface PlaygroundInputProps {
|
||||
groupValue: string
|
||||
onGroupChange: (value: string) => void
|
||||
hasMessages?: boolean
|
||||
onConfigChange: <K extends keyof PlaygroundConfig>(
|
||||
key: K,
|
||||
value: PlaygroundConfig[K]
|
||||
) => void
|
||||
onClearMessages?: () => void
|
||||
onParameterEnabledChange: (
|
||||
key: keyof ParameterEnabled,
|
||||
value: boolean
|
||||
) => void
|
||||
parameterEnabled: ParameterEnabled
|
||||
}
|
||||
|
||||
export function PlaygroundInput({
|
||||
config,
|
||||
onSubmit,
|
||||
onStop,
|
||||
disabled,
|
||||
@@ -60,7 +76,10 @@ export function PlaygroundInput({
|
||||
groupValue,
|
||||
onGroupChange,
|
||||
hasMessages = false,
|
||||
onConfigChange,
|
||||
onClearMessages,
|
||||
onParameterEnabledChange,
|
||||
parameterEnabled,
|
||||
}: PlaygroundInputProps) {
|
||||
const { t } = useTranslation()
|
||||
const [text, setText] = useState('')
|
||||
@@ -107,9 +126,13 @@ export function PlaygroundInput({
|
||||
text={text}
|
||||
tools={
|
||||
<PlaygroundInputTools
|
||||
config={config}
|
||||
disabled={disabled}
|
||||
hasMessages={hasMessages}
|
||||
onConfigChange={onConfigChange}
|
||||
onClearMessages={onClearMessages}
|
||||
onParameterEnabledChange={onParameterEnabledChange}
|
||||
parameterEnabled={parameterEnabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
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 { SlidersHorizontalIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { PromptInputButton } from '@/components/ai-elements/prompt-input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import {
|
||||
getParameterControlValueText,
|
||||
normalizeParameterNumberValue,
|
||||
PLAYGROUND_PARAMETER_CONTROLS,
|
||||
PLAYGROUND_PARAMETER_PANEL_SCROLL_CLASS,
|
||||
type PlaygroundParameterKey,
|
||||
} from '../../lib/parameters/playground-parameters'
|
||||
import type { ParameterEnabled, PlaygroundConfig } from '../../types'
|
||||
|
||||
type PlaygroundParameterPanelProps = {
|
||||
config: PlaygroundConfig
|
||||
disabled?: boolean
|
||||
onConfigChange: <K extends keyof PlaygroundConfig>(
|
||||
key: K,
|
||||
value: PlaygroundConfig[K]
|
||||
) => void
|
||||
onParameterEnabledChange: (
|
||||
key: PlaygroundParameterKey,
|
||||
value: boolean
|
||||
) => void
|
||||
parameterEnabled: ParameterEnabled
|
||||
}
|
||||
|
||||
type PlaygroundParameterContentProps = PlaygroundParameterPanelProps & {
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
function PlaygroundParameterContent({
|
||||
compact = false,
|
||||
config,
|
||||
disabled,
|
||||
onConfigChange,
|
||||
onParameterEnabledChange,
|
||||
parameterEnabled,
|
||||
}: PlaygroundParameterContentProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const updateParameterConfig = (
|
||||
key: PlaygroundParameterKey,
|
||||
value: number | null
|
||||
) => {
|
||||
if (key === 'seed') {
|
||||
onConfigChange('seed', value)
|
||||
return
|
||||
}
|
||||
|
||||
onConfigChange(key, value ?? 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-3',
|
||||
PLAYGROUND_PARAMETER_PANEL_SCROLL_CLASS,
|
||||
compact ? 'px-4 pb-4' : 'p-1'
|
||||
)}
|
||||
>
|
||||
{PLAYGROUND_PARAMETER_CONTROLS.map((control) => {
|
||||
const enabled = parameterEnabled[control.key]
|
||||
const value = config[control.key]
|
||||
const controlId = `playground-${control.key}`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-border/70 bg-background/60 grid gap-2 rounded-lg border p-3 transition-opacity',
|
||||
(!enabled || disabled) && 'opacity-55'
|
||||
)}
|
||||
key={control.key}
|
||||
>
|
||||
<div className='flex items-start justify-between gap-3'>
|
||||
<div className='min-w-0 space-y-1'>
|
||||
<div className='flex min-w-0 items-center gap-2'>
|
||||
<label
|
||||
className='truncate text-sm leading-5 font-medium'
|
||||
htmlFor={controlId}
|
||||
>
|
||||
{t(control.labelKey)}
|
||||
</label>
|
||||
<Badge
|
||||
className='h-5 max-w-24 shrink-0 px-1.5 font-mono text-[11px]'
|
||||
variant='outline'
|
||||
>
|
||||
{t(getParameterControlValueText(control.key, value))}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className='text-muted-foreground text-xs leading-4'>
|
||||
{t(control.descriptionKey)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
aria-label={t('Enable {{parameter}}', {
|
||||
parameter: t(control.labelKey),
|
||||
})}
|
||||
checked={enabled}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onParameterEnabledChange(control.key, checked)
|
||||
}
|
||||
size='sm'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{control.valueType === 'slider' ? (
|
||||
<Slider
|
||||
className='py-1.5'
|
||||
disabled={disabled || !enabled}
|
||||
id={controlId}
|
||||
max={control.max}
|
||||
min={control.min}
|
||||
onValueChange={(nextValue) => {
|
||||
const firstValue = Array.isArray(nextValue)
|
||||
? nextValue[0]
|
||||
: nextValue
|
||||
updateParameterConfig(
|
||||
control.key,
|
||||
normalizeParameterNumberValue(control.key, firstValue)
|
||||
)
|
||||
}}
|
||||
step={control.step}
|
||||
value={[Number(value)]}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
disabled={disabled || !enabled}
|
||||
id={controlId}
|
||||
inputMode='numeric'
|
||||
max={control.max}
|
||||
min={control.min}
|
||||
onChange={(event) => {
|
||||
updateParameterConfig(
|
||||
control.key,
|
||||
normalizeParameterNumberValue(
|
||||
control.key,
|
||||
event.target.value
|
||||
)
|
||||
)
|
||||
}}
|
||||
step={control.step}
|
||||
type='number'
|
||||
value={value ?? ''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PlaygroundParameterPanel(props: PlaygroundParameterPanelProps) {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useIsMobile()
|
||||
const activeCount = PLAYGROUND_PARAMETER_CONTROLS.filter(
|
||||
(control) => props.parameterEnabled[control.key]
|
||||
).length
|
||||
|
||||
const trigger = (
|
||||
<PromptInputButton
|
||||
aria-label={t('Parameters')}
|
||||
className='text-muted-foreground hover:text-foreground hover:bg-muted/70 relative font-medium'
|
||||
disabled={props.disabled}
|
||||
variant='ghost'
|
||||
>
|
||||
<SlidersHorizontalIcon size={16} />
|
||||
<span className='bg-primary text-primary-foreground absolute -top-1 -right-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full px-1 text-[9px] leading-none font-semibold'>
|
||||
{activeCount}
|
||||
</span>
|
||||
</PromptInputButton>
|
||||
)
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<SheetTrigger render={trigger} />} />
|
||||
<TooltipContent>
|
||||
<p>{t('Parameters')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<SheetContent
|
||||
className='max-h-[85vh] overflow-hidden rounded-t-xl'
|
||||
side='bottom'
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('Parameter settings')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<PlaygroundParameterContent {...props} compact />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<PopoverTrigger render={trigger} />} />
|
||||
<TooltipContent>
|
||||
<p>{t('Parameters')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent
|
||||
align='start'
|
||||
className='w-[22rem] max-w-[calc(100vw-2rem)] gap-3 p-3'
|
||||
collisionPadding={8}
|
||||
side='top'
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className='space-y-1 px-1'>
|
||||
<div className='text-sm font-semibold'>{t('Parameter settings')}</div>
|
||||
<div className='text-muted-foreground text-xs leading-4'>
|
||||
{t('Only enabled parameters are sent with the request.')}
|
||||
</div>
|
||||
</div>
|
||||
<PlaygroundParameterContent {...props} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export function Playground() {
|
||||
setModels,
|
||||
setGroups,
|
||||
updateConfig,
|
||||
updateParameterEnabled,
|
||||
clearMessages,
|
||||
} = usePlaygroundState()
|
||||
|
||||
@@ -95,6 +96,7 @@ export function Playground() {
|
||||
{/* Input area: center content and constrain to the same container width */}
|
||||
<div className='mx-auto w-full max-w-4xl'>
|
||||
<PlaygroundInput
|
||||
config={config}
|
||||
disabled={isGenerating}
|
||||
groups={groups}
|
||||
groupValue={config.group}
|
||||
@@ -103,10 +105,13 @@ export function Playground() {
|
||||
modelValue={config.model}
|
||||
models={models}
|
||||
onGroupChange={(value) => updateConfig('group', value)}
|
||||
onConfigChange={updateConfig}
|
||||
onClearMessages={handleClearMessages}
|
||||
onModelChange={(value) => updateConfig('model', value)}
|
||||
onParameterEnabledChange={updateParameterEnabled}
|
||||
onStop={stopGeneration}
|
||||
onSubmit={handleSendMessage}
|
||||
parameterEnabled={parameterEnabled}
|
||||
hasMessages={messages.length > 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,7 @@ export * from './message/message-timing-utils'
|
||||
export * from './message/message-update-utils'
|
||||
export * from './message/message-utils'
|
||||
export * from './options/playground-option-utils'
|
||||
export * from './parameters/playground-parameters'
|
||||
export * from './state/playground-state-utils'
|
||||
export * from './storage/storage'
|
||||
export * from './streaming/payload-builder'
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
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 type { PlaygroundConfig, ParameterEnabled } from '../../types'
|
||||
|
||||
type ParameterValue = PlaygroundConfig[keyof PlaygroundConfig]
|
||||
|
||||
export type PlaygroundParameterKey = keyof ParameterEnabled
|
||||
|
||||
export type PlaygroundParameterControl = {
|
||||
key: PlaygroundParameterKey
|
||||
labelKey: string
|
||||
descriptionKey: string
|
||||
valueType: 'slider' | 'number'
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
}
|
||||
|
||||
export const PLAYGROUND_PARAMETER_CONTROLS = [
|
||||
{
|
||||
key: 'temperature',
|
||||
labelKey: 'Temperature',
|
||||
descriptionKey: 'Controls randomness and creativity',
|
||||
valueType: 'slider',
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
{
|
||||
key: 'top_p',
|
||||
labelKey: 'Top P',
|
||||
descriptionKey: 'Limits token selection to a probability mass',
|
||||
valueType: 'slider',
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
{
|
||||
key: 'frequency_penalty',
|
||||
labelKey: 'Frequency Penalty',
|
||||
descriptionKey: 'Reduces repeated wording',
|
||||
valueType: 'slider',
|
||||
min: -2,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
},
|
||||
{
|
||||
key: 'presence_penalty',
|
||||
labelKey: 'Presence Penalty',
|
||||
descriptionKey: 'Encourages new topics',
|
||||
valueType: 'slider',
|
||||
min: -2,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
},
|
||||
{
|
||||
key: 'max_tokens',
|
||||
labelKey: 'Max Tokens',
|
||||
descriptionKey: 'Caps the response length',
|
||||
valueType: 'number',
|
||||
min: 0,
|
||||
max: 200000,
|
||||
step: 1,
|
||||
},
|
||||
{
|
||||
key: 'seed',
|
||||
labelKey: 'Seed',
|
||||
descriptionKey: 'Keeps compatible responses more repeatable',
|
||||
valueType: 'number',
|
||||
min: 0,
|
||||
max: 2147483647,
|
||||
step: 1,
|
||||
},
|
||||
] as const satisfies readonly PlaygroundParameterControl[]
|
||||
|
||||
export const PLAYGROUND_PARAMETER_PANEL_SCROLL_CLASS =
|
||||
'max-h-[min(28rem,calc(100vh-10rem))] overflow-y-auto pr-1'
|
||||
|
||||
export function normalizeParameterNumberValue(
|
||||
key: PlaygroundParameterKey,
|
||||
value: string | number
|
||||
): number | null {
|
||||
if (value === '') {
|
||||
return key === 'seed' ? null : 0
|
||||
}
|
||||
|
||||
const control = PLAYGROUND_PARAMETER_CONTROLS.find((item) => item.key === key)
|
||||
const parsed = typeof value === 'number' ? value : Number.parseFloat(value)
|
||||
|
||||
if (!control || Number.isNaN(parsed)) {
|
||||
return key === 'seed' ? null : 0
|
||||
}
|
||||
|
||||
const clamped = Math.min(control.max, Math.max(control.min, parsed))
|
||||
|
||||
if (control.step >= 1) {
|
||||
return Math.trunc(clamped)
|
||||
}
|
||||
|
||||
const precision = Math.max(0, String(control.step).split('.')[1]?.length ?? 0)
|
||||
return Number(clamped.toFixed(precision))
|
||||
}
|
||||
|
||||
export function getParameterControlValueText(
|
||||
key: PlaygroundParameterKey,
|
||||
value: ParameterValue
|
||||
): string {
|
||||
if (key === 'seed' && value === null) {
|
||||
return 'Not set'
|
||||
}
|
||||
|
||||
return String(value)
|
||||
}
|
||||
Reference in New Issue
Block a user