merlyn/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx
方程 90e474abcb
Support Gitee AI(LLM Provider) (#3361)
* Support Gitee AI(LLM Provider)

* refactor(server): 重构 GiteeAI 模型窗口限制功能,暂时将窗口限制硬编码,计划使用外部 API 数据和缓存

* updates for Gitee AI

* use legacy lookup since gitee does not enable getting token context windows

* add more missing records

* reorder imports

---------

Co-authored-by: 方程 <fangcheng@oschina.cn>
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2025-11-25 14:19:32 -08:00

214 lines
7.1 KiB
JavaScript

import React, { useEffect, useRef, useState } from "react";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import AgentLLMItem from "./AgentLLMItem";
import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference";
import { CaretUpDown, Gauge, MagnifyingGlass, X } from "@phosphor-icons/react";
import AgentModelSelection from "../AgentModelSelection";
import { useTranslation } from "react-i18next";
const ENABLED_PROVIDERS = [
"openai",
"anthropic",
"lmstudio",
"ollama",
"localai",
"groq",
"azure",
"koboldcpp",
"togetherai",
"openrouter",
"novita",
"mistral",
"perplexity",
"textgenwebui",
"generic-openai",
"bedrock",
"fireworksai",
"deepseek",
"ppio",
"litellm",
"apipie",
"xai",
"nvidia-nim",
"gemini",
"moonshotai",
"cometapi",
"foundry",
"zai",
"giteeai",
// TODO: More agent support.
// "cohere", // Has tool calling and will need to build explicit support
// "huggingface" // Can be done but already has issues with no-chat templated. Needs to be tested.
];
const WARN_PERFORMANCE = [
"lmstudio",
"koboldcpp",
"ollama",
"localai",
"textgenwebui",
];
const LLM_DEFAULT = {
name: "System Default",
value: "none",
logo: AnythingLLMIcon,
options: () => <React.Fragment />,
description:
"Agents will use the workspace or system LLM unless otherwise specified.",
requiredConfig: [],
};
const LLMS = [
LLM_DEFAULT,
...AVAILABLE_LLM_PROVIDERS.filter((llm) =>
ENABLED_PROVIDERS.includes(llm.value)
),
];
export default function AgentLLMSelection({
settings,
workspace,
setHasChanges,
}) {
const [filteredLLMs, setFilteredLLMs] = useState([]);
const [selectedLLM, setSelectedLLM] = useState(
workspace?.agentProvider ?? "none"
);
const [searchQuery, setSearchQuery] = useState("");
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null);
const { t } = useTranslation();
function updateLLMChoice(selection) {
setSearchQuery("");
setSelectedLLM(selection);
setSearchMenuOpen(false);
setHasChanges(true);
}
function handleXButton() {
if (searchQuery.length > 0) {
setSearchQuery("");
if (searchInputRef.current) searchInputRef.current.value = "";
} else {
setSearchMenuOpen(!searchMenuOpen);
}
}
useEffect(() => {
const filtered = LLMS.filter((llm) =>
llm.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredLLMs(filtered);
}, [searchQuery, selectedLLM]);
const selectedLLMObject = LLMS.find((llm) => llm.value === selectedLLM);
return (
<div className="border-b border-white/40 pb-8">
{WARN_PERFORMANCE.includes(selectedLLM) && (
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center">
<Gauge className="shrink-0" size={25} />
<p className="text-sm">{t("agent.performance-warning")}</p>
</div>
</div>
)}
<div className="flex flex-col">
<label htmlFor="name" className="block input-label">
{t("agent.provider.title")}
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{t("agent.provider.description")}
</p>
</div>
<div className="relative">
<input type="hidden" name="agentProvider" value={selectedLLM} />
{searchMenuOpen && (
<div
className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
onClick={() => setSearchMenuOpen(false)}
/>
)}
{searchMenuOpen ? (
<div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20">
<div className="w-full flex flex-col gap-y-1">
<div className="flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg">
<MagnifyingGlass
size={20}
weight="bold"
className="absolute left-4 z-30 text-theme-text-primary -ml-4 my-2"
/>
<input
type="text"
name="llm-search"
autoComplete="off"
placeholder="Search available LLM providers"
className="border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium"
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<X
size={20}
weight="bold"
className="cursor-pointer text-theme-text-primary hover:text-x-button"
onClick={handleXButton}
/>
</div>
<div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]">
{filteredLLMs.map((llm) => {
return (
<AgentLLMItem
llm={llm}
key={llm.name}
availableLLMs={LLMS}
settings={settings}
checked={selectedLLM === llm.value}
onClick={() => updateLLMChoice(llm.value)}
/>
);
})}
</div>
</div>
</div>
) : (
<button
className="w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300"
type="button"
onClick={() => setSearchMenuOpen(true)}
>
<div className="flex gap-x-4 items-center">
<img
src={selectedLLMObject.logo}
alt={`${selectedLLMObject.name} logo`}
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col text-left">
<div className="text-sm font-semibold text-white">
{selectedLLMObject.name}
</div>
<div className="mt-1 text-xs text-description">
{selectedLLMObject.description}
</div>
</div>
</div>
<CaretUpDown size={24} weight="bold" className="text-white" />
</button>
)}
</div>
{selectedLLM !== "none" && (
<div className="mt-4 flex flex-col gap-y-1">
<AgentModelSelection
provider={selectedLLM}
workspace={workspace}
setHasChanges={setHasChanges}
/>
</div>
)}
</div>
);
}