agentscope / examples /web_ui /frontend /src /components /popover /KnowledgeBaseParametersPopover.tsx
ghostdrive1's picture
Upload folder using huggingface_hub
0b9dc2e verified
Raw
History Blame Contribute Delete
9.2 kB
import { ChevronDown, SlidersHorizontal } from 'lucide-react';
import { useEffect, useState } from 'react';
import type { JSONSchema, JSONSchemaProperty, SessionKnowledgeConfig } from '@/api';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from '@/components/ui/popover';
import { Switch } from '@/components/ui/switch';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useTranslation } from '@/i18n/useI18n';
interface Props {
/**
* The current session-knowledge config. The popover edits its
* `parameters` dict; `knowledge_base_ids` is preserved verbatim.
* `null` means no KBs are attached — the trigger is then disabled
* because there is nothing for the parameters to act on.
*/
value: SessionKnowledgeConfig | null;
/** JSON Schema describing the middleware's tunable parameters. */
schema: JSONSchema | null;
/** Persist a new config back to the session. */
onChange: (next: SessionKnowledgeConfig | null) => void;
/** Force-disable the trigger regardless of `value` (e.g. no session). */
disabled?: boolean;
}
type ParamValue = string | number | boolean | null | undefined;
/** Look through `anyOf` to find the property's effective scalar type and
* any enum constraint. Pydantic renders `Literal[...]` directly and
* `Literal[...] | None` inside the non-null `anyOf` branch. */
function resolve(prop: JSONSchemaProperty): {
type: string;
enumValues: unknown[] | null;
} {
if (prop.type && prop.type !== 'null') {
return { type: prop.type, enumValues: prop.enum ?? null };
}
for (const variant of prop.anyOf ?? []) {
const v = variant as JSONSchemaProperty;
if (v.type && v.type !== 'null') {
return { type: v.type, enumValues: v.enum ?? prop.enum ?? null };
}
}
return { type: 'string', enumValues: prop.enum ?? null };
}
/**
* Trigger + popover that edits the `KnowledgeBaseMiddleware` parameters
* for the active session.
*
* Schema-driven (entries come from
* `KnowledgeBaseMiddleware.Parameters.model_json_schema()`) but with a
* hand-rolled label/control grid so labels stay flush-left and inputs
* align in a single right-hand column — same pattern as
* {@link ModelParametersPopover}. Enums render as
* {@link LlmSelect}-style `DropdownMenu` triggers instead of the native
* `<Select>` used by the shared {@link SchemaForm}.
*
* The trigger is disabled until at least one knowledge base is selected
* — until then the parameters have nothing to apply to.
*/
export function KnowledgeBaseParametersPopover({
value,
schema,
onChange,
disabled = false,
}: Props) {
const { t } = useTranslation();
// Local draft so number inputs can transiently hold empty strings
// without racing the persisted `value.parameters`. Kept in sync with
// the source of truth whenever it changes (session switch, etc.).
const [paramValues, setParamValues] = useState<Record<string, ParamValue>>(
(value?.parameters ?? {}) as Record<string, ParamValue>,
);
useEffect(() => {
setParamValues((value?.parameters ?? {}) as Record<string, ParamValue>);
}, [value]);
const handleParamChange = (key: string, val: ParamValue) => {
const next = { ...paramValues, [key]: val };
if (val === undefined) delete next[key];
setParamValues(next);
// Without any selected KB the middleware isn't installed, so a
// parameter edit has nowhere to land. The trigger is disabled in
// that state, but guard here as a belt-and-braces check.
if (!value || value.knowledge_base_ids.length === 0) return;
onChange({
knowledge_base_ids: value.knowledge_base_ids,
parameters: next as Record<string, unknown>,
});
};
const hasSelection = !!value && value.knowledge_base_ids.length > 0;
const triggerDisabled = disabled || !schema || !hasSelection;
const entries = Object.entries(schema?.properties ?? {});
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
disabled={triggerDisabled}
aria-label={t('panel.knowledge.parametersTitle')}
title={t('panel.knowledge.parametersTitle')}
>
<SlidersHorizontal />
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className="w-80 max-h-[28rem] overflow-y-auto"
// Stop the popover from forwarding pointer/key events back
// up to the parent panel header, which would otherwise
// dismiss focus mid-edit.
onPointerDown={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<PopoverHeader>
<PopoverTitle>{t('panel.knowledge.parametersTitle')}</PopoverTitle>
<PopoverDescription>
{t('panel.knowledge.parametersDescription')}
</PopoverDescription>
</PopoverHeader>
{schema && entries.length > 0 ? (
<div className="grid grid-cols-[auto_1fr] items-center gap-x-3 gap-y-3">
{entries.map(([key, prop]) => {
const { type, enumValues } = resolve(prop);
const id = `kb-mw-${key}`;
const label = prop.title ?? key.replace(/_/g, ' ');
const current = paramValues[key];
let control: React.ReactNode;
if (type === 'boolean') {
control = (
<Switch
id={id}
checked={current !== undefined ? !!current : !!prop.default}
onCheckedChange={(checked) =>
handleParamChange(key, !!checked)
}
/>
);
} else if (enumValues) {
const displayValue =
current !== undefined && current !== null
? String(current)
: prop.default != null
? String(prop.default)
: '';
control = (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
id={id}
variant="outline"
size="sm"
className="justify-between gap-1 w-full"
>
<span className="truncate">{displayValue}</span>
<ChevronDown className="size-3.5 opacity-50 shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="min-w-[var(--radix-dropdown-menu-trigger-width)] max-h-60 overflow-y-auto"
>
<DropdownMenuRadioGroup
value={displayValue}
onValueChange={(v) => handleParamChange(key, v)}
>
{enumValues.map((opt) => (
<DropdownMenuRadioItem
key={String(opt)}
value={String(opt)}
>
{String(opt)}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
} else if (type === 'number' || type === 'integer') {
control = (
<Input
id={id}
type="number"
value={
current === undefined || current === null
? ''
: String(current)
}
min={prop.minimum}
max={prop.maximum}
step={type === 'integer' ? 1 : 'any'}
placeholder={
prop.default != null ? String(prop.default) : undefined
}
onChange={(e) => {
const raw = e.target.value;
if (raw === '') {
handleParamChange(key, undefined);
return;
}
const parsed =
type === 'integer'
? parseInt(raw, 10)
: parseFloat(raw);
handleParamChange(
key,
Number.isNaN(parsed) ? raw : parsed,
);
}}
/>
);
} else {
control = (
<Input
id={id}
type="text"
value={
current === undefined || current === null
? ''
: String(current)
}
placeholder={
prop.default != null ? String(prop.default) : undefined
}
onChange={(e) => handleParamChange(key, e.target.value)}
/>
);
}
return (
<Tooltip key={key}>
<TooltipTrigger asChild>
<div className="col-span-2 grid grid-cols-subgrid items-center">
<Label htmlFor={id} className="whitespace-nowrap">
{label}
</Label>
{control}
</div>
</TooltipTrigger>
{prop.description && (
<TooltipContent side="left">
{prop.description}
</TooltipContent>
)}
</Tooltip>
);
})}
</div>
) : null}
</PopoverContent>
</Popover>
);
}