Files
ai-video-fullstack/frontend/src/components/editor/vision-config-section.tsx
T

158 lines
5.1 KiB
TypeScript
Raw Normal View History

"use client";
2026-08-12 17:29:30 +08:00
import { useState } from "react";
import { Camera, Loader2, Sparkles } from "lucide-react";
import {
ResourceSelectField,
ToggleRow,
} from "@/components/assistant-editor/editor-controls";
import { SectionCard } from "@/components/editor/section-card";
2026-08-12 17:29:30 +08:00
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { assistantsApi } from "@/lib/api";
type VisionModelOption = {
value: string;
label: string;
};
export function VisionConfigSection({
description,
hint,
enabled,
modelResourceId,
2026-08-12 17:29:30 +08:00
focusPrompt,
sourcePrompt,
generatorModelResourceId,
mainModelResourceId,
2026-08-05 13:35:00 +08:00
mainModelSupportsImageInput,
modelOptions,
onEnabledChange,
onModelResourceIdChange,
2026-08-12 17:29:30 +08:00
onFocusPromptChange,
}: {
description: string;
hint: string;
enabled: boolean;
modelResourceId: string;
2026-08-12 17:29:30 +08:00
focusPrompt: string;
sourcePrompt: string;
generatorModelResourceId: string;
mainModelResourceId: string;
2026-08-05 13:35:00 +08:00
mainModelSupportsImageInput?: boolean;
modelOptions: VisionModelOption[];
onEnabledChange: (enabled: boolean) => void;
onModelResourceIdChange: (modelResourceId: string) => void;
2026-08-12 17:29:30 +08:00
onFocusPromptChange: (focusPrompt: string) => void;
}) {
2026-08-12 17:29:30 +08:00
const [generating, setGenerating] = useState(false);
const [generationError, setGenerationError] = useState("");
2026-08-05 13:35:00 +08:00
const mainModelSupportsVision =
mainModelSupportsImageInput ??
modelOptions.some((option) => option.value === mainModelResourceId);
const independentModelOptions = modelOptions.filter(
(option) => option.value !== mainModelResourceId,
);
2026-08-12 17:29:30 +08:00
async function generateFocusPrompt() {
if (!sourcePrompt.trim() || !generatorModelResourceId || generating) return;
setGenerating(true);
setGenerationError("");
try {
const result = await assistantsApi.generateVisionFocus({
sourcePrompt,
modelResourceId: generatorModelResourceId,
});
onFocusPromptChange(result.focusPrompt);
} catch (error) {
setGenerationError(
error instanceof Error ? error.message : "生成视觉关注目标失败",
);
} finally {
setGenerating(false);
}
}
return (
<SectionCard
icon={<Camera size={15} />}
title="视觉理解"
description={description}
>
<ToggleRow
title="允许理解当前画面"
hint={hint}
checked={enabled}
onChange={onEnabledChange}
/>
{enabled && (
<>
<ResourceSelectField
label="视觉模型"
value={modelResourceId}
onChange={onModelResourceIdChange}
options={independentModelOptions}
noneLabel="模型自己"
/>
{!modelResourceId && !mainModelSupportsVision && (
<p className="text-xs text-destructive">
2026-08-05 13:35:00 +08:00
当前模型未标记支持图片输入,请选择独立视觉模型。
</p>
)}
2026-08-12 17:29:30 +08:00
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-3">
<label className="text-sm font-medium text-foreground">
视觉关注目标
</label>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 gap-1.5 rounded-full px-2.5 text-xs"
disabled={
generating || !sourcePrompt.trim() || !generatorModelResourceId
}
onClick={() => void generateFocusPrompt()}
title={
!sourcePrompt.trim()
? "请先填写助手提示词"
: !generatorModelResourceId
? "请先选择大语言模型"
: "根据当前助手提示词生成可编辑草稿"
}
>
{generating ? (
<Loader2 size={12} className="animate-spin" />
) : (
<Sparkles size={12} />
)}
{generating ? "生成中" : "根据提示词生成"}
</Button>
</div>
<Textarea
value={focusPrompt}
maxLength={4000}
rows={5}
onChange={(event) => {
onFocusPromptChange(event.target.value);
if (generationError) setGenerationError("");
}}
placeholder="例如:优先识别车辆数量、碰撞位置、明显损伤和人员状态;只报告画面中可直接确认的信息。"
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm leading-6 text-foreground placeholder:text-muted-soft"
/>
<p className="text-[11px] leading-5 text-muted-soft">
可选。系统仍保留通用视觉安全规则;这里用于让每次看图稳定聚焦业务目标。
</p>
{generationError && (
<p className="text-xs leading-5 text-destructive">
{generationError}
</p>
)}
</div>
</>
)}
</SectionCard>
);
}