Add support for image input in model resources and enhance related configurations
- Introduce a new `support_image_input` field in model resources, allowing models to indicate support for image input. - Update the backend models, schemas, and database seed scripts to accommodate the new field. - Enhance the AssistantConfig and related routes to handle image input capabilities, ensuring proper validation and error handling. - Modify the frontend components to include toggles for enabling visual understanding and filtering models based on image input support. - Implement necessary adjustments in the voice preview and pipeline to integrate video stream handling alongside audio functionalities.
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Copy,
|
||||
Eye,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
@@ -37,6 +38,8 @@ import { FilterPills } from "@/components/ui/filter-pills";
|
||||
import { SearchInput } from "@/components/ui/search-input";
|
||||
import { ListToolbar } from "@/components/ui/list-toolbar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -69,6 +72,7 @@ type ResourceDraft = {
|
||||
interfaceType: string;
|
||||
values: Record<string, unknown>;
|
||||
secrets: Record<string, unknown>;
|
||||
supportImageInput: boolean;
|
||||
};
|
||||
|
||||
const emptyDraft: ResourceDraft = {
|
||||
@@ -77,6 +81,7 @@ const emptyDraft: ResourceDraft = {
|
||||
interfaceType: "",
|
||||
values: {},
|
||||
secrets: {},
|
||||
supportImageInput: false,
|
||||
};
|
||||
|
||||
function defaults(definition: InterfaceDefinition): Record<string, unknown> {
|
||||
@@ -115,6 +120,44 @@ function fieldValue(draft: ResourceDraft, field: InterfaceField): unknown {
|
||||
: draft.values[field.key];
|
||||
}
|
||||
|
||||
function extraBodyText(values: Record<string, unknown>): string {
|
||||
const value = values.extraBody;
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function parseExtraBody(text: string):
|
||||
| { ok: true; value: Record<string, unknown> | null }
|
||||
| { ok: false; error: string } {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return { ok: true, value: null };
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return { ok: false, error: "Extra Body 必须是 JSON 对象" };
|
||||
}
|
||||
return { ok: true, value: parsed as Record<string, unknown> };
|
||||
} catch {
|
||||
return { ok: false, error: "Extra Body 不是合法 JSON" };
|
||||
}
|
||||
}
|
||||
|
||||
function valuesWithExtraBody(
|
||||
values: Record<string, unknown>,
|
||||
extraBody: Record<string, unknown> | null,
|
||||
): Record<string, unknown> {
|
||||
const next = { ...values };
|
||||
delete next.extraBody;
|
||||
if (extraBody) next.extraBody = extraBody;
|
||||
return next;
|
||||
}
|
||||
|
||||
function isSelectableDefinition(definition: InterfaceDefinition): boolean {
|
||||
return (
|
||||
definition.capability !== "LLM" || definition.interfaceType === "openai-llm"
|
||||
);
|
||||
}
|
||||
|
||||
export function ComponentsModelsPage() {
|
||||
const [resources, setResources] = useState<ModelResource[]>([]);
|
||||
const [definitions, setDefinitions] = useState<InterfaceDefinition[]>([]);
|
||||
@@ -128,6 +171,7 @@ export function ComponentsModelsPage() {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<ResourceDraft>(emptyDraft);
|
||||
const [extraBodyDraft, setExtraBodyDraft] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState("");
|
||||
const [testing, setTesting] = useState(false);
|
||||
@@ -161,7 +205,8 @@ export function ComponentsModelsPage() {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const availableDefinitions = definitions.filter(
|
||||
const selectableDefinitions = definitions.filter(isSelectableDefinition);
|
||||
const availableDefinitions = selectableDefinitions.filter(
|
||||
(definition) => definition.capability === draft.capability,
|
||||
);
|
||||
const selectedDefinition = definitions.find(
|
||||
@@ -239,11 +284,12 @@ export function ComponentsModelsPage() {
|
||||
values: definition ? defaults(definition) : {},
|
||||
secrets: {},
|
||||
}));
|
||||
setExtraBodyDraft("");
|
||||
setTestResult(null);
|
||||
}
|
||||
|
||||
function selectCapability(capability: ModelType) {
|
||||
const first = definitions.find(
|
||||
const first = selectableDefinitions.find(
|
||||
(definition) => definition.capability === capability,
|
||||
);
|
||||
setDraft((previous) => ({
|
||||
@@ -252,12 +298,15 @@ export function ComponentsModelsPage() {
|
||||
interfaceType: first?.interfaceType ?? "",
|
||||
values: first ? defaults(first) : {},
|
||||
secrets: {},
|
||||
supportImageInput:
|
||||
capability === "LLM" ? previous.supportImageInput : false,
|
||||
}));
|
||||
setExtraBodyDraft("");
|
||||
setTestResult(null);
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
const first = definitions.find(
|
||||
const first = selectableDefinitions.find(
|
||||
(definition) => definition.capability === "LLM",
|
||||
);
|
||||
setEditingId(null);
|
||||
@@ -265,7 +314,9 @@ export function ComponentsModelsPage() {
|
||||
...emptyDraft,
|
||||
interfaceType: first?.interfaceType ?? "",
|
||||
values: first ? defaults(first) : {},
|
||||
supportImageInput: false,
|
||||
});
|
||||
setExtraBodyDraft("");
|
||||
setSaveError("");
|
||||
setTestResult(null);
|
||||
setDialogOpen(true);
|
||||
@@ -279,7 +330,9 @@ export function ComponentsModelsPage() {
|
||||
interfaceType: resource.interfaceType,
|
||||
values: resource.values,
|
||||
secrets: resource.secrets,
|
||||
supportImageInput: resource.supportImageInput,
|
||||
});
|
||||
setExtraBodyDraft(extraBodyText(resource.values));
|
||||
setSaveError("");
|
||||
setTestResult(null);
|
||||
setDialogOpen(true);
|
||||
@@ -294,9 +347,12 @@ export function ComponentsModelsPage() {
|
||||
setTestResult(null);
|
||||
}
|
||||
|
||||
const parsedExtraBody = parseExtraBody(extraBodyDraft);
|
||||
const extraBodyError = parsedExtraBody.ok ? "" : parsedExtraBody.error;
|
||||
const canSave = Boolean(
|
||||
draft.name.trim() &&
|
||||
selectedDefinition &&
|
||||
!extraBodyError &&
|
||||
selectedDefinition.fieldSchema.fields.every((field) => {
|
||||
if (!field.required) return true;
|
||||
const value = fieldValue(draft, field);
|
||||
@@ -305,11 +361,17 @@ export function ComponentsModelsPage() {
|
||||
);
|
||||
|
||||
function payload() {
|
||||
const extraBody = parsedExtraBody.ok ? parsedExtraBody.value : null;
|
||||
return {
|
||||
name: draft.name.trim(),
|
||||
interfaceType: draft.interfaceType,
|
||||
values: draft.values,
|
||||
values:
|
||||
draft.capability === "LLM"
|
||||
? valuesWithExtraBody(draft.values, extraBody)
|
||||
: draft.values,
|
||||
secrets: draft.secrets,
|
||||
supportImageInput:
|
||||
draft.capability === "LLM" ? draft.supportImageInput : false,
|
||||
enabled: editingId
|
||||
? resources.find((resource) => resource.id === editingId)?.enabled ?? true
|
||||
: true,
|
||||
@@ -457,8 +519,19 @@ export function ComponentsModelsPage() {
|
||||
width: "md:w-[360px]",
|
||||
cell: (resource) => (
|
||||
<>
|
||||
<div className="truncate font-medium text-foreground">
|
||||
{resource.name}
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{resource.name}
|
||||
</span>
|
||||
{resource.supportImageInput && (
|
||||
<span
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-surface-strong text-muted-foreground"
|
||||
title="支持图片输入"
|
||||
aria-label="支持图片输入"
|
||||
>
|
||||
<Eye size={12} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 truncate text-xs text-muted-soft">
|
||||
{String(
|
||||
@@ -652,6 +725,20 @@ export function ComponentsModelsPage() {
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
{draft.capability === "LLM" && (
|
||||
<div className="flex items-center justify-between rounded-xl border border-hairline p-4">
|
||||
<span className="text-sm font-medium">支持图片输入</span>
|
||||
<Switch
|
||||
checked={draft.supportImageInput}
|
||||
onCheckedChange={(checked) =>
|
||||
setDraft((previous) => ({
|
||||
...previous,
|
||||
supportImageInput: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</FieldSection>
|
||||
|
||||
<FieldSection title="连接与鉴权信息" scrollable fill>
|
||||
@@ -667,6 +754,25 @@ export function ComponentsModelsPage() {
|
||||
</div>
|
||||
|
||||
<FieldSection title="可选项" scrollable tall>
|
||||
{draft.capability === "LLM" && (
|
||||
<Field label="Extra Body">
|
||||
<Textarea
|
||||
rows={7}
|
||||
value={extraBodyDraft}
|
||||
onChange={(event) => {
|
||||
setExtraBodyDraft(event.target.value);
|
||||
setTestResult(null);
|
||||
}}
|
||||
placeholder={'例如:{\n "thinking": { "type": "disabled" }\n}'}
|
||||
className="font-mono text-xs leading-5"
|
||||
/>
|
||||
{extraBodyError && (
|
||||
<div className="text-xs text-destructive">
|
||||
{extraBodyError}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
{optionalFields.length > 0 ? (
|
||||
optionalFields.map((field) => (
|
||||
<DynamicField
|
||||
@@ -676,7 +782,7 @@ export function ComponentsModelsPage() {
|
||||
onChange={(value) => updateField(field, value)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
) : draft.capability === "LLM" ? null : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
当前接口没有可选项
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user