This commit is contained in:
Junyan Qin
2026-03-27 17:22:24 +08:00
parent cad259fe39
commit 244e16c491
12 changed files with 790 additions and 502 deletions

View File

@@ -14,6 +14,7 @@ import DynamicFormItemComponent from '@/app/home/components/dynamic-form/Dynamic
import { useEffect, useRef } from 'react';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
export default function DynamicFormComponent({
itemConfigList,
@@ -273,6 +274,46 @@ export default function DynamicFormComponent({
// All fields are disabled when editing (creation_settings are immutable)
const isFieldDisabled = !!isEditing;
// Boolean fields use a special inline layout
if (config.type === 'boolean') {
return (
<FormField
key={config.id}
control={form.control}
name={config.name as keyof FormValues}
render={({ field }) => (
<FormItem>
<div
className={cn(
'flex flex-row items-center justify-between rounded-lg border p-4 max-w-2xl',
isFieldDisabled && 'pointer-events-none opacity-60',
)}
>
<div className="space-y-0.5">
<FormLabel className="text-base">
{extractI18nObject(config.label)}
</FormLabel>
{config.description && (
<p className="text-sm text-muted-foreground">
{extractI18nObject(config.description)}
</p>
)}
</div>
<FormControl>
<DynamicFormItemComponent
config={config}
field={field}
onFileUploaded={onFileUploaded}
/>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
);
}
return (
<FormField
key={config.id}

View File

@@ -37,7 +37,7 @@ import {
DialogFooter,
} from '@/components/ui/dialog';
import { Checkbox } from '@/components/ui/checkbox';
import { Plus, X, Eye, Wrench } from 'lucide-react';
import { Plus, X, Eye, Wrench, Trash2 } from 'lucide-react';
export default function DynamicFormItemComponent({
config,
@@ -181,27 +181,28 @@ export default function DynamicFormItemComponent({
return (
<Input
type="number"
className="max-w-xs"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
);
case DynamicFormItemType.STRING:
return <Input {...field} />;
return <Input className="max-w-md" {...field} />;
case DynamicFormItemType.TEXT:
return <Textarea {...field} className="min-h-[120px]" />;
return <Textarea {...field} className="min-h-[120px] max-w-2xl" />;
case DynamicFormItemType.BOOLEAN:
return <Switch checked={field.value} onCheckedChange={field.onChange} />;
case DynamicFormItemType.STRING_ARRAY:
return (
<div className="space-y-2">
<div className="space-y-2 max-w-md">
{field.value.map((item: string, index: number) => (
<div key={index} className="flex gap-2 items-center">
<div key={index} className="flex gap-1.5 items-center">
<Input
className="w-[200px]"
className="flex-1"
value={item}
onChange={(e) => {
const newValue = [...field.value];
@@ -209,9 +210,11 @@ export default function DynamicFormItemComponent({
field.onChange(newValue);
}}
/>
<button
<Button
type="button"
className="p-2 hover:bg-gray-100 rounded"
variant="ghost"
size="icon"
className="shrink-0 text-muted-foreground hover:text-destructive"
onClick={() => {
const newValue = field.value.filter(
(_: string, i: number) => i !== index,
@@ -219,24 +222,19 @@ export default function DynamicFormItemComponent({
field.onChange(newValue);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-5 h-5 text-red-500"
>
<path d="M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z"></path>
</svg>
</button>
<Trash2 className="size-4" />
</Button>
</div>
))}
<Button
type="button"
variant="outline"
className="w-full border-dashed text-muted-foreground hover:text-foreground"
onClick={() => {
field.onChange([...field.value, '']);
}}
>
<Plus className="size-4 mr-1.5" />
{t('common.add')}
</Button>
</div>
@@ -245,7 +243,7 @@ export default function DynamicFormItemComponent({
case DynamicFormItemType.SELECT:
return (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectTrigger className="max-w-md bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('common.select')} />
</SelectTrigger>
<SelectContent>
@@ -274,31 +272,33 @@ export default function DynamicFormItemComponent({
);
return (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('models.selectModel')} />
</SelectTrigger>
<SelectContent>
{Object.entries(groupedModels).map(([providerName, models]) => (
<SelectGroup key={providerName}>
<SelectLabel>{providerName}</SelectLabel>
{models.map((model) => (
<SelectItem key={model.uuid} value={model.uuid}>
<span className="inline-flex items-center gap-1">
{model.name}
{model.abilities?.includes('vision') && (
<Eye className="h-3 w-3 text-muted-foreground" />
)}
{model.abilities?.includes('func_call') && (
<Wrench className="h-3 w-3 text-muted-foreground" />
)}
</span>
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
<div className="max-w-md">
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('models.selectModel')} />
</SelectTrigger>
<SelectContent>
{Object.entries(groupedModels).map(([providerName, models]) => (
<SelectGroup key={providerName}>
<SelectLabel>{providerName}</SelectLabel>
{models.map((model) => (
<SelectItem key={model.uuid} value={model.uuid}>
<span className="inline-flex items-center gap-1">
{model.name}
{model.abilities?.includes('vision') && (
<Eye className="h-3 w-3 text-muted-foreground" />
)}
{model.abilities?.includes('func_call') && (
<Wrench className="h-3 w-3 text-muted-foreground" />
)}
</span>
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
</div>
);
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR:
@@ -314,25 +314,27 @@ export default function DynamicFormItemComponent({
);
return (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('knowledge.selectEmbeddingModel')} />
</SelectTrigger>
<SelectContent>
{Object.entries(groupedEmbeddingModels).map(
([providerName, models]) => (
<SelectGroup key={providerName}>
<SelectLabel>{providerName}</SelectLabel>
{models.map((model) => (
<SelectItem key={model.uuid} value={model.uuid}>
{model.name}
</SelectItem>
))}
</SelectGroup>
),
)}
</SelectContent>
</Select>
<div className="max-w-md">
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('knowledge.selectEmbeddingModel')} />
</SelectTrigger>
<SelectContent>
{Object.entries(groupedEmbeddingModels).map(
([providerName, models]) => (
<SelectGroup key={providerName}>
<SelectLabel>{providerName}</SelectLabel>
{models.map((model) => (
<SelectItem key={model.uuid} value={model.uuid}>
{model.name}
</SelectItem>
))}
</SelectGroup>
),
)}
</SelectContent>
</Select>
</div>
);
case DynamicFormItemType.MODEL_FALLBACK_SELECTOR: {
@@ -495,11 +497,11 @@ export default function DynamicFormItemComponent({
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={() => removeFallbackModel(index)}
>
<X className="h-4 w-4" />
<Trash2 className="size-4" />
</Button>
</div>
</div>
@@ -512,10 +514,10 @@ export default function DynamicFormItemComponent({
type="button"
variant="outline"
size="sm"
className="w-full"
className="w-full border-dashed text-muted-foreground hover:text-foreground"
onClick={addFallbackModel}
>
<Plus className="h-4 w-4 mr-1" />
<Plus className="size-4 mr-1.5" />
{t('models.fallback.addFallback')}
</Button>
</div>

View File

@@ -77,6 +77,11 @@ import {
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { ChevronRight, Plus } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { useSidebarData, SidebarEntityItem } from './SidebarDataContext';
@@ -346,7 +351,7 @@ function NavItems({
{canCreate && (
<button
type="button"
className="p-1 rounded-sm hover:bg-sidebar-accent opacity-0 group-hover/category-header:opacity-100 transition-opacity"
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground opacity-0 group-hover/category-header:opacity-100 transition-all"
onClick={(e) => {
e.stopPropagation();
router.push(`${routePrefix}?id=new`);
@@ -395,49 +400,66 @@ function NavItems({
isPlugin ? 'group/plugin-item relative' : ''
}
>
<SidebarMenuSubButton
asChild
isActive={isItemActive}
>
<a
href={itemRoute}
className={cn(
isPlugin && !item.debug ? 'pr-6' : '',
)}
onClick={(e) => {
e.preventDefault();
router.push(itemRoute);
}}
>
{item.emoji ? (
<span className="text-sm shrink-0">
{item.emoji}
</span>
) : item.iconURL ? (
<span className="relative shrink-0">
<img
src={item.iconURL}
alt=""
className="size-4 rounded"
/>
{isBot && (
<span
className={cn(
'absolute -bottom-0.5 -right-0.5 size-2 rounded-full border-2 border-sidebar',
item.enabled === false
? 'bg-muted-foreground/40'
: 'bg-green-500',
)}
/>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<SidebarMenuSubButton
asChild
isActive={isItemActive}
>
<a
href={itemRoute}
className={cn(
isPlugin && !item.debug ? 'pr-6' : '',
)}
</span>
) : null}
<span className="truncate">{item.name}</span>
{item.debug && (
<Bug className="size-3.5 shrink-0 text-orange-400" />
)}
</a>
</SidebarMenuSubButton>
onClick={(e) => {
e.preventDefault();
router.push(itemRoute);
}}
>
{item.emoji ? (
<span className="text-sm shrink-0">
{item.emoji}
</span>
) : item.iconURL ? (
<span className="relative shrink-0">
<img
src={item.iconURL}
alt=""
className="size-4 rounded"
/>
{isBot && (
<span
className={cn(
'absolute -bottom-0.5 -right-0.5 size-2 rounded-full border-2 border-sidebar',
item.enabled === false
? 'bg-muted-foreground/40'
: 'bg-green-500',
)}
/>
)}
</span>
) : null}
<span className="truncate">
{item.name}
</span>
{item.debug && (
<Bug className="size-3.5 shrink-0 text-orange-400" />
)}
</a>
</SidebarMenuSubButton>
</TooltipTrigger>
{item.description && (
<TooltipContent
side="right"
align="center"
className="max-w-64"
>
{item.description.length > 80
? item.description.slice(0, 80) + '…'
: item.description}
</TooltipContent>
)}
</Tooltip>
{/* Plugin context menu - shown on hover (not for debug plugins) */}
{isPlugin && !item.debug && (
<PluginItemMenu

View File

@@ -15,6 +15,7 @@ import { isNewerVersion } from '@/app/utils/versionCompare';
export interface SidebarEntityItem {
id: string;
name: string;
description?: string;
emoji?: string;
iconURL?: string;
updatedAt?: string; // ISO timestamp for sorting by most recently edited
@@ -63,6 +64,7 @@ export function SidebarDataProvider({
resp.bots.map((bot) => ({
id: bot.uuid || '',
name: bot.name,
description: bot.description,
iconURL: httpClient.getAdapterIconURL(bot.adapter),
updatedAt: bot.updated_at,
enabled: bot.enable ?? true,
@@ -80,6 +82,7 @@ export function SidebarDataProvider({
resp.pipelines.map((p) => ({
id: p.uuid || '',
name: p.name,
description: p.description,
emoji: p.emoji,
updatedAt: p.updated_at,
})),
@@ -96,6 +99,7 @@ export function SidebarDataProvider({
resp.bases.map((kb) => ({
id: kb.uuid || '',
name: kb.name,
description: kb.description,
emoji: kb.emoji,
updatedAt: kb.updated_at,
})),