2026-08-01 23:31:14 +08:00
|
|
|
"use client";
|
|
|
|
|
|
2026-08-12 17:29:30 +08:00
|
|
|
import { useState } from "react";
|
|
|
|
|
import { Camera, Loader2, Sparkles } from "lucide-react";
|
2026-08-01 23:31:14 +08:00
|
|
|
|
|
|
|
|
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";
|
2026-08-01 23:31:14 +08:00
|
|
|
|
|
|
|
|
type VisionModelOption = {
|
|
|
|
|
value: string;
|
|
|
|
|
label: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function VisionConfigSection({
|
|
|
|
|
description,
|
|
|
|
|
hint,
|
|
|
|
|
enabled,
|
|
|
|
|
modelResourceId,
|
2026-08-12 17:29:30 +08:00
|
|
|
focusPrompt,
|
|
|
|
|
sourcePrompt,
|
|
|
|
|
generatorModelResourceId,
|
2026-08-01 23:31:14 +08:00
|
|
|
mainModelResourceId,
|
2026-08-05 13:35:00 +08:00
|
|
|
mainModelSupportsImageInput,
|
2026-08-01 23:31:14 +08:00
|
|
|
modelOptions,
|
|
|
|
|
onEnabledChange,
|
|
|
|
|
onModelResourceIdChange,
|
2026-08-12 17:29:30 +08:00
|
|
|
onFocusPromptChange,
|
2026-08-01 23:31:14 +08:00
|
|
|
}: {
|
|
|
|
|
description: string;
|
|
|
|
|
hint: string;
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
modelResourceId: string;
|
2026-08-12 17:29:30 +08:00
|
|
|
focusPrompt: string;
|
|
|
|
|
sourcePrompt: string;
|
|
|
|
|
generatorModelResourceId: string;
|
2026-08-01 23:31:14 +08:00
|
|
|
mainModelResourceId: string;
|
2026-08-05 13:35:00 +08:00
|
|
|
mainModelSupportsImageInput?: boolean;
|
2026-08-01 23:31:14 +08:00
|
|
|
modelOptions: VisionModelOption[];
|
|
|
|
|
onEnabledChange: (enabled: boolean) => void;
|
|
|
|
|
onModelResourceIdChange: (modelResourceId: string) => void;
|
2026-08-12 17:29:30 +08:00
|
|
|
onFocusPromptChange: (focusPrompt: string) => void;
|
2026-08-01 23:31:14 +08:00
|
|
|
}) {
|
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);
|
2026-08-01 23:31:14 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 23:31:14 +08:00
|
|
|
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
|
|
|
当前模型未标记支持图片输入,请选择独立视觉模型。
|
2026-08-01 23:31:14 +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>
|
2026-08-01 23:31:14 +08:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</SectionCard>
|
|
|
|
|
);
|
|
|
|
|
}
|