kit初版,模型引入,agent优化
This commit is contained in:
125
art-agent/frontend/src/components/chat/model-selector.tsx
Normal file
125
art-agent/frontend/src/components/chat/model-selector.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ImageModelInfo } from "@/lib/types";
|
||||
import { fetchModels } from "@/lib/api";
|
||||
|
||||
const STORAGE_KEY = "epeekit-selected-image-model";
|
||||
|
||||
interface ModelSelectorProps {
|
||||
value: string;
|
||||
onChange: (modelId: string) => void;
|
||||
}
|
||||
|
||||
export function ModelSelector({ value, onChange }: ModelSelectorProps) {
|
||||
const [models, setModels] = useState<ImageModelInfo[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchModels()
|
||||
.then(({ models: list, default: defaultId }) => {
|
||||
setModels(list);
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
const validIds = new Set(list.map((m) => m.id));
|
||||
if (saved && validIds.has(saved)) {
|
||||
onChange(saved);
|
||||
} else if (!value || !validIds.has(value)) {
|
||||
onChange(defaultId);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 后端不可用时静默降级
|
||||
});
|
||||
// 仅初始化时执行一次
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// 点击外部关闭
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [open]);
|
||||
|
||||
const selected = models.find((m) => m.id === value);
|
||||
|
||||
if (models.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs
|
||||
border border-[var(--border)] bg-[var(--bg-tertiary)]
|
||||
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||
hover:border-[var(--accent)] transition-colors cursor-pointer"
|
||||
title="切换生图模型"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z" />
|
||||
<path d="M2 17l10 5 10-5" />
|
||||
<path d="M2 12l10 5 10-5" />
|
||||
</svg>
|
||||
<span className="max-w-[100px] truncate">{selected?.name ?? "模型"}</span>
|
||||
<svg
|
||||
width="10" height="10" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2.5"
|
||||
className={`transition-transform ${open ? "rotate-180" : ""}`}
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className="absolute bottom-full left-0 mb-1.5 w-56 rounded-lg
|
||||
border border-[var(--border)] bg-[var(--bg-secondary)]
|
||||
shadow-lg overflow-hidden z-50"
|
||||
>
|
||||
{models.map((m) => {
|
||||
const isActive = m.id === value;
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={() => {
|
||||
onChange(m.id);
|
||||
localStorage.setItem(STORAGE_KEY, m.id);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2.5 flex flex-col gap-0.5
|
||||
transition-colors cursor-pointer
|
||||
${isActive
|
||||
? "bg-[var(--accent)]/10 text-[var(--accent)]"
|
||||
: "text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm font-medium flex items-center gap-1.5">
|
||||
{m.name}
|
||||
{m.supports_ref_image && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full
|
||||
bg-emerald-500/15 text-emerald-400 font-normal leading-none">
|
||||
参考图
|
||||
</span>
|
||||
)}
|
||||
{isActive && (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-xs text-[var(--text-secondary)]">{m.description}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user