mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 18:06:06 +00:00
feat(mcp): support mcp resources (#2215)
* feat(mcp): support mcp resources * feat(web): split MCP resources into tab * docs: add MCP resources PR review * feat(mcp): productionize resource support * feat(mcp): scope local agent tools and resources * fix(web): gate space embedding models behind login * fix(web): prevent clipped space model CTA * test: update preproc resource tool expectations * fix(web): expose skill authoring tools in selector --------- Co-authored-by: yang.xiang <yang.xiang@advancegroup.com> Co-authored-by: Hyu <chenhyu@proton.me> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
DynamicFormItemType,
|
||||
IDynamicFormItemSchema,
|
||||
SYSTEM_FIELD_PREFIX,
|
||||
} from '@/app/infra/entities/form/dynamic';
|
||||
@@ -57,6 +58,95 @@ function resolveShowIfValue(
|
||||
return externalDependentValues?.[field];
|
||||
}
|
||||
|
||||
type DynamicFormValueSpec = Pick<
|
||||
IDynamicFormItemSchema,
|
||||
'default' | 'name' | 'required' | 'type'
|
||||
>;
|
||||
|
||||
function getValueSpecs(item: IDynamicFormItemSchema): DynamicFormValueSpec[] {
|
||||
if (item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR) {
|
||||
return [
|
||||
item,
|
||||
{
|
||||
name: 'enable-all-tools',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (item.type === DynamicFormItemType.RESOURCES_SELECTOR) {
|
||||
return [
|
||||
item,
|
||||
{
|
||||
name: 'mcp-resources',
|
||||
type: DynamicFormItemType.UNKNOWN,
|
||||
required: false,
|
||||
default: [],
|
||||
},
|
||||
{
|
||||
name: 'mcp-resource-agent-read-enabled',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [item];
|
||||
}
|
||||
|
||||
function getValueSchema(spec: DynamicFormValueSpec) {
|
||||
if (spec.name === 'mcp-resources') {
|
||||
return z.array(z.any());
|
||||
}
|
||||
|
||||
switch (spec.type) {
|
||||
case DynamicFormItemType.INT:
|
||||
return z.number();
|
||||
case DynamicFormItemType.FLOAT:
|
||||
return z.number();
|
||||
case DynamicFormItemType.BOOLEAN:
|
||||
return z.boolean();
|
||||
case DynamicFormItemType.STRING:
|
||||
return z.string();
|
||||
case DynamicFormItemType.STRING_ARRAY:
|
||||
return z.array(z.string());
|
||||
case DynamicFormItemType.SELECT:
|
||||
return z.string();
|
||||
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.RERANK_MODEL_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.KNOWLEDGE_BASE_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.KNOWLEDGE_BASE_MULTI_SELECTOR:
|
||||
case DynamicFormItemType.RESOURCES_SELECTOR:
|
||||
case DynamicFormItemType.RICH_TOOLS_SELECTOR:
|
||||
case DynamicFormItemType.TOOLS_SELECTOR:
|
||||
return z.array(z.string());
|
||||
case DynamicFormItemType.BOT_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.MODEL_FALLBACK_SELECTOR:
|
||||
return z.object({
|
||||
primary: z.string(),
|
||||
fallbacks: z.array(z.string()),
|
||||
});
|
||||
case DynamicFormItemType.PROMPT_EDITOR:
|
||||
return z.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
role: z.string(),
|
||||
}),
|
||||
);
|
||||
default:
|
||||
return z.string();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display-only component for embed code fields with copy animation.
|
||||
*/
|
||||
@@ -322,9 +412,16 @@ export default function DynamicFormComponent({
|
||||
// model-fallback-selector) is coerced to the expected shape
|
||||
// so that downstream components never crash.
|
||||
const normalizeFieldValue = (
|
||||
item: IDynamicFormItemSchema,
|
||||
item: DynamicFormValueSpec,
|
||||
value: unknown,
|
||||
): unknown => {
|
||||
if (
|
||||
item.name === 'mcp-resources' ||
|
||||
item.type === DynamicFormItemType.RESOURCES_SELECTOR ||
|
||||
item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR
|
||||
) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
if (item.type === 'model-fallback-selector') {
|
||||
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const obj = value as Record<string, unknown>;
|
||||
@@ -368,68 +465,16 @@ export default function DynamicFormComponent({
|
||||
[itemConfigList],
|
||||
);
|
||||
|
||||
const editableValueSpecs = useMemo(
|
||||
() => editableItems.flatMap(getValueSpecs),
|
||||
[editableItems],
|
||||
);
|
||||
|
||||
// 根据 itemConfigList 动态生成 zod schema
|
||||
const formSchema = z.object(
|
||||
editableItems.reduce(
|
||||
editableValueSpecs.reduce(
|
||||
(acc, item) => {
|
||||
let fieldSchema;
|
||||
switch (item.type) {
|
||||
case 'integer':
|
||||
fieldSchema = z.number();
|
||||
break;
|
||||
case 'float':
|
||||
fieldSchema = z.number();
|
||||
break;
|
||||
case 'boolean':
|
||||
fieldSchema = z.boolean();
|
||||
break;
|
||||
case 'string':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'array[string]':
|
||||
fieldSchema = z.array(z.string());
|
||||
break;
|
||||
case 'select':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'llm-model-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'embedding-model-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'rerank-model-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'knowledge-base-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'knowledge-base-multi-selector':
|
||||
fieldSchema = z.array(z.string());
|
||||
break;
|
||||
case 'bot-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'tools-selector':
|
||||
fieldSchema = z.array(z.string());
|
||||
break;
|
||||
case 'model-fallback-selector':
|
||||
fieldSchema = z.object({
|
||||
primary: z.string(),
|
||||
fallbacks: z.array(z.string()),
|
||||
});
|
||||
break;
|
||||
case 'prompt-editor':
|
||||
fieldSchema = z.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
role: z.string(),
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
fieldSchema = z.string();
|
||||
}
|
||||
let fieldSchema = getValueSchema(item);
|
||||
|
||||
if (
|
||||
item.required &&
|
||||
@@ -454,7 +499,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: editableItems.reduce((acc, item) => {
|
||||
defaultValues: editableValueSpecs.reduce((acc, item) => {
|
||||
// 优先使用 initialValues,如果没有则使用默认值
|
||||
const rawValue = initialValues?.[item.name] ?? item.default;
|
||||
return {
|
||||
@@ -493,7 +538,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
if (initialValues && hasRealChange) {
|
||||
// 合并默认值和初始值
|
||||
const mergedValues = editableItems.reduce(
|
||||
const mergedValues = editableValueSpecs.reduce(
|
||||
(acc, item) => {
|
||||
const rawValue = initialValues[item.name] ?? item.default;
|
||||
acc[item.name] = normalizeFieldValue(item, rawValue) as object;
|
||||
@@ -508,10 +553,16 @@ export default function DynamicFormComponent({
|
||||
|
||||
previousInitialValues.current = initialValues;
|
||||
}
|
||||
}, [initialValues, form, editableItems]);
|
||||
}, [initialValues, form, editableValueSpecs]);
|
||||
|
||||
// Get reactive form values for conditional rendering
|
||||
const watchedValues = form.watch();
|
||||
const setFormValue = (name: string, value: unknown) => {
|
||||
form.setValue(name as keyof FormValues, value as never, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Stable ref for onSubmit to avoid re-triggering the effect when the
|
||||
// parent passes a new closure on every render.
|
||||
@@ -524,7 +575,7 @@ export default function DynamicFormComponent({
|
||||
// even if the user saves without modifying any field.
|
||||
// form.watch(callback) only fires on subsequent changes, not on mount.
|
||||
const formValues = form.getValues();
|
||||
const initialFinalValues = editableItems.reduce(
|
||||
const initialFinalValues = editableValueSpecs.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.name] = formValues[item.name] ?? item.default;
|
||||
return acc;
|
||||
@@ -544,7 +595,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
const subscription = form.watch(() => {
|
||||
const formValues = form.getValues();
|
||||
const finalValues = editableItems.reduce(
|
||||
const finalValues = editableValueSpecs.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.name] = formValues[item.name] ?? item.default;
|
||||
return acc;
|
||||
@@ -555,7 +606,7 @@ export default function DynamicFormComponent({
|
||||
previousInitialValues.current = finalValues as Record<string, object>;
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form, editableItems]);
|
||||
}, [form, editableValueSpecs]);
|
||||
|
||||
// State for QR code login dialog
|
||||
const [qrDialogOpen, setQrDialogOpen] = useState(false);
|
||||
@@ -786,6 +837,41 @@ export default function DynamicFormComponent({
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
config.type === DynamicFormItemType.RICH_TOOLS_SELECTOR ||
|
||||
config.type === DynamicFormItemType.RESOURCES_SELECTOR
|
||||
) {
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
<FormItem className="min-w-0">
|
||||
<FormControl>
|
||||
<div
|
||||
className={cn(
|
||||
'min-w-0 max-w-full overflow-x-hidden',
|
||||
isFieldDisabled && 'pointer-events-none opacity-60',
|
||||
)}
|
||||
>
|
||||
<DynamicFormItemComponent
|
||||
config={config}
|
||||
field={field}
|
||||
formValues={watchedValues as Record<string, unknown>}
|
||||
onFileUploaded={onFileUploaded}
|
||||
setFormValue={setFormValue}
|
||||
systemContext={systemContext}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Boolean fields use a special inline layout
|
||||
if (config.type === 'boolean') {
|
||||
return (
|
||||
@@ -816,7 +902,10 @@ export default function DynamicFormComponent({
|
||||
<DynamicFormItemComponent
|
||||
config={config}
|
||||
field={field}
|
||||
formValues={watchedValues as Record<string, unknown>}
|
||||
onFileUploaded={onFileUploaded}
|
||||
setFormValue={setFormValue}
|
||||
systemContext={systemContext}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
@@ -853,7 +942,10 @@ export default function DynamicFormComponent({
|
||||
<DynamicFormItemComponent
|
||||
config={config}
|
||||
field={field}
|
||||
formValues={watchedValues as Record<string, unknown>}
|
||||
onFileUploaded={onFileUploaded}
|
||||
setFormValue={setFormValue}
|
||||
systemContext={systemContext}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
@@ -64,15 +64,23 @@ import {
|
||||
import SettingsDialog, {
|
||||
SettingsSection,
|
||||
} from '@/app/home/components/settings-dialog/SettingsDialog';
|
||||
import ToolResourceSelectors from '@/app/home/components/dynamic-form/ToolResourceSelectors';
|
||||
import { LANGBOT_MODELS_PROVIDER_REQUESTER } from '@/app/home/components/models-dialog/types';
|
||||
|
||||
export default function DynamicFormItemComponent({
|
||||
config,
|
||||
field,
|
||||
formValues,
|
||||
onFileUploaded,
|
||||
setFormValue,
|
||||
systemContext,
|
||||
}: {
|
||||
config: IDynamicFormItemSchema;
|
||||
field: ControllerRenderProps<any, any>;
|
||||
formValues?: Record<string, unknown>;
|
||||
onFileUploaded?: (fileKey: string) => void;
|
||||
setFormValue?: (name: string, value: unknown) => void;
|
||||
systemContext?: Record<string, unknown>;
|
||||
}) {
|
||||
const [llmModels, setLlmModels] = useState<LLMModel[]>([]);
|
||||
const [embeddingModels, setEmbeddingModels] = useState<EmbeddingModel[]>([]);
|
||||
@@ -103,10 +111,34 @@ export default function DynamicFormItemComponent({
|
||||
});
|
||||
};
|
||||
|
||||
const fetchEmbeddingModels = () => {
|
||||
httpClient
|
||||
.getProviderEmbeddingModels()
|
||||
.then((resp) => {
|
||||
setEmbeddingModels(resp.models);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(t('embedding.getModelListError') + err.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchRerankModels = () => {
|
||||
httpClient
|
||||
.getProviderRerankModels()
|
||||
.then((resp) => {
|
||||
setRerankModels(resp.models);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Failed to load rerank models: ' + err.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const handleModelsDialogChange = (open: boolean) => {
|
||||
setModelsDialogOpen(open);
|
||||
if (!open) {
|
||||
fetchLlmModels();
|
||||
fetchEmbeddingModels();
|
||||
fetchRerankModels();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -174,27 +206,13 @@ export default function DynamicFormItemComponent({
|
||||
|
||||
useEffect(() => {
|
||||
if (config.type === DynamicFormItemType.EMBEDDING_MODEL_SELECTOR) {
|
||||
httpClient
|
||||
.getProviderEmbeddingModels()
|
||||
.then((resp) => {
|
||||
setEmbeddingModels(resp.models);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(t('embedding.getModelListError') + err.msg);
|
||||
});
|
||||
fetchEmbeddingModels();
|
||||
}
|
||||
}, [config.type]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config.type === DynamicFormItemType.RERANK_MODEL_SELECTOR) {
|
||||
httpClient
|
||||
.getProviderRerankModels()
|
||||
.then((resp) => {
|
||||
setRerankModels(resp.models);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Failed to load rerank models: ' + err.msg);
|
||||
});
|
||||
fetchRerankModels();
|
||||
}
|
||||
}, [config.type]);
|
||||
|
||||
@@ -248,6 +266,16 @@ export default function DynamicFormItemComponent({
|
||||
}
|
||||
}, [config.type]);
|
||||
|
||||
const handleCompositePatch = (patch: Record<string, unknown>) => {
|
||||
for (const [name, value] of Object.entries(patch)) {
|
||||
if (setFormValue) {
|
||||
setFormValue(name, value);
|
||||
} else if (name === field.name) {
|
||||
field.onChange(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
switch (config.type) {
|
||||
case DynamicFormItemType.INT:
|
||||
case DynamicFormItemType.FLOAT:
|
||||
@@ -377,10 +405,10 @@ export default function DynamicFormItemComponent({
|
||||
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
||||
// Separate space models from regular models
|
||||
const spaceModels = llmModels.filter(
|
||||
(m) => m.provider?.requester === 'space-chat-completions',
|
||||
(m) => m.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
const regularModels = llmModels.filter(
|
||||
(m) => m.provider?.requester !== 'space-chat-completions',
|
||||
(m) => m.provider?.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
|
||||
// Group regular models by provider
|
||||
@@ -481,7 +509,7 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
))}
|
||||
{/* Blurred remaining models with login overlay */}
|
||||
<div className="relative">
|
||||
<div className="relative min-h-10">
|
||||
<div
|
||||
className="select-none overflow-hidden"
|
||||
style={{ maxHeight: '3rem' }}
|
||||
@@ -558,7 +586,10 @@ export default function DynamicFormItemComponent({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={() => setModelsDialogOpen(true)}
|
||||
onClick={() => {
|
||||
setSettingsSection('models');
|
||||
setModelsDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
@@ -574,9 +605,15 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR:
|
||||
// Group embedding models by provider
|
||||
const groupedEmbeddingModels = embeddingModels.reduce(
|
||||
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR: {
|
||||
const spaceEmbeddingModels = embeddingModels.filter(
|
||||
(m) => m.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
const regularEmbeddingModels = embeddingModels.filter(
|
||||
(m) => m.provider?.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
|
||||
const groupedEmbeddingModels = regularEmbeddingModels.reduce(
|
||||
(acc, model) => {
|
||||
const providerName = model.provider?.name || 'Unknown';
|
||||
if (!acc[providerName]) acc[providerName] = [];
|
||||
@@ -586,29 +623,169 @@ export default function DynamicFormItemComponent({
|
||||
{} as Record<string, EmbeddingModel[]>,
|
||||
);
|
||||
|
||||
const groupedSpaceEmbeddingModels = spaceEmbeddingModels.reduce(
|
||||
(acc, model) => {
|
||||
const providerName =
|
||||
model.provider?.name || model.provider?.requester || 'Unknown';
|
||||
if (!acc[providerName]) acc[providerName] = [];
|
||||
acc[providerName].push(model);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, EmbeddingModel[]>,
|
||||
);
|
||||
|
||||
const previewEmbeddingModelNames = [
|
||||
'text-embedding-3-large',
|
||||
'text-embedding-3-small',
|
||||
'bge-m3',
|
||||
'jina-embeddings-v3',
|
||||
'qwen3-embedding-8b',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md min-w-0">
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="min-w-0 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>
|
||||
))}
|
||||
<div className="flex w-full max-w-md min-w-0 items-center gap-1.5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="min-w-0 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>
|
||||
),
|
||||
)}
|
||||
{showSpaceLoginCTA ? (
|
||||
<SelectGroup>
|
||||
<SelectLabel>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||
{t('models.langbotModels')}
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<Info className="h-3 w-3 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-[240px]">
|
||||
{t('models.spaceTrialTooltip')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</SelectLabel>
|
||||
<div
|
||||
className="relative"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
{(spaceEmbeddingModels.length > 0
|
||||
? spaceEmbeddingModels.map((m) => m.name)
|
||||
: previewEmbeddingModelNames
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((name) => (
|
||||
<div
|
||||
key={name}
|
||||
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-muted-foreground/60"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
<div className="relative min-h-10">
|
||||
<div
|
||||
className="select-none overflow-hidden"
|
||||
style={{ maxHeight: '3rem' }}
|
||||
>
|
||||
{(spaceEmbeddingModels.length > 0
|
||||
? spaceEmbeddingModels.map((m) => m.name)
|
||||
: previewEmbeddingModelNames
|
||||
)
|
||||
.slice(3)
|
||||
.map((name) => (
|
||||
<div
|
||||
key={name}
|
||||
className="flex w-full items-center py-1.5 pl-8 pr-2 text-sm text-muted-foreground/40 blur-[2px]"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-b from-transparent to-background/80">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs px-3 gap-1.5 shadow-sm"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSpaceLogin();
|
||||
}}
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
{t('models.unlockModels')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectGroup>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : !systemInfo.disable_models_service ? (
|
||||
Object.entries(groupedSpaceEmbeddingModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectLabel>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||
{providerName}
|
||||
</span>
|
||||
</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
),
|
||||
)
|
||||
) : null}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={() => {
|
||||
setSettingsSection('models');
|
||||
setModelsDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{t('models.title')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<SettingsDialog
|
||||
open={modelsDialogOpen}
|
||||
onOpenChange={handleModelsDialogChange}
|
||||
section={settingsSection}
|
||||
onSectionChange={setSettingsSection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case DynamicFormItemType.RERANK_MODEL_SELECTOR:
|
||||
const groupedRerankModels = rerankModels.reduce(
|
||||
@@ -652,10 +829,10 @@ export default function DynamicFormItemComponent({
|
||||
case DynamicFormItemType.MODEL_FALLBACK_SELECTOR: {
|
||||
// Separate space models from regular models
|
||||
const fbSpaceModels = llmModels.filter(
|
||||
(m) => m.provider?.requester === 'space-chat-completions',
|
||||
(m) => m.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
const fbRegularModels = llmModels.filter(
|
||||
(m) => m.provider?.requester !== 'space-chat-completions',
|
||||
(m) => m.provider?.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
|
||||
// Group regular models by provider
|
||||
@@ -786,7 +963,7 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
))}
|
||||
{/* Blurred remaining models with login overlay */}
|
||||
<div className="relative">
|
||||
<div className="relative min-h-10">
|
||||
<div
|
||||
className="select-none overflow-hidden"
|
||||
style={{ maxHeight: '3rem' }}
|
||||
@@ -1383,6 +1560,32 @@ export default function DynamicFormItemComponent({
|
||||
</>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.RICH_TOOLS_SELECTOR:
|
||||
return (
|
||||
<ToolResourceSelectors
|
||||
mode="tools"
|
||||
pipelineId={systemContext?.pipeline_id as string | undefined}
|
||||
value={{
|
||||
...(formValues || {}),
|
||||
[field.name]: field.value,
|
||||
}}
|
||||
onChange={handleCompositePatch}
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.RESOURCES_SELECTOR:
|
||||
return (
|
||||
<ToolResourceSelectors
|
||||
mode="resources"
|
||||
pipelineId={systemContext?.pipeline_id as string | undefined}
|
||||
value={{
|
||||
...(formValues || {}),
|
||||
[field.name]: field.value,
|
||||
}}
|
||||
onChange={handleCompositePatch}
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.PROMPT_EDITOR: {
|
||||
// Guard: field.value may be undefined when the form resets or
|
||||
// initialValues haven't propagated yet. Fall back to a default
|
||||
|
||||
@@ -56,6 +56,13 @@ export function getDefaultValues(
|
||||
return acc;
|
||||
}
|
||||
acc[item.name] = item.default;
|
||||
if (item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR) {
|
||||
acc['enable-all-tools'] = true;
|
||||
}
|
||||
if (item.type === DynamicFormItemType.RESOURCES_SELECTOR) {
|
||||
acc['mcp-resources'] = [];
|
||||
acc['mcp-resource-agent-read-enabled'] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user