ModelSettings.tsx 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185
  1. import { useEffect, useState, useRef } from 'react';
  2. import type { KeyboardEvent } from 'react';
  3. import { Button } from '@extension/ui';
  4. import {
  5. llmProviderStore,
  6. agentModelStore,
  7. AgentNameEnum,
  8. llmProviderModelNames,
  9. ProviderTypeEnum,
  10. OPENAI_PROVIDER,
  11. ANTHROPIC_PROVIDER,
  12. GEMINI_PROVIDER,
  13. OLLAMA_PROVIDER,
  14. llmProviderParameters,
  15. } from '@extension/storage';
  16. interface ModelSettingsProps {
  17. isDarkMode?: boolean;
  18. }
  19. export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => {
  20. const [providers, setProviders] = useState<
  21. Record<
  22. string,
  23. {
  24. apiKey: string;
  25. baseUrl?: string;
  26. name?: string;
  27. modelNames?: string[];
  28. type?: ProviderTypeEnum;
  29. createdAt?: number;
  30. }
  31. >
  32. >({});
  33. const [modifiedProviders, setModifiedProviders] = useState<Set<string>>(new Set());
  34. const [providersFromStorage, setProvidersFromStorage] = useState<Set<string>>(new Set());
  35. const [selectedModels, setSelectedModels] = useState<Record<AgentNameEnum, string>>({
  36. [AgentNameEnum.Navigator]: '',
  37. [AgentNameEnum.Planner]: '',
  38. [AgentNameEnum.Validator]: '',
  39. });
  40. const [modelParameters, setModelParameters] = useState<Record<AgentNameEnum, { temperature: number; topP: number }>>({
  41. [AgentNameEnum.Navigator]: { temperature: 0, topP: 0 },
  42. [AgentNameEnum.Planner]: { temperature: 0, topP: 0 },
  43. [AgentNameEnum.Validator]: { temperature: 0, topP: 0 },
  44. });
  45. const [newModelInputs, setNewModelInputs] = useState<Record<string, string>>({});
  46. const [isProviderSelectorOpen, setIsProviderSelectorOpen] = useState(false);
  47. const newlyAddedProviderRef = useRef<string | null>(null);
  48. const [nameErrors, setNameErrors] = useState<Record<string, string>>({});
  49. useEffect(() => {
  50. const loadProviders = async () => {
  51. try {
  52. const allProviders = await llmProviderStore.getAllProviders();
  53. console.log('allProviders', allProviders);
  54. // Track which providers are from storage
  55. const fromStorage = new Set(Object.keys(allProviders));
  56. setProvidersFromStorage(fromStorage);
  57. // Only use providers from storage, don't add default ones
  58. setProviders(allProviders);
  59. } catch (error) {
  60. console.error('Error loading providers:', error);
  61. // Set empty providers on error
  62. setProviders({});
  63. // No providers from storage on error
  64. setProvidersFromStorage(new Set());
  65. }
  66. };
  67. loadProviders();
  68. }, []);
  69. // Load existing agent models and parameters on mount
  70. useEffect(() => {
  71. const loadAgentModels = async () => {
  72. try {
  73. const models: Record<AgentNameEnum, string> = {
  74. [AgentNameEnum.Planner]: '',
  75. [AgentNameEnum.Navigator]: '',
  76. [AgentNameEnum.Validator]: '',
  77. };
  78. for (const agent of Object.values(AgentNameEnum)) {
  79. const config = await agentModelStore.getAgentModel(agent);
  80. if (config) {
  81. models[agent] = config.modelName;
  82. if (config.parameters?.temperature !== undefined || config.parameters?.topP !== undefined) {
  83. setModelParameters(prev => ({
  84. ...prev,
  85. [agent]: {
  86. temperature: config.parameters?.temperature ?? prev[agent].temperature,
  87. topP: config.parameters?.topP ?? prev[agent].topP,
  88. },
  89. }));
  90. }
  91. }
  92. }
  93. setSelectedModels(models);
  94. } catch (error) {
  95. console.error('Error loading agent models:', error);
  96. }
  97. };
  98. loadAgentModels();
  99. }, []);
  100. // Auto-focus the input field when a new provider is added
  101. useEffect(() => {
  102. // Only focus if we have a newly added provider reference
  103. if (newlyAddedProviderRef.current && providers[newlyAddedProviderRef.current]) {
  104. const providerId = newlyAddedProviderRef.current;
  105. const config = providers[providerId];
  106. // For custom providers, focus on the name input
  107. if (config.type === ProviderTypeEnum.CustomOpenAI) {
  108. const nameInput = document.getElementById(`${providerId}-name`);
  109. if (nameInput) {
  110. nameInput.focus();
  111. }
  112. } else {
  113. // For default providers, focus on the API key input
  114. const apiKeyInput = document.getElementById(`${providerId}-api-key`);
  115. if (apiKeyInput) {
  116. apiKeyInput.focus();
  117. }
  118. }
  119. // Clear the ref after focusing
  120. newlyAddedProviderRef.current = null;
  121. }
  122. }, [providers]);
  123. // Add a click outside handler to close the dropdown
  124. useEffect(() => {
  125. const handleClickOutside = (event: MouseEvent) => {
  126. const target = event.target as HTMLElement;
  127. if (isProviderSelectorOpen && !target.closest('.provider-selector-container')) {
  128. setIsProviderSelectorOpen(false);
  129. }
  130. };
  131. document.addEventListener('mousedown', handleClickOutside);
  132. return () => {
  133. document.removeEventListener('mousedown', handleClickOutside);
  134. };
  135. }, [isProviderSelectorOpen]);
  136. const handleApiKeyChange = (provider: string, apiKey: string, baseUrl?: string) => {
  137. setModifiedProviders(prev => new Set(prev).add(provider));
  138. setProviders(prev => ({
  139. ...prev,
  140. [provider]: {
  141. ...prev[provider],
  142. apiKey: apiKey.trim(),
  143. baseUrl: baseUrl !== undefined ? baseUrl.trim() : prev[provider]?.baseUrl,
  144. },
  145. }));
  146. };
  147. const handleNameChange = (provider: string, name: string) => {
  148. console.log('handleNameChange called with:', provider, name);
  149. setModifiedProviders(prev => new Set(prev).add(provider));
  150. setProviders(prev => {
  151. const updated = {
  152. ...prev,
  153. [provider]: {
  154. ...prev[provider],
  155. name: name.trim(),
  156. },
  157. };
  158. console.log('Updated providers state:', updated);
  159. return updated;
  160. });
  161. };
  162. const handleModelsChange = (provider: string, modelsString: string) => {
  163. setNewModelInputs(prev => ({
  164. ...prev,
  165. [provider]: modelsString,
  166. }));
  167. };
  168. const addModel = (provider: string, model: string) => {
  169. if (!model.trim()) return;
  170. setModifiedProviders(prev => new Set(prev).add(provider));
  171. setProviders(prev => {
  172. const providerData = prev[provider] || {};
  173. // Get current models - either from provider config or default models
  174. let currentModels = providerData.modelNames;
  175. if (currentModels === undefined) {
  176. currentModels = [...(llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [])];
  177. }
  178. // Don't add duplicates
  179. if (currentModels.includes(model.trim())) return prev;
  180. return {
  181. ...prev,
  182. [provider]: {
  183. ...providerData,
  184. modelNames: [...currentModels, model.trim()],
  185. },
  186. };
  187. });
  188. // Clear the input
  189. setNewModelInputs(prev => ({
  190. ...prev,
  191. [provider]: '',
  192. }));
  193. };
  194. const removeModel = (provider: string, modelToRemove: string) => {
  195. setModifiedProviders(prev => new Set(prev).add(provider));
  196. setProviders(prev => {
  197. const providerData = prev[provider] || {};
  198. // If modelNames doesn't exist in the provider data yet, we need to initialize it
  199. // with the default models from llmProviderModelNames first
  200. if (!providerData.modelNames) {
  201. const defaultModels = llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [];
  202. const filteredModels = defaultModels.filter(model => model !== modelToRemove);
  203. return {
  204. ...prev,
  205. [provider]: {
  206. ...providerData,
  207. modelNames: filteredModels,
  208. },
  209. };
  210. }
  211. // If modelNames already exists, just filter out the model to remove
  212. return {
  213. ...prev,
  214. [provider]: {
  215. ...providerData,
  216. modelNames: providerData.modelNames.filter(model => model !== modelToRemove),
  217. },
  218. };
  219. });
  220. };
  221. const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>, provider: string) => {
  222. if (e.key === 'Enter' || e.key === ' ') {
  223. e.preventDefault();
  224. const value = newModelInputs[provider] || '';
  225. addModel(provider, value);
  226. }
  227. };
  228. const getButtonProps = (provider: string) => {
  229. const isInStorage = providersFromStorage.has(provider);
  230. const isModified = modifiedProviders.has(provider);
  231. const isCustom = providers[provider]?.type === ProviderTypeEnum.CustomOpenAI;
  232. // For deletion, we only care if it's in storage and not modified
  233. if (isInStorage && !isModified) {
  234. return {
  235. theme: isDarkMode ? 'dark' : 'light',
  236. variant: 'danger' as const,
  237. children: 'Delete',
  238. disabled: false,
  239. };
  240. }
  241. // For saving, we need to check if it has the required inputs
  242. // Only custom providers can be saved without an API key
  243. const hasInput = isCustom || Boolean(providers[provider]?.apiKey?.trim());
  244. return {
  245. theme: isDarkMode ? 'dark' : 'light',
  246. variant: 'primary' as const,
  247. children: 'Save',
  248. disabled: !hasInput || !isModified,
  249. };
  250. };
  251. const handleSave = async (provider: string) => {
  252. try {
  253. // Check if name contains spaces for custom providers
  254. if (providers[provider].type === ProviderTypeEnum.CustomOpenAI && providers[provider].name?.includes(' ')) {
  255. setNameErrors(prev => ({
  256. ...prev,
  257. [provider]: 'Spaces are not allowed in provider names. Please use underscores or other characters instead.',
  258. }));
  259. return;
  260. }
  261. // Check if base URL is required but missing for custom_openai
  262. if (
  263. providers[provider].type === ProviderTypeEnum.CustomOpenAI &&
  264. (!providers[provider].baseUrl || !providers[provider].baseUrl.trim())
  265. ) {
  266. alert('Base URL is required for custom OpenAI providers');
  267. return;
  268. }
  269. // Check if API key is required but empty for built-in providers (except custom)
  270. const isCustom = providers[provider].type === ProviderTypeEnum.CustomOpenAI;
  271. if (!isCustom && (!providers[provider].apiKey || !providers[provider].apiKey.trim())) {
  272. alert('API key is required for this provider');
  273. return;
  274. }
  275. // Ensure modelNames is provided
  276. let modelNames = providers[provider].modelNames;
  277. if (!modelNames) {
  278. // Use default model names if not explicitly set
  279. modelNames = [...(llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [])];
  280. }
  281. // The provider store will handle filling in the missing fields
  282. await llmProviderStore.setProvider(provider, {
  283. apiKey: providers[provider].apiKey || '',
  284. baseUrl: providers[provider].baseUrl,
  285. name: providers[provider].name,
  286. modelNames: modelNames,
  287. type: providers[provider].type,
  288. createdAt: providers[provider].createdAt,
  289. });
  290. // Clear any name errors on successful save
  291. setNameErrors(prev => {
  292. const newErrors = { ...prev };
  293. delete newErrors[provider];
  294. return newErrors;
  295. });
  296. // Add to providersFromStorage since it's now saved
  297. setProvidersFromStorage(prev => new Set(prev).add(provider));
  298. setModifiedProviders(prev => {
  299. const next = new Set(prev);
  300. next.delete(provider);
  301. return next;
  302. });
  303. } catch (error) {
  304. console.error('Error saving API key:', error);
  305. }
  306. };
  307. const handleDelete = async (provider: string) => {
  308. try {
  309. // Delete the provider from storage regardless of its API key value
  310. await llmProviderStore.removeProvider(provider);
  311. // Remove from providersFromStorage
  312. setProvidersFromStorage(prev => {
  313. const next = new Set(prev);
  314. next.delete(provider);
  315. return next;
  316. });
  317. // Remove from providers state
  318. setProviders(prev => {
  319. const next = { ...prev };
  320. delete next[provider];
  321. return next;
  322. });
  323. // Also remove from modifiedProviders if it's there
  324. setModifiedProviders(prev => {
  325. const next = new Set(prev);
  326. next.delete(provider);
  327. return next;
  328. });
  329. } catch (error) {
  330. console.error('Error deleting provider:', error);
  331. }
  332. };
  333. const handleCancelProvider = (providerId: string) => {
  334. // Remove the provider from the state
  335. setProviders(prev => {
  336. const next = { ...prev };
  337. delete next[providerId];
  338. return next;
  339. });
  340. // Remove from modified providers
  341. setModifiedProviders(prev => {
  342. const next = new Set(prev);
  343. next.delete(providerId);
  344. return next;
  345. });
  346. };
  347. const getAvailableModels = () => {
  348. const models: Array<{ provider: string; providerName: string; model: string }> = [];
  349. // Only get models from configured providers
  350. for (const [provider, config] of Object.entries(providers)) {
  351. const isCustom = config.type === ProviderTypeEnum.CustomOpenAI;
  352. // Include provider if:
  353. // 1. It has an API key, or
  354. // 2. It's a custom provider that's already in storage
  355. if (config.apiKey || (isCustom && providersFromStorage.has(provider))) {
  356. const providerModels =
  357. config.modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [];
  358. models.push(
  359. ...providerModels.map(model => ({
  360. provider,
  361. providerName: config.name || provider,
  362. model,
  363. })),
  364. );
  365. }
  366. }
  367. return models;
  368. };
  369. const handleModelChange = async (agentName: AgentNameEnum, modelValue: string) => {
  370. // modelValue will be in format "provider>model"
  371. const [provider, model] = modelValue.split('>');
  372. // Set parameters based on provider type
  373. const newParameters = llmProviderParameters[provider as keyof typeof llmProviderParameters]?.[agentName] || {
  374. temperature: 0.1,
  375. topP: 0.1,
  376. };
  377. setModelParameters(prev => ({
  378. ...prev,
  379. [agentName]: newParameters,
  380. }));
  381. setSelectedModels(prev => ({
  382. ...prev,
  383. [agentName]: model,
  384. }));
  385. try {
  386. if (model) {
  387. await agentModelStore.setAgentModel(agentName, {
  388. provider,
  389. modelName: model,
  390. parameters: newParameters,
  391. });
  392. } else {
  393. // Reset storage if no model is selected
  394. await agentModelStore.resetAgentModel(agentName);
  395. }
  396. } catch (error) {
  397. console.error('Error saving agent model:', error);
  398. }
  399. };
  400. const handleParameterChange = async (agentName: AgentNameEnum, paramName: 'temperature' | 'topP', value: number) => {
  401. const newParameters = {
  402. ...modelParameters[agentName],
  403. [paramName]: value,
  404. };
  405. setModelParameters(prev => ({
  406. ...prev,
  407. [agentName]: newParameters,
  408. }));
  409. // Only update if we have a selected model
  410. if (selectedModels[agentName]) {
  411. try {
  412. // Find provider
  413. let provider: string | undefined;
  414. for (const [providerKey, providerConfig] of Object.entries(providers)) {
  415. const modelNames =
  416. providerConfig.modelNames || llmProviderModelNames[providerKey as keyof typeof llmProviderModelNames] || [];
  417. if (modelNames.includes(selectedModels[agentName])) {
  418. provider = providerKey;
  419. break;
  420. }
  421. }
  422. if (provider) {
  423. await agentModelStore.setAgentModel(agentName, {
  424. provider,
  425. modelName: selectedModels[agentName],
  426. parameters: newParameters,
  427. });
  428. }
  429. } catch (error) {
  430. console.error('Error saving agent parameters:', error);
  431. }
  432. }
  433. };
  434. const renderModelSelect = (agentName: AgentNameEnum) => (
  435. <div
  436. className={`rounded-lg border ${isDarkMode ? 'border-gray-700 bg-slate-800' : 'border-gray-200 bg-gray-50'} p-4`}>
  437. <h3 className={`mb-2 text-lg font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
  438. {agentName.charAt(0).toUpperCase() + agentName.slice(1)}
  439. </h3>
  440. <p className={`mb-4 text-sm font-normal ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
  441. {getAgentDescription(agentName)}
  442. </p>
  443. <div className="space-y-4">
  444. {/* Model Selection */}
  445. <div className="flex items-center">
  446. <label
  447. htmlFor={`${agentName}-model`}
  448. className={`w-24 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
  449. Model
  450. </label>
  451. <select
  452. id={`${agentName}-model`}
  453. className={`flex-1 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-3 py-2`}
  454. disabled={getAvailableModels().length <= 1}
  455. value={
  456. selectedModels[agentName]
  457. ? `${getProviderForModel(selectedModels[agentName])}>${selectedModels[agentName]}`
  458. : ''
  459. }
  460. onChange={e => handleModelChange(agentName, e.target.value)}>
  461. <option key="default" value="">
  462. Choose model
  463. </option>
  464. {getAvailableModels().map(({ provider, providerName, model }) => (
  465. <option key={`${provider}>${model}`} value={`${provider}>${model}`}>
  466. {`${providerName} > ${model}`}
  467. </option>
  468. ))}
  469. </select>
  470. </div>
  471. {/* Temperature Slider */}
  472. <div className="flex items-center">
  473. <label
  474. htmlFor={`${agentName}-temperature`}
  475. className={`w-24 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
  476. Temperature
  477. </label>
  478. <div className="flex flex-1 items-center space-x-2">
  479. <input
  480. id={`${agentName}-temperature`}
  481. type="range"
  482. min="0"
  483. max="2"
  484. step="0.01"
  485. value={modelParameters[agentName].temperature}
  486. onChange={e => handleParameterChange(agentName, 'temperature', Number.parseFloat(e.target.value))}
  487. style={{
  488. background: `linear-gradient(to right, ${isDarkMode ? '#3b82f6' : '#60a5fa'} 0%, ${isDarkMode ? '#3b82f6' : '#60a5fa'} ${(modelParameters[agentName].temperature / 2) * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} ${(modelParameters[agentName].temperature / 2) * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} 100%)`,
  489. }}
  490. className={`flex-1 ${isDarkMode ? 'accent-blue-500' : 'accent-blue-400'} appearance-none h-1 rounded-full`}
  491. />
  492. <div className="flex items-center space-x-2">
  493. <span className={`w-12 text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
  494. {modelParameters[agentName].temperature.toFixed(2)}
  495. </span>
  496. <input
  497. type="number"
  498. min="0"
  499. max="2"
  500. step="0.01"
  501. value={modelParameters[agentName].temperature}
  502. onChange={e => {
  503. const value = Number.parseFloat(e.target.value);
  504. if (!Number.isNaN(value) && value >= 0 && value <= 2) {
  505. handleParameterChange(agentName, 'temperature', value);
  506. }
  507. }}
  508. className={`w-20 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-2 py-1 text-sm`}
  509. aria-label={`${agentName} temperature number input`}
  510. />
  511. </div>
  512. </div>
  513. </div>
  514. {/* Top P Slider */}
  515. <div className="flex items-center">
  516. <label
  517. htmlFor={`${agentName}-topP`}
  518. className={`w-24 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
  519. Top P
  520. </label>
  521. <div className="flex flex-1 items-center space-x-2">
  522. <input
  523. id={`${agentName}-topP`}
  524. type="range"
  525. min="0"
  526. max="1"
  527. step="0.001"
  528. value={modelParameters[agentName].topP}
  529. onChange={e => handleParameterChange(agentName, 'topP', Number.parseFloat(e.target.value))}
  530. style={{
  531. background: `linear-gradient(to right, ${isDarkMode ? '#3b82f6' : '#60a5fa'} 0%, ${isDarkMode ? '#3b82f6' : '#60a5fa'} ${modelParameters[agentName].topP * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} ${modelParameters[agentName].topP * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} 100%)`,
  532. }}
  533. className={`flex-1 ${isDarkMode ? 'accent-blue-500' : 'accent-blue-400'} appearance-none h-1 rounded-full`}
  534. />
  535. <div className="flex items-center space-x-2">
  536. <span className={`w-12 text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
  537. {modelParameters[agentName].topP.toFixed(3)}
  538. </span>
  539. <input
  540. type="number"
  541. min="0"
  542. max="1"
  543. step="0.001"
  544. value={modelParameters[agentName].topP}
  545. onChange={e => {
  546. const value = Number.parseFloat(e.target.value);
  547. if (!Number.isNaN(value) && value >= 0 && value <= 1) {
  548. handleParameterChange(agentName, 'topP', value);
  549. }
  550. }}
  551. className={`w-20 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} px-2 py-1 text-sm`}
  552. aria-label={`${agentName} top P number input`}
  553. />
  554. </div>
  555. </div>
  556. </div>
  557. </div>
  558. </div>
  559. );
  560. const getAgentDescription = (agentName: AgentNameEnum) => {
  561. switch (agentName) {
  562. case AgentNameEnum.Navigator:
  563. return 'Navigates websites and performs actions';
  564. case AgentNameEnum.Planner:
  565. return 'Develops and refines strategies to complete tasks';
  566. case AgentNameEnum.Validator:
  567. return 'Checks if tasks are completed successfully';
  568. default:
  569. return '';
  570. }
  571. };
  572. const getMaxCustomProviderNumber = () => {
  573. let maxNumber = 0;
  574. for (const providerId of Object.keys(providers)) {
  575. if (providerId.startsWith('custom_openai_')) {
  576. const match = providerId.match(/custom_openai_(\d+)/);
  577. if (match) {
  578. const number = Number.parseInt(match[1], 10);
  579. maxNumber = Math.max(maxNumber, number);
  580. }
  581. }
  582. }
  583. return maxNumber;
  584. };
  585. const addCustomProvider = () => {
  586. const nextNumber = getMaxCustomProviderNumber() + 1;
  587. const providerId = `custom_openai_${nextNumber}`;
  588. setProviders(prev => ({
  589. ...prev,
  590. [providerId]: {
  591. apiKey: '',
  592. name: `CustomProvider${nextNumber}`,
  593. type: ProviderTypeEnum.CustomOpenAI,
  594. baseUrl: '',
  595. modelNames: [],
  596. createdAt: Date.now(),
  597. },
  598. }));
  599. setModifiedProviders(prev => new Set(prev).add(providerId));
  600. // Set the newly added provider ref
  601. newlyAddedProviderRef.current = providerId;
  602. // Scroll to the newly added provider after render
  603. setTimeout(() => {
  604. const providerElement = document.getElementById(`provider-${providerId}`);
  605. if (providerElement) {
  606. providerElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
  607. }
  608. }, 100);
  609. };
  610. const addDefaultProvider = (provider: string) => {
  611. // Get the default provider configuration
  612. let config: {
  613. apiKey: string;
  614. name: string;
  615. type: ProviderTypeEnum;
  616. modelNames: string[];
  617. baseUrl?: string;
  618. createdAt: number;
  619. };
  620. switch (provider) {
  621. case OPENAI_PROVIDER:
  622. config = {
  623. apiKey: '',
  624. name: 'OpenAI',
  625. type: ProviderTypeEnum.OpenAI,
  626. modelNames: [...(llmProviderModelNames[OPENAI_PROVIDER] || [])],
  627. createdAt: Date.now(),
  628. };
  629. break;
  630. case ANTHROPIC_PROVIDER:
  631. config = {
  632. apiKey: '',
  633. name: 'Anthropic',
  634. type: ProviderTypeEnum.Anthropic,
  635. modelNames: [...(llmProviderModelNames[ANTHROPIC_PROVIDER] || [])],
  636. createdAt: Date.now(),
  637. };
  638. break;
  639. case GEMINI_PROVIDER:
  640. config = {
  641. apiKey: '',
  642. name: 'Gemini',
  643. type: ProviderTypeEnum.Gemini,
  644. modelNames: [...(llmProviderModelNames[GEMINI_PROVIDER] || [])],
  645. createdAt: Date.now(),
  646. };
  647. break;
  648. case OLLAMA_PROVIDER:
  649. config = {
  650. apiKey: 'ollama', // Set default API key for Ollama
  651. name: 'Ollama',
  652. type: ProviderTypeEnum.Ollama,
  653. modelNames: [],
  654. baseUrl: 'http://localhost:11434',
  655. createdAt: Date.now(),
  656. };
  657. break;
  658. default:
  659. return;
  660. }
  661. // Add the provider to the state
  662. setProviders(prev => ({
  663. ...prev,
  664. [provider]: config,
  665. }));
  666. // Mark as modified so it shows up in the UI
  667. setModifiedProviders(prev => new Set(prev).add(provider));
  668. // Set the newly added provider ref
  669. newlyAddedProviderRef.current = provider;
  670. // Scroll to the newly added provider after render
  671. setTimeout(() => {
  672. const providerElement = document.getElementById(`provider-${provider}`);
  673. if (providerElement) {
  674. providerElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
  675. }
  676. }, 100);
  677. };
  678. // Sort providers to ensure newly added providers appear at the bottom
  679. const getSortedProviders = () => {
  680. // Filter providers to only include those from storage and newly added providers
  681. const filteredProviders = Object.entries(providers).filter(([providerId]) => {
  682. // Include if it's from storage
  683. if (providersFromStorage.has(providerId)) {
  684. return true;
  685. }
  686. // Include if it's a newly added provider (has been modified)
  687. if (modifiedProviders.has(providerId)) {
  688. return true;
  689. }
  690. // Exclude providers that aren't from storage and haven't been modified
  691. return false;
  692. });
  693. // Sort the filtered providers
  694. return filteredProviders.sort(([keyA, configA], [keyB, configB]) => {
  695. // First, separate newly added providers from stored providers
  696. const isNewA = !providersFromStorage.has(keyA) && modifiedProviders.has(keyA);
  697. const isNewB = !providersFromStorage.has(keyB) && modifiedProviders.has(keyB);
  698. // If one is new and one is stored, new ones go to the end
  699. if (isNewA && !isNewB) return 1;
  700. if (!isNewA && isNewB) return -1;
  701. // If both are new or both are stored, sort by createdAt
  702. if (configA.createdAt && configB.createdAt) {
  703. return configA.createdAt - configB.createdAt; // Sort in ascending order (oldest first)
  704. }
  705. // If only one has createdAt, put the one without createdAt at the end
  706. if (configA.createdAt) return -1;
  707. if (configB.createdAt) return 1;
  708. // If neither has createdAt, sort by type and then name
  709. const isCustomA = configA.type === ProviderTypeEnum.CustomOpenAI;
  710. const isCustomB = configB.type === ProviderTypeEnum.CustomOpenAI;
  711. if (isCustomA && !isCustomB) {
  712. return 1; // Custom providers come after non-custom
  713. }
  714. if (!isCustomA && isCustomB) {
  715. return -1; // Non-custom providers come before custom
  716. }
  717. // Sort alphabetically by name within each group
  718. return (configA.name || keyA).localeCompare(configB.name || keyB);
  719. });
  720. };
  721. const handleProviderSelection = (providerType: string) => {
  722. // Close the dropdown immediately
  723. setIsProviderSelectorOpen(false);
  724. if (providerType === 'custom') {
  725. addCustomProvider();
  726. return;
  727. }
  728. // Handle default providers
  729. switch (providerType) {
  730. case OPENAI_PROVIDER:
  731. case ANTHROPIC_PROVIDER:
  732. case GEMINI_PROVIDER:
  733. case OLLAMA_PROVIDER:
  734. addDefaultProvider(providerType);
  735. break;
  736. default:
  737. console.error('Unknown provider type:', providerType);
  738. }
  739. };
  740. const getProviderForModel = (modelName: string): string => {
  741. for (const [provider, config] of Object.entries(providers)) {
  742. const modelNames =
  743. config.modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [];
  744. if (modelNames.includes(modelName)) {
  745. return provider;
  746. }
  747. }
  748. return '';
  749. };
  750. return (
  751. <section className="space-y-6">
  752. {/* LLM Providers Section */}
  753. <div
  754. className={`rounded-lg border ${isDarkMode ? 'border-slate-700 bg-slate-800' : 'border-blue-100 bg-gray-50'} p-6 text-left shadow-sm`}>
  755. <h2 className={`mb-4 text-xl font-semibold ${isDarkMode ? 'text-gray-200' : 'text-gray-800'}`}>
  756. LLM Providers
  757. </h2>
  758. <div className="space-y-6">
  759. {getSortedProviders().length === 0 ? (
  760. <div className="py-8 text-center text-gray-500">
  761. <p className="mb-4">No providers configured yet. Add a provider to get started.</p>
  762. </div>
  763. ) : (
  764. getSortedProviders().map(([providerId, providerConfig]) => (
  765. <div
  766. key={providerId}
  767. id={`provider-${providerId}`}
  768. className={`space-y-4 ${modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) ? `rounded-lg border p-4 ${isDarkMode ? 'border-blue-700 bg-slate-700' : 'border-blue-200 bg-blue-50/70'}` : ''}`}>
  769. <div className="flex items-center justify-between">
  770. <h3 className={`text-lg font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
  771. {providerConfig.name || providerId}
  772. </h3>
  773. <div className="flex space-x-2">
  774. {/* Show Cancel button for newly added providers */}
  775. {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && (
  776. <Button variant="secondary" onClick={() => handleCancelProvider(providerId)}>
  777. Cancel
  778. </Button>
  779. )}
  780. <Button
  781. variant={getButtonProps(providerId).variant}
  782. disabled={getButtonProps(providerId).disabled}
  783. onClick={() =>
  784. providersFromStorage.has(providerId) && !modifiedProviders.has(providerId)
  785. ? handleDelete(providerId)
  786. : handleSave(providerId)
  787. }>
  788. {getButtonProps(providerId).children}
  789. </Button>
  790. </div>
  791. </div>
  792. {/* Show message for newly added providers */}
  793. {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && (
  794. <div className={`mb-2 text-sm ${isDarkMode ? 'text-teal-300' : 'text-teal-700'}`}>
  795. <p>This provider is newly added. Enter your API key and click Save to configure it.</p>
  796. </div>
  797. )}
  798. <div className="space-y-3">
  799. {/* Name input (only for custom_openai) - moved to top for prominence */}
  800. {providerConfig.type === ProviderTypeEnum.CustomOpenAI && (
  801. <div className="flex flex-col">
  802. <div className="flex items-center">
  803. <label
  804. htmlFor={`${providerId}-name`}
  805. className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
  806. Name
  807. </label>
  808. <input
  809. id={`${providerId}-name`}
  810. type="text"
  811. placeholder="Provider name"
  812. value={providerConfig.name || ''}
  813. onChange={e => {
  814. console.log('Name input changed:', e.target.value);
  815. handleNameChange(providerId, e.target.value);
  816. }}
  817. className={`flex-1 rounded-md border p-2 ${
  818. nameErrors[providerId]
  819. ? isDarkMode
  820. ? 'border-red-700 bg-slate-700 text-gray-200 focus:border-red-600 focus:ring-2 focus:ring-red-900'
  821. : 'border-red-300 bg-gray-50 focus:border-red-400 focus:ring-2 focus:ring-red-200'
  822. : isDarkMode
  823. ? 'border-blue-700 bg-slate-700 text-gray-200 focus:border-blue-600 focus:ring-2 focus:ring-blue-900'
  824. : 'border-blue-300 bg-gray-50 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'
  825. } outline-none`}
  826. />
  827. </div>
  828. {nameErrors[providerId] ? (
  829. <p className={`ml-20 mt-1 text-xs ${isDarkMode ? 'text-red-400' : 'text-red-500'}`}>
  830. {nameErrors[providerId]}
  831. </p>
  832. ) : (
  833. <p className={`ml-20 mt-1 text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
  834. Provider name (spaces are not allowed when saving)
  835. </p>
  836. )}
  837. </div>
  838. )}
  839. {/* API Key input with label */}
  840. <div className="flex items-center">
  841. <label
  842. htmlFor={`${providerId}-api-key`}
  843. className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
  844. Key{providerConfig.type !== ProviderTypeEnum.CustomOpenAI ? '*' : ''}
  845. </label>
  846. <input
  847. id={`${providerId}-api-key`}
  848. type="password"
  849. placeholder={
  850. providerConfig.type === ProviderTypeEnum.CustomOpenAI
  851. ? `${providerConfig.name || providerId} API key (optional)`
  852. : `${providerConfig.name || providerId} API key (required)`
  853. }
  854. value={providerConfig.apiKey || ''}
  855. onChange={e => handleApiKeyChange(providerId, e.target.value, providerConfig.baseUrl)}
  856. className={`flex-1 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`}
  857. />
  858. </div>
  859. {/* Base URL input (for custom_openai and ollama) */}
  860. {(providerConfig.type === ProviderTypeEnum.CustomOpenAI ||
  861. providerConfig.type === ProviderTypeEnum.Ollama) && (
  862. <div className="flex flex-col">
  863. <div className="flex items-center">
  864. <label
  865. htmlFor={`${providerId}-base-url`}
  866. className={`w-20 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
  867. Base URL
  868. {providerConfig.type === ProviderTypeEnum.CustomOpenAI ||
  869. providerConfig.type === ProviderTypeEnum.Ollama
  870. ? '*'
  871. : ''}
  872. </label>
  873. <input
  874. id={`${providerId}-base-url`}
  875. type="text"
  876. placeholder={
  877. providerConfig.type === ProviderTypeEnum.CustomOpenAI
  878. ? 'Required for custom OpenAI providers'
  879. : 'Ollama base URL'
  880. }
  881. value={providerConfig.baseUrl || ''}
  882. onChange={e => handleApiKeyChange(providerId, providerConfig.apiKey || '', e.target.value)}
  883. className={`flex-1 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`}
  884. />
  885. </div>
  886. </div>
  887. )}
  888. {/* Models input field with tags */}
  889. <div className="flex items-start">
  890. <label
  891. htmlFor={`${providerId}-models`}
  892. className={`w-20 pt-2 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
  893. Models
  894. </label>
  895. <div className="flex-1">
  896. <div
  897. className={`flex min-h-[42px] flex-wrap items-center gap-2 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} p-2`}>
  898. {/* Display existing models as tags */}
  899. {(() => {
  900. // Get models from provider config or default models
  901. const models =
  902. providerConfig.modelNames !== undefined
  903. ? providerConfig.modelNames
  904. : llmProviderModelNames[providerId as keyof typeof llmProviderModelNames] || [];
  905. return models.map(model => (
  906. <div
  907. key={model}
  908. className={`flex items-center rounded-full ${isDarkMode ? 'bg-blue-900 text-blue-100' : 'bg-blue-100 text-blue-800'} px-2 py-1 text-sm`}>
  909. <span>{model}</span>
  910. <button
  911. type="button"
  912. onClick={() => removeModel(providerId, model)}
  913. className={`ml-1 font-bold ${isDarkMode ? 'text-blue-300 hover:text-blue-100' : 'text-blue-600 hover:text-blue-800'}`}
  914. aria-label={`Remove ${model}`}>
  915. ×
  916. </button>
  917. </div>
  918. ));
  919. })()}
  920. {/* Input for new models */}
  921. <input
  922. id={`${providerId}-models`}
  923. type="text"
  924. placeholder=""
  925. value={newModelInputs[providerId] || ''}
  926. onChange={e => handleModelsChange(providerId, e.target.value)}
  927. onKeyDown={e => handleKeyDown(e, providerId)}
  928. className={`min-w-[150px] flex-1 border-none ${isDarkMode ? 'bg-transparent text-gray-200' : 'bg-transparent text-gray-700'} p-1 outline-none`}
  929. />
  930. </div>
  931. <p className={`mt-1 text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
  932. Type and Press Enter or Space to add a model
  933. </p>
  934. </div>
  935. </div>
  936. {/* Ollama reminder at the bottom of the section */}
  937. {providerConfig.type === ProviderTypeEnum.Ollama && (
  938. <div
  939. className={`mt-4 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700' : 'border-blue-100 bg-blue-50'} p-3`}>
  940. <p className={`text-sm ${isDarkMode ? 'text-gray-200' : 'text-gray-700'}`}>
  941. <strong>Remember:</strong> Add{' '}
  942. <code
  943. className={`rounded italic ${isDarkMode ? 'bg-slate-600 px-1 py-0.5' : 'bg-blue-100 px-1 py-0.5'}`}>
  944. OLLAMA_ORIGINS=chrome-extension://*
  945. </code>{' '}
  946. environment variable for the Ollama server.
  947. <a
  948. href="https://github.com/ollama/ollama/issues/6489"
  949. target="_blank"
  950. rel="noopener noreferrer"
  951. className={`ml-1 ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-800'}`}>
  952. Learn more
  953. </a>
  954. </p>
  955. </div>
  956. )}
  957. </div>
  958. {/* Add divider except for the last item */}
  959. {Object.keys(providers).indexOf(providerId) < Object.keys(providers).length - 1 && (
  960. <div className={`mt-4 border-t ${isDarkMode ? 'border-gray-700' : 'border-gray-200'}`} />
  961. )}
  962. </div>
  963. ))
  964. )}
  965. {/* Add Provider button and dropdown */}
  966. <div className="provider-selector-container relative pt-4">
  967. <Button
  968. variant="secondary"
  969. onClick={() => setIsProviderSelectorOpen(prev => !prev)}
  970. className={`flex w-full items-center justify-center font-medium ${
  971. isDarkMode
  972. ? 'bg-blue-600 hover:bg-blue-500 border-blue-700 text-white'
  973. : 'bg-blue-100 hover:bg-blue-200 border-blue-200 text-blue-800'
  974. }`}>
  975. <span className="mr-2">+</span> Add Provider
  976. </Button>
  977. {isProviderSelectorOpen && (
  978. <div
  979. className={`absolute z-10 mt-2 w-full overflow-hidden rounded-md border ${
  980. isDarkMode
  981. ? 'border-blue-600 bg-slate-700 shadow-lg shadow-slate-900/50'
  982. : 'border-blue-200 bg-white shadow-xl shadow-blue-100/50'
  983. }`}>
  984. <div className="py-1">
  985. {/* Check if all default providers are already added */}
  986. {(providersFromStorage.has(OPENAI_PROVIDER) || modifiedProviders.has(OPENAI_PROVIDER)) &&
  987. (providersFromStorage.has(ANTHROPIC_PROVIDER) || modifiedProviders.has(ANTHROPIC_PROVIDER)) &&
  988. (providersFromStorage.has(GEMINI_PROVIDER) || modifiedProviders.has(GEMINI_PROVIDER)) &&
  989. (providersFromStorage.has(OLLAMA_PROVIDER) || modifiedProviders.has(OLLAMA_PROVIDER)) && (
  990. <div
  991. className={`px-4 py-3 text-sm font-medium ${isDarkMode ? 'text-blue-300' : 'text-blue-600'}`}>
  992. All default providers already added. You can still add a custom provider.
  993. </div>
  994. )}
  995. {!providersFromStorage.has(OPENAI_PROVIDER) && !modifiedProviders.has(OPENAI_PROVIDER) && (
  996. <button
  997. type="button"
  998. className={`flex w-full items-center px-4 py-3 text-left text-sm ${
  999. isDarkMode
  1000. ? 'text-blue-200 hover:bg-blue-600/30 hover:text-white'
  1001. : 'text-blue-700 hover:bg-blue-100 hover:text-blue-800'
  1002. } transition-colors duration-150`}
  1003. onClick={() => handleProviderSelection(OPENAI_PROVIDER)}>
  1004. <span className="font-medium">OpenAI</span>
  1005. </button>
  1006. )}
  1007. {!providersFromStorage.has(ANTHROPIC_PROVIDER) && !modifiedProviders.has(ANTHROPIC_PROVIDER) && (
  1008. <button
  1009. type="button"
  1010. className={`flex w-full items-center px-4 py-3 text-left text-sm ${
  1011. isDarkMode
  1012. ? 'text-blue-200 hover:bg-blue-600/30 hover:text-white'
  1013. : 'text-blue-700 hover:bg-blue-100 hover:text-blue-800'
  1014. } transition-colors duration-150`}
  1015. onClick={() => handleProviderSelection(ANTHROPIC_PROVIDER)}>
  1016. <span className="font-medium">Anthropic</span>
  1017. </button>
  1018. )}
  1019. {!providersFromStorage.has(GEMINI_PROVIDER) && !modifiedProviders.has(GEMINI_PROVIDER) && (
  1020. <button
  1021. type="button"
  1022. className={`flex w-full items-center px-4 py-3 text-left text-sm ${
  1023. isDarkMode
  1024. ? 'text-blue-200 hover:bg-blue-600/30 hover:text-white'
  1025. : 'text-blue-700 hover:bg-blue-100 hover:text-blue-800'
  1026. } transition-colors duration-150`}
  1027. onClick={() => handleProviderSelection(GEMINI_PROVIDER)}>
  1028. <span className="font-medium">Gemini</span>
  1029. </button>
  1030. )}
  1031. {!providersFromStorage.has(OLLAMA_PROVIDER) && !modifiedProviders.has(OLLAMA_PROVIDER) && (
  1032. <button
  1033. type="button"
  1034. className={`flex w-full items-center px-4 py-3 text-left text-sm ${
  1035. isDarkMode
  1036. ? 'text-blue-200 hover:bg-blue-600/30 hover:text-white'
  1037. : 'text-blue-700 hover:bg-blue-100 hover:text-blue-800'
  1038. } transition-colors duration-150`}
  1039. onClick={() => handleProviderSelection(OLLAMA_PROVIDER)}>
  1040. <span className="font-medium">Ollama</span>
  1041. </button>
  1042. )}
  1043. <button
  1044. type="button"
  1045. className={`flex w-full items-center px-4 py-3 text-left text-sm ${
  1046. isDarkMode
  1047. ? 'text-blue-200 hover:bg-blue-600/30 hover:text-white'
  1048. : 'text-blue-700 hover:bg-blue-100 hover:text-blue-800'
  1049. } transition-colors duration-150`}
  1050. onClick={() => handleProviderSelection('custom')}>
  1051. <span className="font-medium">Custom OpenAI-compatible</span>
  1052. </button>
  1053. </div>
  1054. </div>
  1055. )}
  1056. </div>
  1057. </div>
  1058. </div>
  1059. {/* Updated Agent Models Section */}
  1060. <div
  1061. className={`rounded-lg border ${isDarkMode ? 'border-slate-700 bg-slate-800' : 'border-blue-100 bg-gray-50'} p-6 text-left shadow-sm`}>
  1062. <h2 className={`mb-4 text-left text-xl font-semibold ${isDarkMode ? 'text-gray-200' : 'text-gray-800'}`}>
  1063. Model Selection
  1064. </h2>
  1065. <div className="space-y-4">
  1066. {[AgentNameEnum.Planner, AgentNameEnum.Navigator, AgentNameEnum.Validator].map(agentName => (
  1067. <div key={agentName}>{renderModelSelect(agentName)}</div>
  1068. ))}
  1069. </div>
  1070. </div>
  1071. </section>
  1072. );
  1073. };