mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-22 07:03:33 +00:00
Update Dahsboard UI (#1349)
Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com> Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com>
This commit is contained in:
parent
ddada3e642
commit
38c7717f3d
41 changed files with 2405 additions and 2105 deletions
|
|
@ -36,7 +36,7 @@ import {
|
|||
} from '@/hooks/useIntegrationManagement';
|
||||
import { getProxyBaseURL } from '@/lib';
|
||||
import { OAuth } from '@/lib/oauth';
|
||||
import { MCPEnvDialog } from '@/pages/Setting/components/MCPEnvDialog';
|
||||
import { MCPEnvDialog } from '@/pages/Connectors/components/MCPEnvDialog';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -53,6 +53,7 @@ interface IntegrationListProps {
|
|||
// Manage mode props (Setting)
|
||||
showSelect?: boolean;
|
||||
selectPlaceholder?: string;
|
||||
selectValue?: string;
|
||||
selectContent?: React.ReactNode;
|
||||
onSelectChange?: (value: string, item: IntegrationItem) => void;
|
||||
showConfigButton?: boolean;
|
||||
|
|
@ -73,6 +74,7 @@ export default function IntegrationList({
|
|||
onShowEnvConfig,
|
||||
showSelect = false,
|
||||
selectPlaceholder = 'Select...',
|
||||
selectValue,
|
||||
selectContent,
|
||||
onSelectChange,
|
||||
showConfigButton = true,
|
||||
|
|
@ -293,7 +295,7 @@ export default function IntegrationList({
|
|||
|
||||
const itemClassName = isSelectMode
|
||||
? 'cursor-pointer hover:bg-surface-hover-subtle px-3 py-2 flex justify-between'
|
||||
: 'w-full px-6 py-4 bg-surface-secondary rounded-2xl';
|
||||
: 'w-full px-6 py-4 bg-surface-tertiary rounded-2xl';
|
||||
|
||||
const titleClassName = isSelectMode
|
||||
? 'text-base leading-snug font-bold text-text-action'
|
||||
|
|
@ -338,7 +340,7 @@ export default function IntegrationList({
|
|||
}
|
||||
>
|
||||
{isSelectMode ? (
|
||||
<div className="flex items-center gap-xs">
|
||||
<div className="gap-xs flex items-center">
|
||||
{(isSelectMode || showStatusDot) && (
|
||||
<img
|
||||
src={ellipseIcon}
|
||||
|
|
@ -359,8 +361,8 @@ export default function IntegrationList({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-row items-center justify-between gap-xs">
|
||||
<div className="flex flex-row items-center gap-xs">
|
||||
<div className="gap-xs flex w-full flex-row items-center justify-between">
|
||||
<div className="gap-xs flex flex-row items-center">
|
||||
{showStatusDot && (
|
||||
<img
|
||||
src={ellipseIcon}
|
||||
|
|
@ -385,7 +387,7 @@ export default function IntegrationList({
|
|||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-md">
|
||||
<div className="gap-md flex flex-row items-center">
|
||||
{showConfigButton && (
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -453,13 +455,16 @@ export default function IntegrationList({
|
|||
</div>
|
||||
|
||||
{!isSelectMode && showSelect && (
|
||||
<div className="mt-6 flex w-full flex-row items-center gap-md border-x-0 border-b-0 border-solid border-border-secondary pt-6">
|
||||
<div className="flex w-full flex-row items-center justify-between gap-md">
|
||||
<div className="mt-6 gap-md border-border-secondary pt-6 flex w-full flex-row items-center border-x-0 border-b-0 border-solid">
|
||||
<div className="gap-md flex w-full flex-row items-center justify-between">
|
||||
<div className="text-body-md text-text-body">
|
||||
{' '}
|
||||
Default {item.name}
|
||||
</div>
|
||||
<Select onValueChange={(v) => onSelectChange?.(v, item)}>
|
||||
<Select
|
||||
{...(selectValue !== undefined && { value: selectValue })}
|
||||
onValueChange={(v) => onSelectChange?.(v, item)}
|
||||
>
|
||||
<SelectTrigger size="default" className="w-[240px]">
|
||||
<SelectValue placeholder={selectPlaceholder} />
|
||||
</SelectTrigger>
|
||||
|
|
|
|||
|
|
@ -63,11 +63,12 @@ export default function SearchInput({
|
|||
}, [onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userExpanded && inputRef.current) {
|
||||
const id = requestAnimationFrame(() => {
|
||||
if (userExpanded) {
|
||||
// Delay focus until input is mounted (AnimatePresence mode="wait" ~150ms)
|
||||
const id = setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, 150);
|
||||
return () => clearTimeout(id);
|
||||
}
|
||||
}, [userExpanded]);
|
||||
|
||||
|
|
@ -79,9 +80,9 @@ export default function SearchInput({
|
|||
return (
|
||||
<motion.div
|
||||
className={cn(
|
||||
'flex items-center justify-center overflow-hidden rounded-lg border border-solid border-transparent bg-transparent py-0.5',
|
||||
'rounded-lg py-0.5 flex items-center justify-center overflow-hidden border border-solid border-transparent bg-transparent',
|
||||
'focus-within:border-input-border-focus focus-within:bg-input-bg-input',
|
||||
'hover:border-transparent hover:bg-surface-tertiary'
|
||||
'hover:bg-surface-tertiary hover:border-transparent'
|
||||
)}
|
||||
initial={false}
|
||||
animate={{ width: isExpanded ? EXPANDED_WIDTH : COLLAPSED_WIDTH }}
|
||||
|
|
@ -116,13 +117,13 @@ export default function SearchInput({
|
|||
) : (
|
||||
<motion.div
|
||||
key="input"
|
||||
className="flex min-w-0 flex-1 items-center gap-0 pr-1"
|
||||
className="min-w-0 gap-0 pr-1 flex flex-1 items-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<span className="pointer-events-none ml-2 inline-flex h-4 w-4 shrink-0 items-center justify-center text-icon-secondary">
|
||||
<span className="ml-2 h-4 w-4 text-icon-secondary pointer-events-none inline-flex shrink-0 items-center justify-center">
|
||||
<Search className="h-4 w-4" />
|
||||
</span>
|
||||
<input
|
||||
|
|
@ -139,14 +140,14 @@ export default function SearchInput({
|
|||
onSearch?.();
|
||||
}
|
||||
}}
|
||||
className="h-6 min-w-0 flex-1 bg-transparent pl-2 text-label-sm text-text-heading outline-none placeholder:text-text-label"
|
||||
className="h-6 min-w-0 pl-2 text-label-sm text-text-heading placeholder:text-text-label flex-1 bg-transparent outline-none"
|
||||
/>
|
||||
<TooltipSimple content={clearLabel}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 rounded-full text-icon-secondary"
|
||||
className="text-icon-secondary shrink-0 rounded-full"
|
||||
onClick={collapse}
|
||||
aria-label={clearLabel}
|
||||
>
|
||||
|
|
|
|||
127
src/components/animate-ui/icons/radio.tsx
Normal file
127
src/components/animate-ui/icons/radio.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
'use client';
|
||||
|
||||
import { motion, type Variants } from 'motion/react';
|
||||
|
||||
import {
|
||||
getVariants,
|
||||
IconWrapper,
|
||||
useAnimateIconContext,
|
||||
type IconProps,
|
||||
} from '@/components/animate-ui/icons/icon';
|
||||
|
||||
type RadioProps = IconProps<keyof typeof animations>;
|
||||
|
||||
const animations = {
|
||||
default: (() => {
|
||||
const animation: Record<string, Variants> = {};
|
||||
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
animation[`path${i}`] = {
|
||||
initial: { opacity: 1, scale: 1 },
|
||||
animate: {
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
transition: {
|
||||
opacity: {
|
||||
duration: 0.2,
|
||||
ease: 'easeInOut',
|
||||
repeat: 1,
|
||||
repeatType: 'reverse',
|
||||
repeatDelay: 0.2,
|
||||
delay: 0.2 * (i - 1),
|
||||
},
|
||||
scale: {
|
||||
duration: 0.2,
|
||||
ease: 'easeInOut',
|
||||
repeat: 1,
|
||||
repeatType: 'reverse',
|
||||
repeatDelay: 0.2,
|
||||
delay: 0.2 * (i - 1),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return animation;
|
||||
})() satisfies Record<string, Variants>,
|
||||
} as const;
|
||||
|
||||
function IconComponent({ size, ...props }: RadioProps) {
|
||||
const { controls } = useAnimateIconContext();
|
||||
const variants = getVariants(animations);
|
||||
|
||||
return (
|
||||
<motion.svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<motion.path
|
||||
d="M16.247 7.761a6 6 0 0 1 0 8.478"
|
||||
variants={variants.path1}
|
||||
initial="initial"
|
||||
animate={controls}
|
||||
/>
|
||||
<motion.path
|
||||
d="M19.075 4.933a10 10 0 0 1 0 14.134"
|
||||
variants={variants.path2}
|
||||
initial="initial"
|
||||
animate={controls}
|
||||
/>
|
||||
<motion.path
|
||||
d="M7.753 16.239a6 6 0 0 1 0-8.478"
|
||||
variants={variants.path1}
|
||||
initial="initial"
|
||||
animate={controls}
|
||||
/>
|
||||
<motion.path
|
||||
d="M4.925 19.067a10 10 0 0 1 0-14.134"
|
||||
variants={variants.path2}
|
||||
initial="initial"
|
||||
animate={controls}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="2"
|
||||
variants={variants.circle}
|
||||
initial="initial"
|
||||
animate={controls}
|
||||
/>
|
||||
</motion.svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Radio(props: RadioProps) {
|
||||
return <IconWrapper icon={IconComponent} {...props} />;
|
||||
}
|
||||
|
||||
export {
|
||||
animations,
|
||||
Radio,
|
||||
Radio as RadioIcon,
|
||||
type RadioProps as RadioIconProps,
|
||||
type RadioProps,
|
||||
};
|
||||
|
|
@ -30,8 +30,19 @@
|
|||
"continue-with-github-login": "الدخول باستخدام جيت هاب",
|
||||
"installation-failed": "فشل التثبيت",
|
||||
"projects": "المشاريع",
|
||||
"agents": "الوكلاء",
|
||||
"mcp-tools": "MCP والأدوات",
|
||||
"connectors": "الموصلات",
|
||||
"connectors-description": "إدارة محركات البحث وأدوات MCP وتكاملات المتصفح.",
|
||||
"channels": "القنوات",
|
||||
"channels-overview": "نظرة عامة",
|
||||
"channels-overview-coming-soon-description": "نظرة عامة على القنوات قيد التطوير.",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "Lark",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "تكامل هذه القناة قيد التطوير.",
|
||||
"browser": "المتصفح",
|
||||
"settings": "الإعدادات",
|
||||
"general": "عام",
|
||||
|
|
@ -137,7 +148,13 @@
|
|||
"projects-hub": "مركز المشاريع",
|
||||
"browser-management": "إدارة المتصفح",
|
||||
"browser-management-description": "يتم تخزين ملفات تعريف الارتباط للمتصفح محلياً على جهازك، وتُستخدم فقط للمهام على نفس المواقع، ويمكن مسحها أو تعطيلها في أي وقت.",
|
||||
"browser-connections": "الاتصالات",
|
||||
"browser-connections-description": "تكوين بروتوكول أدوات مطوري Chrome لأتمتة المتصفح.",
|
||||
"browser-plugins": "المكوّنات الإضافية",
|
||||
"browser-plugins-description": "إدارة إضافات المتصفح لتحسين الوظائف.",
|
||||
"restart-to-apply": "إعادة التشغيل للتطبيق",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "إدارة ملفات تعريف الارتباط",
|
||||
"browser-cookies": "ملفات تعريف الارتباط للمتصفح",
|
||||
"browser-cookies-description": "استخدم \"فتح المتصفح\" لتسجيل الدخول إلى صفحة ويب وحفظ ملفات تعريف الارتباط الخاصة بها. بعد إعادة تشغيل التطبيق، سيتذكرها Eigent ويساعدك في إكمال المهام المستقبلية على تلك المواقع.",
|
||||
"cookie-domains": "نطاقات ملفات تعريف الارتباط",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,19 @@
|
|||
"continue-with-github-login": "Mit Github anmelden",
|
||||
"installation-failed": "Installation fehlgeschlagen",
|
||||
"projects": "Projekte",
|
||||
"agents": "Agenten",
|
||||
"mcp-tools": "MCP & Tools",
|
||||
"connectors": "Konnektoren",
|
||||
"connectors-description": "Suchmaschinen, MCP-Tools und Browser-Integrationen verwalten.",
|
||||
"channels": "Kanäle",
|
||||
"channels-overview": "Übersicht",
|
||||
"channels-overview-coming-soon-description": "Die Kanäle-Übersicht wird entwickelt.",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "Lark",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "Die Integration für diesen Kanal wird entwickelt.",
|
||||
"browser": "Browser",
|
||||
"settings": "Einstellungen",
|
||||
"general": "Allgemein",
|
||||
|
|
@ -137,7 +148,13 @@
|
|||
"projects-hub": "Projekte-Hub",
|
||||
"browser-management": "Browser-Verwaltung",
|
||||
"browser-management-description": "Die Browser-Cookies werden lokal auf Ihrem Gerät gespeichert, werden nur für Aufgaben auf denselben Websites verwendet und können jederzeit gelöscht oder deaktiviert werden.",
|
||||
"browser-connections": "Verbindungen",
|
||||
"browser-connections-description": "Chrome DevTools Protocol-Konfiguration für Browser-Automatisierung.",
|
||||
"browser-plugins": "Plugins",
|
||||
"browser-plugins-description": "Browser-Erweiterungen für erweiterte Funktionalität verwalten.",
|
||||
"restart-to-apply": "Neu starten, um anzuwenden",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "Cookie-Verwaltung",
|
||||
"browser-cookies": "Browser-Cookies",
|
||||
"browser-cookies-description": "Verwenden Sie \"Browser öffnen\" um sich bei einer Webseite anzumelden und deren Cookies zu speichern. Nach dem Neustart der Desktop-App wird sich Eigent daran erinnern und Ihnen helfen, zukünftige Aufgaben auf diesen Websites zu erledigen.",
|
||||
"cookie-domains": "Cookie-Domains",
|
||||
|
|
|
|||
|
|
@ -31,8 +31,19 @@
|
|||
"installation-failed": "Installation Failed",
|
||||
"backend-startup-failed": "Backend Startup Failed",
|
||||
"projects": "Projects",
|
||||
"agents": "Agents",
|
||||
"mcp-tools": "MCP & Tools",
|
||||
"connectors": "Connectors",
|
||||
"connectors-description": "Manage search engines, MCP tools, and browser integrations.",
|
||||
"channels": "Channels",
|
||||
"channels-overview": "Overview",
|
||||
"channels-overview-coming-soon-description": "Channels overview is under development.",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "Lark",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "Integration for this channel is under development.",
|
||||
"browser": "Browser",
|
||||
"settings": "Settings",
|
||||
"general": "General",
|
||||
|
|
@ -140,7 +151,13 @@
|
|||
"projects-hub": "Projects Hub",
|
||||
"browser-management": "Browser Management",
|
||||
"browser-management-description": "The browser cookies are stored locally on your device, used only for tasks on those same sites, and can be cleared or disabled at any time.",
|
||||
"browser-connections": "Connections",
|
||||
"browser-connections-description": "Chrome DevTools Protocol configuration for browser automation.",
|
||||
"browser-plugins": "Plugins",
|
||||
"browser-plugins-description": "Manage browser extensions for enhanced functionality.",
|
||||
"restart-to-apply": "Restart to Apply",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "Cookie Management",
|
||||
"browser-cookies": "Browser Cookies",
|
||||
"browser-cookies-description": "Use 「Open Browser」to sign in to a webpage and save its cookies. After you restart the desktop app, Eigent will remember them and help you complete future tasks on those sites.",
|
||||
"cookie-domains": "Cookie Domains",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,19 @@
|
|||
"continue-with-github-login": "Continuar con Github",
|
||||
"installation-failed": "Error al instalar",
|
||||
"projects": "Proyectos",
|
||||
"agents": "Agentes",
|
||||
"mcp-tools": "MCP & Herramientas",
|
||||
"connectors": "Conectores",
|
||||
"connectors-description": "Gestionar motores de búsqueda, herramientas MCP e integraciones del navegador.",
|
||||
"channels": "Canales",
|
||||
"channels-overview": "Resumen",
|
||||
"channels-overview-coming-soon-description": "El resumen de canales está en desarrollo.",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "Lark",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "La integración de este canal está en desarrollo.",
|
||||
"browser": "Navegador",
|
||||
"settings": "Ajustes",
|
||||
"general": "General",
|
||||
|
|
@ -137,7 +148,13 @@
|
|||
"projects-hub": "Centro de Proyectos",
|
||||
"browser-management": "Gestión del Navegador",
|
||||
"browser-management-description": "Las cookies del navegador se almacenan localmente en su dispositivo, se usan solo para tareas en esos mismos sitios y se pueden borrar o desactivar en cualquier momento.",
|
||||
"browser-connections": "Conexiones",
|
||||
"browser-connections-description": "Configuración del protocolo Chrome DevTools para automatización del navegador.",
|
||||
"browser-plugins": "Complementos",
|
||||
"browser-plugins-description": "Gestionar extensiones del navegador para mejorar la funcionalidad.",
|
||||
"restart-to-apply": "Reiniciar para Aplicar",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "Gestión de Cookies",
|
||||
"browser-cookies": "Cookies del Navegador",
|
||||
"browser-cookies-description": "Use \"Abrir Navegador\" para iniciar sesión en una página web y guardar sus cookies. Después de reiniciar la aplicación, Eigent las recordará y le ayudará a completar tareas futuras en esos sitios.",
|
||||
"cookie-domains": "Dominios de Cookies",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,19 @@
|
|||
"continue-with-github-login": "Continuer avec Github",
|
||||
"installation-failed": "Échec de l'installation",
|
||||
"projects": "Projets",
|
||||
"agents": "Agents",
|
||||
"mcp-tools": "MCP & Outils",
|
||||
"connectors": "Connecteurs",
|
||||
"connectors-description": "Gérer les moteurs de recherche, les outils MCP et les intégrations du navigateur.",
|
||||
"channels": "Canaux",
|
||||
"channels-overview": "Aperçu",
|
||||
"channels-overview-coming-soon-description": "L'aperçu des canaux est en cours de développement.",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "Lark",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "L'intégration de ce canal est en cours de développement.",
|
||||
"browser": "Navigateur",
|
||||
"settings": "Paramètres",
|
||||
"general": "Général",
|
||||
|
|
@ -137,7 +148,13 @@
|
|||
"projects-hub": "Hub des Projets",
|
||||
"browser-management": "Gestion du Navigateur",
|
||||
"browser-management-description": "Les cookies du navigateur sont stockés localement sur votre appareil, utilisés uniquement pour les tâches sur ces mêmes sites, et peuvent être effacés ou désactivés à tout moment.",
|
||||
"browser-connections": "Connexions",
|
||||
"browser-connections-description": "Configuration du protocole Chrome DevTools pour l'automatisation du navigateur.",
|
||||
"browser-plugins": "Plugins",
|
||||
"browser-plugins-description": "Gérer les extensions du navigateur pour des fonctionnalités améliorées.",
|
||||
"restart-to-apply": "Redémarrer pour Appliquer",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "Gestion des Cookies",
|
||||
"browser-cookies": "Cookies du Navigateur",
|
||||
"browser-cookies-description": "Utilisez \"Ouvrir le Navigateur\" pour vous connecter à une page web et enregistrer ses cookies. Après avoir redémarré l'application, Eigent s'en souviendra et vous aidera à compléter les tâches futures sur ces sites.",
|
||||
"cookie-domains": "Domaines de Cookies",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,19 @@
|
|||
"continue-with-github-login": "Continua con Github",
|
||||
"installation-failed": "Installazione fallita",
|
||||
"projects": "Progetti",
|
||||
"agents": "Agenti",
|
||||
"mcp-tools": "MCP e Strumenti",
|
||||
"connectors": "Connettori",
|
||||
"connectors-description": "Gestisci motori di ricerca, strumenti MCP e integrazioni del browser.",
|
||||
"channels": "Canali",
|
||||
"channels-overview": "Panoramica",
|
||||
"channels-overview-coming-soon-description": "La panoramica dei canali è in fase di sviluppo.",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "Lark",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "L'integrazione per questo canale è in fase di sviluppo.",
|
||||
"browser": "Navigatore",
|
||||
"settings": "Impostazioni",
|
||||
"general": "Generale",
|
||||
|
|
@ -137,7 +148,13 @@
|
|||
"projects-hub": "Hub Progetti",
|
||||
"browser-management": "Gestione Browser",
|
||||
"browser-management-description": "I cookie del browser sono memorizzati localmente sul tuo dispositivo, utilizzati solo per le attività su quegli stessi siti e possono essere cancellati o disabilitati in qualsiasi momento.",
|
||||
"browser-connections": "Connessioni",
|
||||
"browser-connections-description": "Configurazione del protocollo Chrome DevTools per l'automazione del browser.",
|
||||
"browser-plugins": "Plugin",
|
||||
"browser-plugins-description": "Gestisci le estensioni del browser per funzionalità avanzate.",
|
||||
"restart-to-apply": "Riavvia per Applicare",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "Gestione Cookie",
|
||||
"browser-cookies": "Cookie del Browser",
|
||||
"browser-cookies-description": "Usa \"Apri Browser\" per accedere a una pagina web e salvare i suoi cookie. Dopo aver riavviato l'applicazione, Eigent li ricorderà e ti aiuterà a completare le attività future su quei siti.",
|
||||
"cookie-domains": "Domini dei Cookie",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,19 @@
|
|||
"continue-with-github-login": "Githubでログイン",
|
||||
"installation-failed": "インストールに失敗しました",
|
||||
"projects": "プロジェクト",
|
||||
"agents": "エージェント",
|
||||
"mcp-tools": "MCP & ツール",
|
||||
"connectors": "コネクタ",
|
||||
"connectors-description": "検索エンジン、MCPツール、ブラウザ連携を管理します。",
|
||||
"channels": "チャンネル",
|
||||
"channels-overview": "概要",
|
||||
"channels-overview-coming-soon-description": "チャンネル概要は開発中です。",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "Lark",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "このチャンネルの統合は開発中です。",
|
||||
"browser": "ブラウザ",
|
||||
"settings": "設定",
|
||||
"general": "一般",
|
||||
|
|
@ -137,7 +148,13 @@
|
|||
"projects-hub": "プロジェクトハブ",
|
||||
"browser-management": "ブラウザ管理",
|
||||
"browser-management-description": "ブラウザのCookieはデバイスにローカルに保存され、同じサイトのタスクでのみ使用され、いつでもクリアまたは無効にできます。",
|
||||
"browser-connections": "接続",
|
||||
"browser-connections-description": "ブラウザ自動化のためのChrome DevTools Protocol設定。",
|
||||
"browser-plugins": "プラグイン",
|
||||
"browser-plugins-description": "機能強化のためのブラウザ拡張機能を管理。",
|
||||
"restart-to-apply": "適用するために再起動",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "Cookie管理",
|
||||
"browser-cookies": "ブラウザCookie",
|
||||
"browser-cookies-description": "「ブラウザを開く」を使用して、ウェブページにログインし、そのCookieを保存します。再起動すると、Eigentはそれらを記憶し、それらのサイトで将来のタスクを完了するのに役立ちます。",
|
||||
"cookie-domains": "Cookieドメイン",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,19 @@
|
|||
"continue-with-github-login": "Github로 로그인",
|
||||
"installation-failed": "설치 실패",
|
||||
"projects": "프로젝트",
|
||||
"agents": "에이전트",
|
||||
"mcp-tools": "MCP 및 도구",
|
||||
"connectors": "커넥터",
|
||||
"connectors-description": "검색 엔진, MCP 도구 및 브라우저 통합을 관리합니다.",
|
||||
"channels": "채널",
|
||||
"channels-overview": "개요",
|
||||
"channels-overview-coming-soon-description": "채널 개요는 개발 중입니다.",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "Lark",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "이 채널 통합은 개발 중입니다.",
|
||||
"browser": "브라우저",
|
||||
"settings": "설정",
|
||||
"general": "일반",
|
||||
|
|
@ -137,7 +148,13 @@
|
|||
"projects-hub": "프로젝트 허브",
|
||||
"browser-management": "브라우저 관리",
|
||||
"browser-management-description": "브라우저 쿠키는 기기에 로컬로 저장되며, 동일한 사이트의 작업에만 사용되며 언제든지 지우거나 비활성화할 수 있습니다.",
|
||||
"browser-connections": "연결",
|
||||
"browser-connections-description": "브라우저 자동화를 위한 Chrome DevTools Protocol 설정.",
|
||||
"browser-plugins": "플러그인",
|
||||
"browser-plugins-description": "향상된 기능을 위한 브라우저 확장 프로그램 관리.",
|
||||
"restart-to-apply": "적용하려면 다시 시작",
|
||||
"browser-cookie": "쿠키",
|
||||
"browser-cookie-management": "Cookie 관리",
|
||||
"browser-cookies": "브라우저 쿠키",
|
||||
"browser-cookies-description": "「브라우저 열기」를 사용하여 웹페이지에 로그인하고 쿠키를 저장하세요. 다시 시작한 후 Eigent는 이를 기억하고 해당 사이트에서 향후 작업을 완료하는 데 도움을 줍니다.",
|
||||
"cookie-domains": "쿠키 도메인",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,19 @@
|
|||
"continue-with-github-login": "Продолжить с Github",
|
||||
"installation-failed": "Установка не удалась",
|
||||
"projects": "Проекты",
|
||||
"agents": "Агенты",
|
||||
"mcp-tools": "MCP и Инструменты",
|
||||
"connectors": "Коннекторы",
|
||||
"connectors-description": "Управление поисковыми системами, инструментами MCP и интеграциями браузера.",
|
||||
"channels": "Каналы",
|
||||
"channels-overview": "Обзор",
|
||||
"channels-overview-coming-soon-description": "Обзор каналов находится в разработке.",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "Lark",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "Интеграция для этого канала находится в разработке.",
|
||||
"browser": "Браузер",
|
||||
"settings": "Настройки",
|
||||
"general": "Общие",
|
||||
|
|
@ -137,7 +148,13 @@
|
|||
"projects-hub": "Центр проектов",
|
||||
"browser-management": "Управление браузером",
|
||||
"browser-management-description": "Файлы cookie браузера хранятся локально на вашем устройстве, используются только для задач на тех же сайтах и могут быть очищены или отключены в любое время.",
|
||||
"browser-connections": "Подключения",
|
||||
"browser-connections-description": "Настройка протокола Chrome DevTools для автоматизации браузера.",
|
||||
"browser-plugins": "Плагины",
|
||||
"browser-plugins-description": "Управление расширениями браузера для расширенной функциональности.",
|
||||
"restart-to-apply": "Перезапустить для применения",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "Управление Cookie",
|
||||
"browser-cookies": "Файлы cookie браузера",
|
||||
"browser-cookies-description": "Используйте \"Открыть браузер\" чтобы войти на веб-страницу и сохранить её файлы cookie. После перезапуска клиента Eigent запомнит их и поможет вам выполнить будущие задачи на этих сайтах.",
|
||||
"cookie-domains": "Домены cookie",
|
||||
|
|
|
|||
|
|
@ -33,8 +33,19 @@
|
|||
"installation-failed": "安装失败",
|
||||
"backend-startup-failed": "后端启动失败",
|
||||
"projects": "项目",
|
||||
"agents": "智能体",
|
||||
"mcp-tools": "MCP & 工具",
|
||||
"connectors": "连接器",
|
||||
"connectors-description": "管理搜索引擎、MCP 工具和浏览器集成。",
|
||||
"channels": "频道",
|
||||
"channels-overview": "概览",
|
||||
"channels-overview-coming-soon-description": "频道概览正在开发中。",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "飞书",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "此频道的集成正在开发中。",
|
||||
"browser": "浏览器",
|
||||
"settings": "设置",
|
||||
"general": "通用",
|
||||
|
|
@ -140,7 +151,13 @@
|
|||
"projects-hub": "项目中心",
|
||||
"browser-management": "浏览器管理",
|
||||
"browser-management-description": "浏览器 Cookie 存储在您设备本地,仅用于相同网站的任务,可随时清除或禁用。",
|
||||
"browser-connections": "连接",
|
||||
"browser-connections-description": "Chrome 开发者工具协议配置,用于浏览器自动化。",
|
||||
"browser-plugins": "插件",
|
||||
"browser-plugins-description": "管理浏览器扩展以增强功能。",
|
||||
"restart-to-apply": "重启以应用",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "Cookie 管理",
|
||||
"browser-cookies": "浏览器 Cookie",
|
||||
"browser-cookies-description": "使用「打开浏览器」登录网页并保存其 Cookie。重启客户端后,Eigent 会记住它们,并帮助您在这些网站上完成未来的任务。",
|
||||
"cookie-domains": "Cookie 域名",
|
||||
|
|
|
|||
|
|
@ -32,8 +32,19 @@
|
|||
"continue-with-github-login": "使用 Github 登录",
|
||||
"installation-failed": "安装失败",
|
||||
"projects": "專案",
|
||||
"agents": "智能體",
|
||||
"mcp-tools": "MCP & 工具",
|
||||
"connectors": "連接器",
|
||||
"connectors-description": "管理搜尋引擎、MCP 工具與瀏覽器整合。",
|
||||
"channels": "頻道",
|
||||
"channels-overview": "概覽",
|
||||
"channels-overview-coming-soon-description": "頻道概覽正在開發中。",
|
||||
"channels-whatsapp": "WhatsApp",
|
||||
"channels-lark": "飛書",
|
||||
"channels-telegram": "Telegram",
|
||||
"channels-slack": "Slack",
|
||||
"channels-discord": "Discord",
|
||||
"channels-coming-soon-description": "此頻道的整合正在開發中。",
|
||||
"browser": "瀏覽器",
|
||||
"settings": "設定",
|
||||
"general": "一般",
|
||||
|
|
@ -140,7 +151,13 @@
|
|||
"projects-hub": "專案中心",
|
||||
"browser-management": "瀏覽器管理",
|
||||
"browser-management-description": "瀏覽器 Cookie 儲存在您裝置本地,僅用於相同網站的工作,可隨時清除或停用。",
|
||||
"browser-connections": "連線",
|
||||
"browser-connections-description": "Chrome 開發者工具協定設定,用於瀏覽器自動化。",
|
||||
"browser-plugins": "外掛程式",
|
||||
"browser-plugins-description": "管理瀏覽器擴充功能以增強功能。",
|
||||
"restart-to-apply": "重新啟動以套用",
|
||||
"browser-cookie": "Cookies",
|
||||
"browser-cookie-management": "Cookie 管理",
|
||||
"browser-cookies": "瀏覽器 Cookie",
|
||||
"browser-cookies-description": "使用「開啟瀏覽器」登入網頁並儲存其 Cookie。重新啟動用戶端後,Eigent 會記住它們,並幫助您在這些網站上完成未來的任務。",
|
||||
"cookie-domains": "Cookie 網域",
|
||||
|
|
|
|||
|
|
@ -1115,13 +1115,13 @@ export default function SettingModels() {
|
|||
<button
|
||||
key={tabId}
|
||||
onClick={() => setSelectedTab(tabId)}
|
||||
className={`flex w-full items-center justify-between rounded-xl px-3 py-2 transition-all duration-200 ${isSubItem ? 'pl-3' : ''} ${
|
||||
className={`rounded-xl px-3 py-2 flex w-full items-center justify-between transition-all duration-200 ${isSubItem ? 'pl-3' : ''} ${
|
||||
isActive
|
||||
? 'bg-fill-fill-transparent-active'
|
||||
: 'bg-fill-fill-transparent hover:bg-fill-fill-transparent-hover'
|
||||
} `}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<div className="gap-3 flex items-center justify-center">
|
||||
{modelImage ? (
|
||||
<img
|
||||
src={modelImage}
|
||||
|
|
@ -1141,7 +1141,7 @@ export default function SettingModels() {
|
|||
</span>
|
||||
</div>
|
||||
{isConfigured && (
|
||||
<div className="m-1 h-2 w-2 rounded-full bg-text-success" />
|
||||
<div className="m-1 h-2 w-2 bg-text-success rounded-full" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
|
@ -1153,16 +1153,16 @@ export default function SettingModels() {
|
|||
if (selectedTab === 'cloud') {
|
||||
if (import.meta.env.VITE_USE_LOCAL_PROXY === 'true') {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center text-text-label">
|
||||
<div className="h-64 text-text-label flex items-center justify-center">
|
||||
{t('setting.cloud-not-available-in-local-proxy')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex w-full flex-col rounded-2xl bg-surface-tertiary">
|
||||
<div className="mx-6 mb-4 flex flex-col justify-start self-stretch border-x-0 border-b-[0.5px] border-t-0 border-solid border-border-secondary pb-4 pt-2">
|
||||
<div className="inline-flex items-center justify-start gap-2 self-stretch">
|
||||
<div className="text-body-base my-2 flex-1 justify-center font-bold text-text-heading">
|
||||
<div className="rounded-2xl bg-surface-tertiary flex w-full flex-col">
|
||||
<div className="mx-6 mb-4 border-border-secondary pb-4 pt-2 flex flex-col justify-start self-stretch border-x-0 border-t-0 border-b-[0.5px] border-solid">
|
||||
<div className="gap-2 inline-flex items-center justify-start self-stretch">
|
||||
<div className="text-body-base my-2 font-bold text-text-heading flex-1 justify-center">
|
||||
{t('setting.eigent-cloud')}
|
||||
</div>
|
||||
{cloudPrefer ? (
|
||||
|
|
@ -1181,7 +1181,7 @@ export default function SettingModels() {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="rounded-full !text-text-label"
|
||||
className="!text-text-label rounded-full"
|
||||
onClick={() => {
|
||||
setLocalPrefer(false);
|
||||
setActiveModelIdx(null);
|
||||
|
|
@ -1205,7 +1205,7 @@ export default function SettingModels() {
|
|||
onClick={() => {
|
||||
window.location.href = `https://www.eigent.ai/pricing`;
|
||||
}}
|
||||
className="cursor-pointer text-body-sm text-text-label underline"
|
||||
className="text-body-sm text-text-label cursor-pointer underline"
|
||||
>
|
||||
{t('setting.pricing-options')}
|
||||
</span>
|
||||
|
|
@ -1215,7 +1215,7 @@ export default function SettingModels() {
|
|||
</div>
|
||||
</div>
|
||||
{/*Content Area*/}
|
||||
<div className="flex w-full flex-row items-center justify-between gap-4 px-6 pb-4">
|
||||
<div className="gap-4 px-6 pb-4 flex w-full flex-row items-center justify-between">
|
||||
<div className="text-body-sm text-text-body">
|
||||
{t('setting.credits')}:{' '}
|
||||
{loadingCredits ? (
|
||||
|
|
@ -1240,9 +1240,9 @@ export default function SettingModels() {
|
|||
<Settings />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-center justify-between px-6 pb-4">
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-body-sm">
|
||||
<div className="px-6 pb-4 flex w-full flex-1 items-center justify-between">
|
||||
<div className="min-w-0 flex flex-1 items-center">
|
||||
<span className="text-body-sm overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{t('setting.select-model-type')}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -1306,15 +1306,15 @@ export default function SettingModels() {
|
|||
const canSwitch = !!form[idx].provider_id;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col rounded-2xl bg-surface-tertiary">
|
||||
<div className="mx-6 mb-4 flex flex-col items-start justify-between border-x-0 border-b-[0.5px] border-t-0 border-solid border-border-secondary pb-4 pt-2">
|
||||
<div className="inline-flex items-center justify-between gap-2 self-stretch">
|
||||
<div className="rounded-2xl bg-surface-tertiary flex w-full flex-col">
|
||||
<div className="mx-6 mb-4 border-border-secondary pb-4 pt-2 flex flex-col items-start justify-between border-x-0 border-t-0 border-b-[0.5px] border-solid">
|
||||
<div className="gap-2 inline-flex items-center justify-between self-stretch">
|
||||
<div className="text-body-base my-2 font-bold text-text-heading">
|
||||
{item.name}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="gap-2 flex items-center">
|
||||
{form[idx].prefer ? (
|
||||
<span className="inline-flex items-center rounded-full px-2 py-1 text-label-xs font-bold text-text-success">
|
||||
<span className="px-2 py-1 text-label-xs font-bold text-text-success inline-flex items-center rounded-full">
|
||||
{t('setting.default')}
|
||||
</span>
|
||||
) : (
|
||||
|
|
@ -1325,8 +1325,8 @@ export default function SettingModels() {
|
|||
onClick={() => handleSwitch(idx, true)}
|
||||
className={
|
||||
canSwitch
|
||||
? 'inline-flex items-center rounded-full bg-button-transparent-fill-hover !text-text-label shadow-none hover:bg-button-transparent-fill-active'
|
||||
: 'inline-flex items-center gap-1.5'
|
||||
? 'bg-button-transparent-fill-hover !text-text-label hover:bg-button-transparent-fill-active inline-flex items-center rounded-full shadow-none'
|
||||
: 'gap-1.5 inline-flex items-center'
|
||||
}
|
||||
>
|
||||
{!canSwitch
|
||||
|
|
@ -1335,9 +1335,9 @@ export default function SettingModels() {
|
|||
</Button>
|
||||
)}
|
||||
{form[idx].provider_id ? (
|
||||
<div className="h-2 w-2 shrink-0 rounded-full bg-text-success" />
|
||||
<div className="h-2 w-2 bg-text-success shrink-0 rounded-full" />
|
||||
) : (
|
||||
<div className="h-2 w-2 shrink-0 rounded-full bg-text-label opacity-10" />
|
||||
<div className="h-2 w-2 bg-text-label shrink-0 rounded-full opacity-10" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1345,7 +1345,7 @@ export default function SettingModels() {
|
|||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-center gap-4 px-6">
|
||||
<div className="gap-4 px-6 flex w-full flex-col items-center">
|
||||
{/* API Key Setting */}
|
||||
<Input
|
||||
id={`apiKey-${item.id}`}
|
||||
|
|
@ -1426,7 +1426,7 @@ export default function SettingModels() {
|
|||
{item.externalConfig &&
|
||||
form[idx].externalConfig &&
|
||||
form[idx].externalConfig.map((ec, ecIdx) => (
|
||||
<div key={ec.key} className="flex h-full w-full flex-col gap-4">
|
||||
<div key={ec.key} className="gap-4 flex h-full w-full flex-col">
|
||||
{ec.options && ec.options.length > 0 ? (
|
||||
<Select
|
||||
value={ec.value}
|
||||
|
|
@ -1493,7 +1493,7 @@ export default function SettingModels() {
|
|||
))}
|
||||
</div>
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-end gap-2 px-6 py-4">
|
||||
<div className="gap-2 px-6 py-4 flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -1526,10 +1526,10 @@ export default function SettingModels() {
|
|||
const isPreferred = localPrefer && localPlatform === platform;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col rounded-2xl bg-surface-tertiary">
|
||||
<div className="mx-6 mb-4 flex flex-col items-start justify-between border-x-0 border-b-[0.5px] border-t-0 border-solid border-border-secondary pb-4 pt-2">
|
||||
<div className="inline-flex items-center justify-between gap-2 self-stretch">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="rounded-2xl bg-surface-tertiary flex w-full flex-col">
|
||||
<div className="mx-6 mb-4 border-border-secondary pb-4 pt-2 flex flex-col items-start justify-between border-x-0 border-t-0 border-b-[0.5px] border-solid">
|
||||
<div className="gap-2 inline-flex items-center justify-between self-stretch">
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="text-body-base my-2 font-bold text-text-heading">
|
||||
{platform === 'ollama'
|
||||
? 'Ollama'
|
||||
|
|
@ -1557,7 +1557,7 @@ export default function SettingModels() {
|
|||
onClick={() => handleLocalSwitch(true)}
|
||||
className={
|
||||
isConfigured
|
||||
? 'rounded-full bg-button-transparent-fill-hover !text-text-label shadow-none'
|
||||
? 'bg-button-transparent-fill-hover !text-text-label rounded-full shadow-none'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
|
|
@ -1568,14 +1568,14 @@ export default function SettingModels() {
|
|||
)}
|
||||
</div>
|
||||
{isConfigured ? (
|
||||
<div className="h-2 w-2 rounded-full bg-text-success" />
|
||||
<div className="h-2 w-2 bg-text-success rounded-full" />
|
||||
) : (
|
||||
<div className="h-2 w-2 rounded-full bg-text-label opacity-10" />
|
||||
<div className="h-2 w-2 bg-text-label rounded-full opacity-10" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Model Endpoint URL Setting */}
|
||||
<div className="flex w-full flex-col items-center gap-4 px-6">
|
||||
<div className="gap-4 px-6 flex w-full flex-col items-center">
|
||||
<Input
|
||||
size="default"
|
||||
title={t('setting.model-endpoint-url')}
|
||||
|
|
@ -1623,8 +1623,8 @@ export default function SettingModels() {
|
|||
note={localError ?? undefined}
|
||||
/>
|
||||
{platform === 'ollama' ? (
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex w-full items-end gap-2">
|
||||
<div className="gap-1 flex w-full flex-col">
|
||||
<div className="gap-2 flex w-full items-end">
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={currentType}
|
||||
|
|
@ -1718,7 +1718,7 @@ export default function SettingModels() {
|
|||
)}
|
||||
</div>
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-end gap-2 px-6 py-4">
|
||||
<div className="gap-2 px-6 py-4 flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -1748,8 +1748,8 @@ export default function SettingModels() {
|
|||
return (
|
||||
<div className="m-auto flex h-auto w-full flex-1 flex-col">
|
||||
{/* Header Section */}
|
||||
<div className="sticky top-0 z-10 flex w-full items-center justify-between bg-surface-primary px-6 pb-6 pt-8">
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4">
|
||||
<div className="top-0 bg-surface-primary px-6 pb-6 pt-8 sticky z-10 flex w-full items-center justify-between">
|
||||
<div className="gap-4 flex w-full flex-col items-start justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('setting.models')}
|
||||
|
|
@ -1758,10 +1758,10 @@ export default function SettingModels() {
|
|||
</div>
|
||||
</div>
|
||||
{/* Content Section */}
|
||||
<div className="mb-8 flex flex-col gap-6">
|
||||
<div className="mb-8 gap-6 flex flex-col">
|
||||
{/* Default Model Cascading Dropdown */}
|
||||
<div className="flex w-full flex-row items-center justify-between gap-4 rounded-2xl bg-surface-secondary px-6 py-4">
|
||||
<div className="flex w-full flex-col items-start justify-center gap-1">
|
||||
<div className="gap-4 rounded-2xl bg-surface-secondary px-6 py-4 flex w-full flex-col items-end justify-between">
|
||||
<div className="gap-1 flex w-full flex-col items-start justify-center">
|
||||
<div className="text-body-base font-bold text-text-heading">
|
||||
{t('setting.models-default-setting-title')}
|
||||
</div>
|
||||
|
|
@ -1771,11 +1771,11 @@ export default function SettingModels() {
|
|||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex w-fit items-center justify-between gap-2 rounded-lg border-[0.5px] border-solid border-border-success bg-surface-success px-3 py-1 font-semibold text-text-success transition-colors hover:opacity-70 active:opacity-90">
|
||||
<span className="whitespace-nowrap text-body-sm">
|
||||
<button className="gap-2 rounded-lg border-border-success bg-surface-success px-3 py-1 font-semibold text-text-success flex w-fit items-center justify-between border-[0.5px] border-solid transition-colors hover:opacity-70 active:opacity-90">
|
||||
<span className="text-body-sm whitespace-nowrap">
|
||||
{getDefaultModelDisplayText()}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 flex-shrink-0 text-text-success" />
|
||||
<ChevronDown className="h-4 w-4 text-text-success flex-shrink-0" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[180px]">
|
||||
|
|
@ -1829,7 +1829,7 @@ export default function SettingModels() {
|
|||
}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="gap-2 flex items-center">
|
||||
{modelImage ? (
|
||||
<img
|
||||
src={modelImage}
|
||||
|
|
@ -1850,15 +1850,15 @@ export default function SettingModels() {
|
|||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="gap-1 flex items-center">
|
||||
{!isConfigured && (
|
||||
<div className="h-2 w-2 rounded-full bg-text-label opacity-10" />
|
||||
<div className="h-2 w-2 bg-text-label rounded-full opacity-10" />
|
||||
)}
|
||||
{isPreferred && (
|
||||
<Check className="h-4 w-4 text-text-success" />
|
||||
)}
|
||||
{isConfigured && !isPreferred && (
|
||||
<div className="h-2 w-2 rounded-full bg-text-success" />
|
||||
<div className="h-2 w-2 bg-text-success rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -1890,7 +1890,7 @@ export default function SettingModels() {
|
|||
}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="gap-2 flex items-center">
|
||||
{modelImage ? (
|
||||
<img
|
||||
src={modelImage}
|
||||
|
|
@ -1911,15 +1911,15 @@ export default function SettingModels() {
|
|||
{model.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="gap-1 flex items-center">
|
||||
{!isConfigured && (
|
||||
<div className="h-2 w-2 rounded-full bg-text-label opacity-10" />
|
||||
<div className="h-2 w-2 bg-text-label rounded-full opacity-10" />
|
||||
)}
|
||||
{isPreferred && (
|
||||
<Check className="h-4 w-4 text-text-success" />
|
||||
)}
|
||||
{isConfigured && !isPreferred && (
|
||||
<div className="h-2 w-2 rounded-full bg-text-success" />
|
||||
<div className="h-2 w-2 bg-text-success rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -1932,17 +1932,17 @@ export default function SettingModels() {
|
|||
</div>
|
||||
|
||||
{/* Content Section with Sidebar */}
|
||||
<div className="flex w-full flex-col items-start justify-between gap-2 rounded-2xl bg-surface-secondary px-6 py-4">
|
||||
<div className="text-body-base sticky top-[86px] z-10 mb-2 w-full border-x-0 border-b-[0.5px] border-t-0 border-solid border-border-secondary bg-surface-secondary pb-4 font-bold text-text-heading">
|
||||
<div className="gap-2 rounded-2xl bg-surface-secondary px-6 py-4 flex w-full flex-col items-start justify-between">
|
||||
<div className="text-body-base mb-2 border-border-secondary bg-surface-secondary pb-4 font-bold text-text-heading sticky top-[86px] z-10 w-full border-x-0 border-t-0 border-b-[0.5px] border-solid">
|
||||
{t('setting.models-configuration')}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-row items-start justify-between">
|
||||
{/* Sidebar */}
|
||||
<div className="-ml-2 mr-4 h-full w-[240px] rounded-2xl bg-surface-secondary">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="-ml-2 mr-4 rounded-2xl bg-surface-secondary h-full w-[240px]">
|
||||
<div className="gap-4 flex flex-col">
|
||||
{/* Eigent Cloud Section */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="gap-1 flex flex-col">
|
||||
<div className="px-3 py-2 text-body-sm font-bold text-text-heading">
|
||||
{t('setting.eigent-cloud')}
|
||||
</div>
|
||||
|
|
@ -1957,10 +1957,10 @@ export default function SettingModels() {
|
|||
)}
|
||||
</div>
|
||||
{/* Bring Your Own Key Section */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="gap-1 flex flex-col">
|
||||
<button
|
||||
onClick={() => setByokCollapsed(!byokCollapsed)}
|
||||
className="flex items-center justify-between rounded-lg bg-transparent px-3 py-2 transition-colors hover:bg-surface-secondary"
|
||||
className="rounded-lg px-3 py-2 hover:bg-surface-secondary flex items-center justify-between bg-transparent transition-colors"
|
||||
>
|
||||
<div className="text-body-sm font-bold text-text-heading">
|
||||
{t('setting.custom-model')}
|
||||
|
|
@ -1972,7 +1972,7 @@ export default function SettingModels() {
|
|||
)}
|
||||
</button>
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
className={`ease-in-out overflow-hidden transition-all duration-300 ${
|
||||
byokCollapsed
|
||||
? 'max-h-0 opacity-0'
|
||||
: 'max-h-[2000px] opacity-100'
|
||||
|
|
@ -1992,10 +1992,10 @@ export default function SettingModels() {
|
|||
</div>
|
||||
|
||||
{/* Local Model Section */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="gap-1 flex flex-col">
|
||||
<button
|
||||
onClick={() => setLocalCollapsed(!localCollapsed)}
|
||||
className="flex items-center justify-between rounded-lg bg-transparent px-3 py-2 transition-colors hover:bg-surface-secondary"
|
||||
className="rounded-lg px-3 py-2 hover:bg-surface-secondary flex items-center justify-between bg-transparent transition-colors"
|
||||
>
|
||||
<div className="text-body-sm font-bold text-text-heading">
|
||||
{t('setting.local-model')}
|
||||
|
|
@ -2007,7 +2007,7 @@ export default function SettingModels() {
|
|||
)}
|
||||
</button>
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
className={`ease-in-out overflow-hidden transition-all duration-300 ${
|
||||
localCollapsed
|
||||
? 'max-h-0 opacity-0'
|
||||
: 'max-h-[2000px] opacity-100'
|
||||
|
|
@ -2050,7 +2050,7 @@ export default function SettingModels() {
|
|||
</div>
|
||||
</div>
|
||||
{/* Main Content */}
|
||||
<div className="sticky top-[136px] z-10 min-w-0 flex-1">
|
||||
<div className="min-w-0 sticky top-[136px] z-10 flex-1">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import Skills from './Skills';
|
|||
|
||||
export default function Capabilities() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('skills');
|
||||
const [activeTab, setActiveTab] = useState('models');
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
|
|
@ -46,8 +46,8 @@ export default function Capabilities() {
|
|||
|
||||
return (
|
||||
<div className="m-auto flex h-auto max-w-[940px] flex-col">
|
||||
<div className="flex h-auto w-full px-6">
|
||||
<div className="sticky top-20 flex h-full w-40 flex-shrink-0 flex-grow-0 flex-col justify-between self-start pr-6 pt-8">
|
||||
<div className="px-6 flex h-auto w-full">
|
||||
<div className="top-20 w-40 pr-6 pt-8 sticky flex h-full flex-shrink-0 flex-grow-0 flex-col justify-between self-start">
|
||||
<VerticalNavigation
|
||||
items={
|
||||
menuItems.map((menu) => ({
|
||||
|
|
@ -59,14 +59,14 @@ export default function Capabilities() {
|
|||
}
|
||||
value={activeTab}
|
||||
onValueChange={handleTabChange}
|
||||
className="h-full min-h-0 w-full flex-1 gap-0"
|
||||
className="min-h-0 gap-0 h-full w-full flex-1"
|
||||
listClassName="w-full h-full overflow-y-auto"
|
||||
contentClassName="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-auto w-full flex-1 flex-col">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="gap-4 flex flex-col">
|
||||
{activeTab === 'models' && <Models />}
|
||||
{activeTab === 'skills' && <Skills />}
|
||||
{activeTab === 'memory' && <Memory />}
|
||||
|
|
|
|||
316
src/pages/Browser/CDP.tsx
Normal file
316
src/pages/Browser/CDP.tsx
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import AlertDialog from '@/components/ui/alertDialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Globe, Link2, Loader2, Plus, Trash2 } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CdpBrowser {
|
||||
id: string;
|
||||
port: number;
|
||||
isExternal: boolean;
|
||||
name?: string;
|
||||
addedAt: number;
|
||||
}
|
||||
|
||||
export default function CDP() {
|
||||
const { t } = useTranslation();
|
||||
const [cdpBrowsers, setCdpBrowsers] = useState<CdpBrowser[]>([]);
|
||||
const [deletingBrowser, setDeletingBrowser] = useState<string | null>(null);
|
||||
const [browserToRemove, setBrowserToRemove] = useState<CdpBrowser | null>(
|
||||
null
|
||||
);
|
||||
const [showConnectDialog, setShowConnectDialog] = useState(false);
|
||||
const [connectPort, setConnectPort] = useState('');
|
||||
const [connectChecking, setConnectChecking] = useState(false);
|
||||
const [connectError, setConnectError] = useState('');
|
||||
|
||||
const loadCdpBrowsers = async () => {
|
||||
if (window.electronAPI?.getCdpBrowsers) {
|
||||
try {
|
||||
const browsers = await window.electronAPI.getCdpBrowsers();
|
||||
setCdpBrowsers(browsers);
|
||||
} catch (error) {
|
||||
console.error('Failed to load CDP browsers:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCdpBrowsers();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI?.onCdpPoolChanged) return;
|
||||
const cleanup = window.electronAPI.onCdpPoolChanged(
|
||||
(browsers: CdpBrowser[]) => {
|
||||
setCdpBrowsers(browsers);
|
||||
}
|
||||
);
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
const handleRemoveBrowser = async (browserId: string) => {
|
||||
setDeletingBrowser(browserId);
|
||||
try {
|
||||
if (window.electronAPI?.removeCdpBrowser) {
|
||||
const result = await window.electronAPI.removeCdpBrowser(browserId);
|
||||
if (result.success) {
|
||||
toast.success(t('layout.browser-removed'));
|
||||
} else {
|
||||
toast.error(result.error || t('layout.failed-to-remove-browser'));
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || t('layout.failed-to-remove-browser'));
|
||||
} finally {
|
||||
setDeletingBrowser(null);
|
||||
setBrowserToRemove(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenNewBrowser = async () => {
|
||||
try {
|
||||
toast.loading(t('layout.launching-browser', { port: '...' }), {
|
||||
id: 'launch-browser',
|
||||
});
|
||||
const result = await window.electronAPI?.launchCdpBrowser();
|
||||
if (result?.success) {
|
||||
toast.success(t('layout.browser-launched', { port: result.port }), {
|
||||
id: 'launch-browser',
|
||||
});
|
||||
} else {
|
||||
toast.error(result?.error || t('layout.failed-to-launch-browser'), {
|
||||
id: 'launch-browser',
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || t('layout.failed-to-launch-browser'), {
|
||||
id: 'launch-browser',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnectExistingBrowser = () => {
|
||||
setConnectPort('');
|
||||
setConnectError('');
|
||||
setShowConnectDialog(true);
|
||||
};
|
||||
|
||||
const handleCheckAndConnect = async () => {
|
||||
const portNum = parseInt(connectPort, 10);
|
||||
if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
||||
setConnectError(t('layout.invalid-port'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cdpBrowsers.some((browser) => browser.port === portNum)) {
|
||||
setConnectError(t('layout.port-already-in-use'));
|
||||
return;
|
||||
}
|
||||
|
||||
setConnectChecking(true);
|
||||
setConnectError('');
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
const response = await fetch(`http://localhost:${portNum}/json/version`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
|
||||
if (!response.ok) {
|
||||
setConnectError(t('layout.no-browser-on-port', { port: portNum }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.electronAPI?.addCdpBrowser) {
|
||||
const addResult = await window.electronAPI.addCdpBrowser(
|
||||
portNum,
|
||||
true,
|
||||
`External Browser (${portNum})`
|
||||
);
|
||||
if (!addResult?.success) {
|
||||
setConnectError(
|
||||
addResult?.error || t('layout.failed-to-add-browser')
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setConnectError(t('layout.failed-to-add-browser'));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(t('layout.connected-browser', { port: portNum }));
|
||||
setShowConnectDialog(false);
|
||||
} catch {
|
||||
setConnectError(t('layout.no-browser-on-port', { port: portNum }));
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
setConnectChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="m-auto flex h-auto w-full flex-1 flex-col">
|
||||
<AlertDialog
|
||||
isOpen={!!browserToRemove}
|
||||
onClose={() => setBrowserToRemove(null)}
|
||||
onConfirm={() => {
|
||||
if (browserToRemove) {
|
||||
handleRemoveBrowser(browserToRemove.id);
|
||||
}
|
||||
}}
|
||||
title={t('layout.remove-browser')}
|
||||
message={t('layout.remove-browser-confirm', {
|
||||
name: browserToRemove?.name || `Browser ${browserToRemove?.port}`,
|
||||
port: browserToRemove?.port,
|
||||
})}
|
||||
confirmText={t('layout.remove')}
|
||||
cancelText={t('layout.cancel')}
|
||||
confirmVariant="cuation"
|
||||
/>
|
||||
|
||||
<div className="flex w-full items-center justify-between px-6 pb-6 pt-8">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('layout.cdp-browser-connection')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 px-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="primary" size="sm" onClick={handleOpenNewBrowser}>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('layout.open-new-browser')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleConnectExistingBrowser}
|
||||
>
|
||||
<Link2 className="h-4 w-4 text-button-tertiery-text-default" />
|
||||
{t('layout.connect-existing-browser')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-body-base font-bold text-text-body">
|
||||
{t('layout.cdp-browser-pool')}
|
||||
</div>
|
||||
|
||||
{cdpBrowsers.length > 0 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{cdpBrowsers.map((browser) => (
|
||||
<div
|
||||
key={browser.id}
|
||||
className="flex items-center justify-between rounded-xl border-solid border-border-disabled bg-surface-tertiary px-4 py-2"
|
||||
>
|
||||
<div className="flex w-full flex-row items-center gap-2">
|
||||
<div className="h-2 w-2 shrink-0 rounded-full bg-text-success" />
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<span className="text-body-sm font-bold text-text-body">
|
||||
{browser.name || `Browser ${browser.port}`}
|
||||
</span>
|
||||
<span className="text-label-xs text-text-label">
|
||||
{t('layout.port')} {browser.port}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setBrowserToRemove(browser)}
|
||||
disabled={deletingBrowser === browser.id}
|
||||
className="ml-3 flex-shrink-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-text-cuation" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-4 py-8">
|
||||
<Globe className="mb-4 h-12 w-12 text-icon-secondary opacity-50" />
|
||||
<div className="text-body-base text-center font-bold text-text-label">
|
||||
{t('layout.no-browsers-in-pool')}
|
||||
</div>
|
||||
<p className="text-center text-label-xs font-medium text-text-label">
|
||||
{t('layout.add-browsers-hint')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showConnectDialog && (
|
||||
<div className="bg-black/50 fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="w-full max-w-md rounded-xl bg-surface-primary p-6 shadow-lg">
|
||||
<div className="text-body-base mb-2 font-bold text-text-heading">
|
||||
{t('layout.connect-existing-browser')}
|
||||
</div>
|
||||
<p className="mb-4 text-label-xs text-text-label">
|
||||
{t('layout.connect-existing-browser-description')}
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={connectPort}
|
||||
onChange={(event) => {
|
||||
setConnectPort(event.target.value);
|
||||
setConnectError('');
|
||||
}}
|
||||
placeholder={t('layout.enter-port-number')}
|
||||
className="w-full rounded-lg border border-border-disabled bg-surface-secondary px-4 py-2 text-body-sm text-text-body outline-none focus:border-border-focus"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') handleCheckAndConnect();
|
||||
}}
|
||||
/>
|
||||
{connectError && (
|
||||
<p className="mt-2 text-label-xs text-text-cuation">
|
||||
{connectError}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowConnectDialog(false)}
|
||||
>
|
||||
{t('layout.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleCheckAndConnect}
|
||||
disabled={connectChecking}
|
||||
>
|
||||
{connectChecking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Link2 className="h-4 w-4" />
|
||||
)}
|
||||
{t('layout.check-and-connect')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
365
src/pages/Browser/Cookies.tsx
Normal file
365
src/pages/Browser/Cookies.tsx
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { fetchDelete, fetchGet, fetchPost } from '@/api/http';
|
||||
import AlertDialog from '@/components/ui/alertDialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Cookie, Plus, RefreshCw, Trash2 } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CookieDomain {
|
||||
domain: string;
|
||||
cookie_count: number;
|
||||
last_access: string;
|
||||
}
|
||||
|
||||
interface GroupedDomain {
|
||||
mainDomain: string;
|
||||
subdomains: CookieDomain[];
|
||||
totalCookies: number;
|
||||
}
|
||||
|
||||
export default function Cookies() {
|
||||
const { t } = useTranslation();
|
||||
const [loginLoading, setLoginLoading] = useState(false);
|
||||
const [cookiesLoading, setCookiesLoading] = useState(false);
|
||||
const [cookieDomains, setCookieDomains] = useState<CookieDomain[]>([]);
|
||||
const [deletingDomain, setDeletingDomain] = useState<string | null>(null);
|
||||
const [deletingAll, setDeletingAll] = useState(false);
|
||||
const [showRestartDialog, setShowRestartDialog] = useState(false);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
const getMainDomain = (domain: string): string => {
|
||||
const cleanDomain = domain.startsWith('.') ? domain.substring(1) : domain;
|
||||
const parts = cleanDomain.split('.');
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return cleanDomain;
|
||||
}
|
||||
|
||||
return parts.slice(-2).join('.');
|
||||
};
|
||||
|
||||
const groupDomainsByMain = (domains: CookieDomain[]): GroupedDomain[] => {
|
||||
const grouped = new Map<string, CookieDomain[]>();
|
||||
|
||||
domains.forEach((item) => {
|
||||
const mainDomain = getMainDomain(item.domain);
|
||||
if (!grouped.has(mainDomain)) {
|
||||
grouped.set(mainDomain, []);
|
||||
}
|
||||
grouped.get(mainDomain)!.push(item);
|
||||
});
|
||||
|
||||
return Array.from(grouped.entries())
|
||||
.map(([mainDomain, subdomains]) => ({
|
||||
mainDomain,
|
||||
subdomains,
|
||||
totalCookies: subdomains.reduce(
|
||||
(sum, item) => sum + item.cookie_count,
|
||||
0
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.mainDomain.localeCompare(b.mainDomain));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleLoadCookies();
|
||||
}, []);
|
||||
|
||||
const handleBrowserLogin = async () => {
|
||||
setLoginLoading(true);
|
||||
try {
|
||||
const currentCookieCount = cookieDomains.reduce(
|
||||
(sum, item) => sum + item.cookie_count,
|
||||
0
|
||||
);
|
||||
|
||||
const response = await fetchPost('/browser/login');
|
||||
if (response) {
|
||||
toast.success('Browser opened successfully for login');
|
||||
const checkInterval = setInterval(async () => {
|
||||
try {
|
||||
const statusResponse = await fetchGet('/browser/status');
|
||||
if (!statusResponse || !statusResponse.is_open) {
|
||||
clearInterval(checkInterval);
|
||||
await handleLoadCookies();
|
||||
const newResponse = await fetchGet('/browser/cookies');
|
||||
if (newResponse && newResponse.success) {
|
||||
const newDomains = newResponse.domains || [];
|
||||
const newCookieCount = newDomains.reduce(
|
||||
(sum: number, item: CookieDomain) => sum + item.cookie_count,
|
||||
0
|
||||
);
|
||||
|
||||
if (newCookieCount > currentCookieCount) {
|
||||
const addedCount = newCookieCount - currentCookieCount;
|
||||
toast.success(
|
||||
`Added ${addedCount} cookie${addedCount !== 1 ? 's' : ''}`
|
||||
);
|
||||
setHasUnsavedChanges(true);
|
||||
setShowRestartDialog(true);
|
||||
} else if (newCookieCount < currentCookieCount) {
|
||||
setHasUnsavedChanges(true);
|
||||
setShowRestartDialog(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
clearInterval(checkInterval);
|
||||
await handleLoadCookies();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || 'Failed to open browser');
|
||||
} finally {
|
||||
setLoginLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadCookies = async () => {
|
||||
setCookiesLoading(true);
|
||||
try {
|
||||
const response = await fetchGet('/browser/cookies');
|
||||
if (response && response.success) {
|
||||
const domains = response.domains || [];
|
||||
setCookieDomains(domains);
|
||||
} else {
|
||||
setCookieDomains([]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || 'Failed to load cookies');
|
||||
setCookieDomains([]);
|
||||
} finally {
|
||||
setCookiesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteMainDomain = async (
|
||||
mainDomain: string,
|
||||
subdomains: CookieDomain[]
|
||||
) => {
|
||||
setDeletingDomain(mainDomain);
|
||||
try {
|
||||
const deletePromises = subdomains.map((item) =>
|
||||
fetchDelete(`/browser/cookies/${encodeURIComponent(item.domain)}`)
|
||||
);
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
toast.success(`Deleted cookies for ${mainDomain} and all subdomains`);
|
||||
const domainsToRemove = new Set(subdomains.map((item) => item.domain));
|
||||
setCookieDomains((prev) =>
|
||||
prev.filter((item) => !domainsToRemove.has(item.domain))
|
||||
);
|
||||
|
||||
setHasUnsavedChanges(true);
|
||||
setShowRestartDialog(true);
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
error?.message || `Failed to delete cookies for ${mainDomain}`
|
||||
);
|
||||
} finally {
|
||||
setDeletingDomain(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAll = async () => {
|
||||
setDeletingAll(true);
|
||||
try {
|
||||
await fetchDelete('/browser/cookies');
|
||||
toast.success('Deleted all cookies');
|
||||
setCookieDomains([]);
|
||||
|
||||
setHasUnsavedChanges(true);
|
||||
setShowRestartDialog(true);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || 'Failed to delete all cookies');
|
||||
} finally {
|
||||
setDeletingAll(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartApp = () => {
|
||||
if (window.electronAPI && window.electronAPI.restartApp) {
|
||||
window.electronAPI.restartApp();
|
||||
} else {
|
||||
toast.error('Restart function not available');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmRestart = () => {
|
||||
setShowRestartDialog(false);
|
||||
handleRestartApp();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="m-auto flex h-auto w-full flex-1 flex-col">
|
||||
<AlertDialog
|
||||
isOpen={showRestartDialog}
|
||||
onClose={() => setShowRestartDialog(false)}
|
||||
onConfirm={handleConfirmRestart}
|
||||
title="Cookies Updated"
|
||||
message="Cookies have been updated. Would you like to restart the application to use the new cookies?"
|
||||
confirmText="Yes, Restart"
|
||||
cancelText="No, Add More"
|
||||
confirmVariant="information"
|
||||
/>
|
||||
|
||||
<div className="px-6 pb-6 pt-8 flex w-full items-center justify-between">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('layout.browser-cookie-management')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gap-4 flex flex-col">
|
||||
<div className="rounded-xl border-border-disabled bg-surface-secondary p-6 relative flex w-full flex-col border">
|
||||
<div className="right-6 top-6 absolute">
|
||||
<Button
|
||||
variant="information"
|
||||
size="xs"
|
||||
onClick={handleRestartApp}
|
||||
className="gap-0 ease-in-out justify-center overflow-hidden rounded-full transition-all duration-300"
|
||||
>
|
||||
<RefreshCw className="flex-shrink-0" />
|
||||
<span
|
||||
className={`ease-in-out overflow-hidden transition-all duration-300 ${
|
||||
hasUnsavedChanges
|
||||
? 'pl-2 max-w-[150px] opacity-100'
|
||||
: 'ml-0 max-w-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
{t('layout.restart-to-apply')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-body-sm text-text-label max-w-[600px]">
|
||||
{t('layout.browser-cookies-description')}
|
||||
</div>
|
||||
<div className="mt-4 gap-3 border-border-secondary pt-3 flex w-full flex-col border-[0.5px] border-x-0 border-b-0 border-solid">
|
||||
<div className="py-2 flex flex-row items-center justify-between">
|
||||
<div className="gap-2 flex flex-row items-center justify-start">
|
||||
<div className="text-body-base font-bold text-text-body">
|
||||
{t('layout.cookie-domains')}
|
||||
</div>
|
||||
{cookieDomains.length > 0 && (
|
||||
<div className="rounded-lg bg-tag-fill-info px-2 text-label-sm font-bold text-text-information">
|
||||
{groupDomainsByMain(cookieDomains).length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="gap-2 flex items-center">
|
||||
{cookieDomains.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDeleteAll}
|
||||
disabled={deletingAll}
|
||||
className="!text-text-cuation uppercase"
|
||||
>
|
||||
{deletingAll
|
||||
? t('layout.deleting')
|
||||
: t('layout.delete-all')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleLoadCookies}
|
||||
disabled={cookiesLoading}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${cookiesLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleBrowserLogin}
|
||||
disabled={loginLoading}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{loginLoading
|
||||
? t('layout.opening')
|
||||
: t('layout.open-browser')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cookieDomains.length > 0 ? (
|
||||
<div className="gap-2 flex flex-col">
|
||||
{groupDomainsByMain(cookieDomains).map((group, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-xl border-border-disabled bg-surface-tertiary px-4 py-2 flex items-center justify-between border-solid"
|
||||
>
|
||||
<div className="flex w-full flex-col items-start justify-start">
|
||||
<span className="text-body-sm font-bold text-text-body truncate">
|
||||
{group.mainDomain}
|
||||
</span>
|
||||
<span className="mt-1 text-label-xs text-text-label">
|
||||
{group.totalCookies} Cookie
|
||||
{group.totalCookies !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
handleDeleteMainDomain(
|
||||
group.mainDomain,
|
||||
group.subdomains
|
||||
)
|
||||
}
|
||||
disabled={deletingDomain === group.mainDomain}
|
||||
className="ml-3 flex-shrink-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-text-cuation" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4 py-8 flex flex-col items-center justify-center">
|
||||
<Cookie className="mb-4 h-12 w-12 text-icon-secondary opacity-50" />
|
||||
<div className="text-body-base font-bold text-text-label text-center">
|
||||
{t('layout.no-cookies-saved-yet')}
|
||||
</div>
|
||||
<p className="text-label-xs font-medium text-text-label text-center">
|
||||
{t('layout.no-cookies-saved-yet-description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-label-xs text-text-label w-full text-center">
|
||||
For more information, check out our
|
||||
<a
|
||||
href="https://www.eigent.ai/privacy-policy"
|
||||
target="_blank"
|
||||
className="ml-1 text-text-information underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('layout.privacy-policy')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
src/pages/Browser/Extension.tsx
Normal file
41
src/pages/Browser/Extension.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { Puzzle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function Extension() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="m-auto flex h-auto w-full flex-1 flex-col">
|
||||
<div className="px-6 pb-6 pt-8 flex w-full items-center justify-between">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('layout.browser-plugins')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="gap-4 px-6 flex flex-col">
|
||||
<div className="rounded-xl border-border-disabled bg-surface-secondary px-6 py-16 flex flex-col items-center justify-center border">
|
||||
<Puzzle className="mb-4 h-12 w-12 text-icon-secondary opacity-50" />
|
||||
<div className="text-body-base font-bold text-text-label text-center">
|
||||
{t('layout.coming-soon')}
|
||||
</div>
|
||||
<p className="mt-2 text-label-sm text-text-label text-center">
|
||||
{t('layout.browser-plugins-description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
78
src/pages/Browser/index.tsx
Normal file
78
src/pages/Browser/index.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import VerticalNavigation, {
|
||||
type VerticalNavItem,
|
||||
} from '@/components/Navigation';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CDP from './CDP';
|
||||
import Cookies from './Cookies';
|
||||
import Extension from './Extension';
|
||||
|
||||
export default function Browser() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('cdp');
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'cdp',
|
||||
name: t('layout.browser-connections'),
|
||||
},
|
||||
{
|
||||
id: 'extension',
|
||||
name: t('layout.browser-plugins'),
|
||||
},
|
||||
{
|
||||
id: 'cookies',
|
||||
name: t('layout.browser-cookie'),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
setActiveTab(tabId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="m-auto flex h-auto max-w-[940px] flex-col">
|
||||
<div className="px-6 flex h-auto w-full">
|
||||
<div className="top-20 w-40 pr-6 pt-8 sticky flex h-full flex-shrink-0 flex-grow-0 flex-col justify-between self-start">
|
||||
<VerticalNavigation
|
||||
items={
|
||||
menuItems.map((menu) => ({
|
||||
value: menu.id,
|
||||
label: (
|
||||
<span className="text-body-sm font-bold">{menu.name}</span>
|
||||
),
|
||||
})) as VerticalNavItem[]
|
||||
}
|
||||
value={activeTab}
|
||||
onValueChange={handleTabChange}
|
||||
className="min-h-0 gap-0 h-full w-full flex-1"
|
||||
listClassName="w-full h-full overflow-y-auto"
|
||||
contentClassName="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-auto w-full flex-1 flex-col">
|
||||
<div className="gap-4 flex flex-col">
|
||||
{activeTab === 'cdp' && <CDP />}
|
||||
{activeTab === 'extension' && <Extension />}
|
||||
{activeTab === 'cookies' && <Cookies />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
88
src/pages/Channels/index.tsx
Normal file
88
src/pages/Channels/index.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import VerticalNavigation, {
|
||||
type VerticalNavItem,
|
||||
} from '@/components/Navigation';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const VISIBLE_CHANNELS = ['overview', 'whatsapp', 'lark'] as const;
|
||||
|
||||
export default function Channels() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<string>('overview');
|
||||
|
||||
const menuItems = VISIBLE_CHANNELS.map((id) => ({
|
||||
id,
|
||||
name: t(`layout.channels-${id}`),
|
||||
}));
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
setActiveTab(tabId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="m-auto flex h-auto max-w-[940px] flex-col">
|
||||
<div className="px-6 flex h-auto w-full">
|
||||
<div className="top-20 w-40 pr-6 pt-8 sticky flex h-full flex-shrink-0 flex-grow-0 flex-col justify-between self-start">
|
||||
<VerticalNavigation
|
||||
items={
|
||||
menuItems.map((menu) => ({
|
||||
value: menu.id,
|
||||
label: (
|
||||
<span className="text-body-sm font-bold">{menu.name}</span>
|
||||
),
|
||||
})) as VerticalNavItem[]
|
||||
}
|
||||
value={activeTab}
|
||||
onValueChange={handleTabChange}
|
||||
className="min-h-0 gap-0 h-full w-full flex-1"
|
||||
listClassName="w-full h-full overflow-y-auto"
|
||||
contentClassName="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-auto w-full flex-1 flex-col">
|
||||
<div className="m-auto flex h-auto w-full flex-1 flex-col">
|
||||
{/* Header Section */}
|
||||
<div className="px-6 pb-6 pt-8 flex w-full items-center justify-between">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{menuItems.find((m) => m.id === activeTab)?.name ?? ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="mb-12 gap-6 flex flex-col">
|
||||
<div className="rounded-2xl bg-surface-secondary px-6 py-4 flex w-full flex-col items-center justify-between">
|
||||
<div className="h-16 w-16 flex items-center justify-center">
|
||||
<MessageSquare className="h-8 w-8 text-icon-secondary" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-body-md font-bold text-text-heading">
|
||||
{t('layout.coming-soon')}
|
||||
</h2>
|
||||
<p className="max-w-md text-body-sm text-text-label text-center">
|
||||
{activeTab === 'overview'
|
||||
? t('layout.channels-overview-coming-soon-description')
|
||||
: t('layout.channels-coming-soon-description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
707
src/pages/Connectors/MCP.tsx
Normal file
707
src/pages/Connectors/MCP.tsx
Normal file
|
|
@ -0,0 +1,707 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import {
|
||||
fetchGet,
|
||||
fetchPost,
|
||||
proxyFetchDelete,
|
||||
proxyFetchGet,
|
||||
proxyFetchPost,
|
||||
proxyFetchPut,
|
||||
} from '@/api/http';
|
||||
import IntegrationList from '@/components/IntegrationList';
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { getProxyBaseURL } from '@/lib';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MCPAddDialog from './components/MCPAddDialog';
|
||||
import MCPConfigDialog from './components/MCPConfigDialog';
|
||||
import MCPDeleteDialog from './components/MCPDeleteDialog';
|
||||
import MCPList from './components/MCPList';
|
||||
import type { MCPConfigForm, MCPUserItem } from './components/types';
|
||||
import { arrayToArgsJson, parseArgsToArray } from './components/utils';
|
||||
|
||||
import { ConfigFile } from 'electron/main/utils/mcpConfig';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Filter out Search from integrations (Search is now in its own Connectors tab)
|
||||
const EXCLUDED_FROM_MCP = ['Search'];
|
||||
|
||||
export default function SettingMCP() {
|
||||
const { checkAgentTool } = useAuthStore();
|
||||
const { t } = useTranslation();
|
||||
const [items, setItems] = useState<MCPUserItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [showConfig, setShowConfig] = useState<MCPUserItem | null>(null);
|
||||
const [configForm, setConfigForm] = useState<MCPConfigForm | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [addType, setAddType] = useState<'local' | 'remote'>('local');
|
||||
const [localJson, setLocalJson] = useState(
|
||||
`{
|
||||
"mcpServers": {
|
||||
"sequential-thinking": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-sequential-thinking"
|
||||
]
|
||||
}
|
||||
}
|
||||
}`
|
||||
);
|
||||
const [remoteName, setRemoteName] = useState('');
|
||||
const [remoteUrl, setRemoteUrl] = useState('');
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<MCPUserItem | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [switchLoading, setSwitchLoading] = useState<Record<number, boolean>>(
|
||||
{}
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<'mcp-tools' | 'your-mcp'>(
|
||||
'mcp-tools'
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// add: integrations list
|
||||
const [integrations, setIntegrations] = useState<any[]>([]);
|
||||
const [isLoadingIntegrations, setIsLoadingIntegrations] = useState(true);
|
||||
const [refreshKey, setRefreshKey] = useState<number>(0);
|
||||
|
||||
// Filter integrations (MCP & Tools) by search
|
||||
const filteredIntegrations = useMemo(() => {
|
||||
if (!searchQuery.trim()) return integrations;
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
return integrations.filter(
|
||||
(item) =>
|
||||
(item.key || '').toLowerCase().includes(q) ||
|
||||
(item.name || '').toLowerCase().includes(q) ||
|
||||
(item.desc || '').toLowerCase().includes(q)
|
||||
);
|
||||
}, [integrations, searchQuery]);
|
||||
|
||||
// Filter your MCPs by search
|
||||
const filteredItems = useMemo(() => {
|
||||
if (!searchQuery.trim()) return items;
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
return items.filter(
|
||||
(item) =>
|
||||
(item.mcp_name || '').toLowerCase().includes(q) ||
|
||||
(item.mcp_desc || '').toLowerCase().includes(q) ||
|
||||
(item.mcp_key || '').toLowerCase().includes(q)
|
||||
);
|
||||
}, [items, searchQuery]);
|
||||
|
||||
// get list
|
||||
const fetchList = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
proxyFetchGet('/api/mcp/users')
|
||||
.then((res) => {
|
||||
if (Array.isArray(res)) {
|
||||
setItems(res);
|
||||
} else if (Array.isArray(res.items)) {
|
||||
setItems(res.items);
|
||||
} else {
|
||||
setItems([]);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err?.message || t('setting.load-failed'));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
// get integrations
|
||||
useEffect(() => {
|
||||
setIsLoadingIntegrations(true);
|
||||
proxyFetchGet('/api/config/info')
|
||||
.then((res) => {
|
||||
if (res && typeof res === 'object') {
|
||||
const baseURL = getProxyBaseURL();
|
||||
const list = Object.entries(res).map(
|
||||
([key, value]: [string, any]) => {
|
||||
let onInstall = null;
|
||||
|
||||
// Special handling for Notion MCP
|
||||
if (key.toLowerCase() === 'notion') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost('/install/tool/notion');
|
||||
if (response.success) {
|
||||
// Check if there's a warning (connection failed but installation marked as complete)
|
||||
if (response.warning) {
|
||||
toast.warning(response.warning, { duration: 5000 });
|
||||
} else {
|
||||
toast.success(
|
||||
t('setting.notion-mcp-installed-successfully')
|
||||
);
|
||||
}
|
||||
// Save to config to mark as installed
|
||||
await proxyFetchPost('/api/configs', {
|
||||
config_group: 'Notion',
|
||||
config_name: 'MCP_REMOTE_CONFIG_DIR',
|
||||
config_value:
|
||||
response.toolkit_name || 'NotionMCPToolkit',
|
||||
});
|
||||
// Refresh the integrations list to show the installed state
|
||||
fetchList();
|
||||
// Force refresh IntegrationList component
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
} else {
|
||||
toast.error(
|
||||
response.error ||
|
||||
t('setting.failed-to-install-notion-mcp')
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
error.message || t('setting.failed-to-install-notion-mcp')
|
||||
);
|
||||
}
|
||||
};
|
||||
} else if (key.toLowerCase() === 'google calendar') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost(
|
||||
'/install/tool/google_calendar'
|
||||
);
|
||||
if (response.success) {
|
||||
// Check if there's a warning (connection failed but installation marked as complete)
|
||||
if (response.warning) {
|
||||
toast.warning(response.warning, { duration: 5000 });
|
||||
} else {
|
||||
toast.success(
|
||||
t('setting.google-calendar-installed-successfully')
|
||||
);
|
||||
}
|
||||
try {
|
||||
// Ensure we persist a marker config to indicate installation
|
||||
const existingConfigs =
|
||||
await proxyFetchGet('/api/configs');
|
||||
const existing = Array.isArray(existingConfigs)
|
||||
? existingConfigs.find(
|
||||
(c: any) =>
|
||||
c.config_group?.toLowerCase() ===
|
||||
'google calendar' &&
|
||||
c.config_name === 'GOOGLE_REFRESH_TOKEN'
|
||||
)
|
||||
: null;
|
||||
|
||||
const configPayload = {
|
||||
config_group: 'Google Calendar',
|
||||
config_name: 'GOOGLE_REFRESH_TOKEN',
|
||||
config_value: 'exists',
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await proxyFetchPut(
|
||||
`/api/configs/${existing.id}`,
|
||||
configPayload
|
||||
);
|
||||
} else {
|
||||
await proxyFetchPost('/api/configs', configPayload);
|
||||
}
|
||||
} catch (configError) {
|
||||
console.warn(
|
||||
'Failed to persist Google Calendar config',
|
||||
configError
|
||||
);
|
||||
}
|
||||
// Refresh the integrations list to show the installed state
|
||||
fetchList();
|
||||
// Force refresh IntegrationList component
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
} else if (response.status === 'authorizing') {
|
||||
// Authorization in progress - start polling for completion
|
||||
toast.info(
|
||||
t('setting.please-complete-authorization-in-browser')
|
||||
);
|
||||
|
||||
// Poll for authorization completion via oauth status endpoint
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const statusResp = await fetchGet(
|
||||
'/oauth/status/google_calendar'
|
||||
);
|
||||
if (statusResp?.status === 'success') {
|
||||
clearInterval(pollInterval);
|
||||
// Now that auth succeeded, run install again to initialize toolkit
|
||||
const finalize = await fetchPost(
|
||||
'/install/tool/google_calendar'
|
||||
);
|
||||
if (finalize?.success) {
|
||||
const configs =
|
||||
await proxyFetchGet('/api/configs');
|
||||
const existing = Array.isArray(configs)
|
||||
? configs.find(
|
||||
(c: any) =>
|
||||
c.config_group?.toLowerCase() ===
|
||||
'google calendar' &&
|
||||
c.config_name === 'GOOGLE_REFRESH_TOKEN'
|
||||
)
|
||||
: null;
|
||||
|
||||
const payload = {
|
||||
config_group: 'Google Calendar',
|
||||
config_name: 'GOOGLE_REFRESH_TOKEN',
|
||||
config_value: 'exists',
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await proxyFetchPut(
|
||||
`/api/configs/${existing.id}`,
|
||||
payload
|
||||
);
|
||||
} else {
|
||||
await proxyFetchPost('/api/configs', payload);
|
||||
}
|
||||
|
||||
toast.success(
|
||||
t(
|
||||
'setting.google-calendar-installed-successfully'
|
||||
)
|
||||
);
|
||||
fetchList();
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
}
|
||||
} else if (
|
||||
statusResp?.status === 'failed' ||
|
||||
statusResp?.status === 'cancelled'
|
||||
) {
|
||||
clearInterval(pollInterval);
|
||||
const msg =
|
||||
statusResp?.error ||
|
||||
(statusResp?.status === 'cancelled'
|
||||
? t('setting.authorization-cancelled')
|
||||
: t('setting.authorization-failed'));
|
||||
toast.error(msg);
|
||||
}
|
||||
// if still authorizing, continue polling
|
||||
} catch (err) {
|
||||
console.error('Polling oauth status failed', err);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Safety timeout
|
||||
setTimeout(
|
||||
() => clearInterval(pollInterval),
|
||||
5 * 60 * 1000
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
response.error ||
|
||||
response.message ||
|
||||
t('setting.failed-to-install-google-calendar')
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
error.message ||
|
||||
t('setting.failed-to-install-google-calendar')
|
||||
);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
onInstall = () => {
|
||||
const url = `${baseURL}/api/oauth/${key.toLowerCase()}/login`;
|
||||
// Open in a new window to avoid navigating the app/webview
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
name: key,
|
||||
env_vars: value.env_vars,
|
||||
desc:
|
||||
value.env_vars && value.env_vars.length > 0
|
||||
? `${t(
|
||||
'setting.environmental-variables-required'
|
||||
)}: ${value.env_vars.join(', ')}`
|
||||
: key.toLowerCase() === 'notion'
|
||||
? t('setting.notion-workspace-integration')
|
||||
: key.toLowerCase() === 'google calendar'
|
||||
? t('setting.google-calendar-integration')
|
||||
: '',
|
||||
onInstall,
|
||||
};
|
||||
}
|
||||
);
|
||||
setIntegrations(
|
||||
list.filter((item) => !EXCLUDED_FROM_MCP.includes(item.key))
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingIntegrations(false);
|
||||
});
|
||||
}, [fetchList, t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchList();
|
||||
}, [fetchList]);
|
||||
|
||||
// MCP list switch
|
||||
const handleSwitch = async (id: number, checked: boolean) => {
|
||||
setSwitchLoading((l) => ({ ...l, [id]: true }));
|
||||
try {
|
||||
await proxyFetchPut(`/api/mcp/users/${id}`, { status: checked ? 1 : 2 });
|
||||
fetchList();
|
||||
} finally {
|
||||
setSwitchLoading((l) => ({ ...l, [id]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// config dialog
|
||||
useEffect(() => {
|
||||
if (showConfig) {
|
||||
setConfigForm({
|
||||
mcp_name: showConfig.mcp_name || '',
|
||||
mcp_desc: showConfig.mcp_desc || '',
|
||||
command: showConfig.command || '',
|
||||
argsArr: showConfig.args ? parseArgsToArray(showConfig.args) : [],
|
||||
env: showConfig.env ? { ...showConfig.env } : {},
|
||||
});
|
||||
setErrorMsg(null);
|
||||
} else {
|
||||
setConfigForm(null);
|
||||
setErrorMsg(null);
|
||||
}
|
||||
}, [showConfig]);
|
||||
|
||||
const handleConfigSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!configForm || !showConfig) return;
|
||||
setSaving(true);
|
||||
setErrorMsg(null);
|
||||
try {
|
||||
const mcpData = {
|
||||
mcp_name: configForm.mcp_name,
|
||||
mcp_desc: configForm.mcp_desc,
|
||||
command: configForm.command,
|
||||
args: arrayToArgsJson(configForm.argsArr),
|
||||
env: configForm.env,
|
||||
};
|
||||
await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, mcpData);
|
||||
|
||||
if (window.ipcRenderer) {
|
||||
//Partial payload to empty env {}
|
||||
const payload: any = {
|
||||
description: configForm.mcp_desc,
|
||||
command: configForm.command,
|
||||
args: arrayToArgsJson(configForm.argsArr),
|
||||
};
|
||||
if (configForm.env && Object.keys(configForm.env).length > 0) {
|
||||
payload.env = configForm.env;
|
||||
}
|
||||
window.ipcRenderer.invoke('mcp-update', mcpData.mcp_name, payload);
|
||||
}
|
||||
|
||||
setShowConfig(null);
|
||||
fetchList();
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err?.message || t('setting.save-failed'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
const handleConfigClose = () => {
|
||||
setShowConfig(null);
|
||||
setConfigForm(null);
|
||||
setErrorMsg(null);
|
||||
};
|
||||
const handleConfigSwitch = async (checked: boolean) => {
|
||||
if (!showConfig) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, {
|
||||
status: checked ? 1 : 0,
|
||||
});
|
||||
setShowConfig((prev) =>
|
||||
prev ? { ...prev, status: checked ? 1 : 0 } : prev
|
||||
);
|
||||
fetchList();
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// add MCP dialog
|
||||
const handleInstall = async () => {
|
||||
setInstalling(true);
|
||||
try {
|
||||
if (addType === 'local') {
|
||||
let data: ConfigFile;
|
||||
try {
|
||||
data = JSON.parse(localJson);
|
||||
|
||||
// validate mcpServers structure
|
||||
if (!data.mcpServers || typeof data.mcpServers !== 'object') {
|
||||
throw new Error('Invalid mcpServers');
|
||||
}
|
||||
|
||||
// check for name conflicts with existing items
|
||||
const serverNames = Object.keys(data.mcpServers);
|
||||
const conflict = serverNames.find((name) =>
|
||||
items.some((d) => d.mcp_name === name)
|
||||
);
|
||||
if (conflict) {
|
||||
toast.error(
|
||||
t('setting.mcp-server-already-exists', { name: conflict }),
|
||||
{
|
||||
closeButton: true,
|
||||
}
|
||||
);
|
||||
setInstalling(false);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON:', e);
|
||||
toast.error(t('setting.invalid-json'), { closeButton: true });
|
||||
setInstalling(false);
|
||||
return;
|
||||
}
|
||||
let res = await proxyFetchPost('/api/mcp/import/local', data);
|
||||
if (res.detail) {
|
||||
toast.error(t('setting.invalid-json'), { closeButton: true });
|
||||
setInstalling(false);
|
||||
return;
|
||||
}
|
||||
if (window.ipcRenderer) {
|
||||
const mcpServers = data['mcpServers'];
|
||||
for (const [key, value] of Object.entries(mcpServers)) {
|
||||
await window.ipcRenderer.invoke('mcp-install', key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
setShowAdd(false);
|
||||
setLocalJson(`{
|
||||
"mcpServers": {}
|
||||
}`);
|
||||
setRemoteName('');
|
||||
setRemoteUrl('');
|
||||
fetchList();
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
// delete dialog
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
checkAgentTool(deleteTarget.mcp_name);
|
||||
await proxyFetchDelete(`/api/mcp/users/${deleteTarget.id}`);
|
||||
// notify main process
|
||||
if (window.ipcRenderer) {
|
||||
console.log('deleteTarget', deleteTarget.mcp_key);
|
||||
await window.ipcRenderer.invoke('mcp-remove', deleteTarget.mcp_key);
|
||||
}
|
||||
setDeleteTarget(null);
|
||||
fetchList();
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="m-auto flex h-auto w-full flex-1 flex-col">
|
||||
{/* Header Section */}
|
||||
<div className="px-6 pb-6 pt-8 flex w-full items-center justify-between">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('setting.mcp-and-tools')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="mb-12 gap-6 flex flex-col">
|
||||
<div className="gap-4 rounded-2xl bg-surface-secondary px-6 py-4 flex w-full flex-col items-center justify-between">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as 'mcp-tools' | 'your-mcp')}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="gap-4 border-border-secondary bg-surface-secondary sticky top-[84px] z-10 flex w-full items-center justify-between border-x-0 border-t-0 border-b-[0.5px] border-solid">
|
||||
<TabsList
|
||||
variant="outline"
|
||||
className="h-auto flex-1 justify-start border-0 bg-transparent"
|
||||
>
|
||||
<TabsTrigger
|
||||
value="mcp-tools"
|
||||
className="data-[state=active]:bg-transparent"
|
||||
>
|
||||
{t('setting.mcp-and-tools')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="your-mcp"
|
||||
className="data-[state=active]:bg-transparent"
|
||||
>
|
||||
{t('setting.your-own-mcps')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="gap-2 flex items-center">
|
||||
<SearchInput
|
||||
variant="icon"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={t('setting.search-mcp')}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => setShowAdd(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('setting.add-mcp-server')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="mcp-tools" className="mt-4">
|
||||
{isLoadingIntegrations ? (
|
||||
<div className="gap-4 flex w-full flex-col items-start justify-start">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-2xl bg-surface-tertiary px-6 py-4 relative w-full overflow-hidden"
|
||||
>
|
||||
<div className="gap-xs flex w-full flex-row items-center justify-between">
|
||||
<div className="gap-xs flex flex-row items-center">
|
||||
<div className="mr-2 h-3 w-3 bg-surface-hover-subtle rounded-full" />
|
||||
<div className="h-5 w-32 rounded-md bg-surface-hover-subtle" />
|
||||
<div className="h-4 w-4 rounded bg-surface-hover-subtle" />
|
||||
</div>
|
||||
<div className="h-9 w-20 rounded-lg bg-surface-hover-subtle" />
|
||||
</div>
|
||||
<motion.div
|
||||
className="inset-0 via-white/20 absolute w-1/2 bg-gradient-to-r from-transparent to-transparent"
|
||||
initial={{ x: '-100%' }}
|
||||
animate={{ x: '200%' }}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
duration: 1.5,
|
||||
ease: 'linear',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filteredIntegrations.length === 0 ? (
|
||||
<div className="py-8 text-text-label text-center">
|
||||
{searchQuery.trim()
|
||||
? t('dashboard.no-results')
|
||||
: t('setting.no-mcp-servers')}
|
||||
</div>
|
||||
) : (
|
||||
<IntegrationList
|
||||
key={refreshKey}
|
||||
variant="manage"
|
||||
items={filteredIntegrations}
|
||||
showConfigButton={false}
|
||||
showInstallButton={true}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="your-mcp" className="mt-4">
|
||||
{isLoading && (
|
||||
<div className="py-8 text-text-label text-center">
|
||||
{t('setting.loading')}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="py-8 text-text-error text-center">{error}</div>
|
||||
)}
|
||||
{!isLoading && !error && items.length === 0 && (
|
||||
<div className="gap-4 py-12 flex flex-col items-center justify-center">
|
||||
<p className="text-body-md text-text-label">
|
||||
{t('setting.no-mcp-servers')}
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => setShowAdd(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('setting.add-mcp-server')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!error &&
|
||||
items.length > 0 &&
|
||||
filteredItems.length === 0 && (
|
||||
<div className="py-8 text-text-label text-center">
|
||||
{t('dashboard.no-results')}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && filteredItems.length > 0 && (
|
||||
<MCPList
|
||||
items={filteredItems}
|
||||
onSetting={setShowConfig}
|
||||
onDelete={setDeleteTarget}
|
||||
onSwitch={handleSwitch}
|
||||
switchLoading={switchLoading}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dialogs */}
|
||||
<MCPConfigDialog
|
||||
open={!!showConfig}
|
||||
form={configForm}
|
||||
mcp={showConfig}
|
||||
onChange={setConfigForm as any}
|
||||
onSave={handleConfigSave}
|
||||
onClose={handleConfigClose}
|
||||
loading={saving}
|
||||
errorMsg={errorMsg}
|
||||
onSwitchStatus={handleConfigSwitch}
|
||||
/>
|
||||
<MCPAddDialog
|
||||
open={showAdd}
|
||||
addType={addType}
|
||||
setAddType={setAddType}
|
||||
localJson={localJson}
|
||||
setLocalJson={setLocalJson}
|
||||
remoteName={remoteName}
|
||||
setRemoteName={setRemoteName}
|
||||
remoteUrl={remoteUrl}
|
||||
setRemoteUrl={setRemoteUrl}
|
||||
installing={installing}
|
||||
onClose={() => setShowAdd(false)}
|
||||
onInstall={handleInstall}
|
||||
/>
|
||||
<MCPDeleteDialog
|
||||
open={!!deleteTarget}
|
||||
target={deleteTarget}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onConfirm={handleDelete}
|
||||
loading={deleting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
241
src/pages/Connectors/Search.tsx
Normal file
241
src/pages/Connectors/Search.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { proxyFetchGet, proxyFetchPost } from '@/api/http';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { Eye } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface SearchEngineProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
requiresApiKey: boolean;
|
||||
enabledByDefault?: boolean;
|
||||
recommended?: boolean;
|
||||
fields: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
note?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function buildSearchEngines(
|
||||
modelType: 'cloud' | 'local' | 'custom'
|
||||
): SearchEngineProvider[] {
|
||||
if (modelType === 'custom') {
|
||||
return [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
description:
|
||||
'Connect to Google Custom Search (requires API key and CSE ID).',
|
||||
requiresApiKey: true,
|
||||
fields: [
|
||||
{
|
||||
key: 'GOOGLE_API_KEY',
|
||||
label: 'Google API Key',
|
||||
placeholder: 'Enter your Google API key from Google Cloud Console',
|
||||
note: 'Learn how to get your Google API key → https://developers.google.com/custom-search/v1/overview',
|
||||
},
|
||||
{
|
||||
key: 'SEARCH_ENGINE_ID',
|
||||
label: 'Search Engine ID',
|
||||
placeholder:
|
||||
'Enter the Custom Search Engine ID associated with your API key',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
description:
|
||||
'Google Search integration available. No setup required — enabled by default.',
|
||||
requiresApiKey: false,
|
||||
enabledByDefault: true,
|
||||
recommended: true,
|
||||
fields: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function Search() {
|
||||
const { t } = useTranslation();
|
||||
const { modelType } = useAuthStore();
|
||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||
const [showKeys, setShowKeys] = useState<Record<string, boolean>>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const engines = useMemo(() => buildSearchEngines(modelType), [modelType]);
|
||||
|
||||
useEffect(() => {
|
||||
proxyFetchGet('/api/configs').then((configsRes) => {
|
||||
const configsList = Array.isArray(configsRes) ? configsRes : [];
|
||||
const existingData: Record<string, string> = {};
|
||||
engines.forEach((engine) => {
|
||||
engine.fields.forEach((field) => {
|
||||
const config = configsList.find(
|
||||
(c: any) => c.config_name === field.key
|
||||
);
|
||||
if (config) {
|
||||
existingData[field.key] = config.config_value || '';
|
||||
}
|
||||
});
|
||||
});
|
||||
setFormData(existingData);
|
||||
});
|
||||
}, [engines]);
|
||||
|
||||
const selectedProvider = engines[0];
|
||||
|
||||
const handleFieldChange = (fieldKey: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [fieldKey]: value }));
|
||||
};
|
||||
|
||||
const saveConfiguration = async () => {
|
||||
if (selectedProvider.enabledByDefault) {
|
||||
toast.info(t('setting.this-service-is-already-enabled-by-default'));
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
if (selectedProvider.requiresApiKey) {
|
||||
for (const field of selectedProvider.fields) {
|
||||
const value = formData[field.key];
|
||||
if (value && value.trim() !== '') {
|
||||
await proxyFetchPost('/api/configs', {
|
||||
config_group: 'Search',
|
||||
config_name: field.key,
|
||||
config_value: value.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await proxyFetchPost('/api/configs', {
|
||||
config_group: 'Search',
|
||||
config_name: `ENABLE_${selectedProvider.id.toUpperCase()}_SEARCH`,
|
||||
config_value: 'true',
|
||||
});
|
||||
}
|
||||
toast.success(t('setting.configuration-saved-successfully'));
|
||||
const res = await proxyFetchGet('/api/configs');
|
||||
const configsList = Array.isArray(res) ? res : [];
|
||||
const existingData: Record<string, string> = {};
|
||||
engines.forEach((engine) => {
|
||||
engine.fields.forEach((field) => {
|
||||
const config = configsList.find(
|
||||
(c: any) => c.config_name === field.key
|
||||
);
|
||||
if (config) {
|
||||
existingData[field.key] = config.config_value || '';
|
||||
}
|
||||
});
|
||||
});
|
||||
setFormData(existingData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(t('setting.failed-to-save-configuration'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="m-auto flex h-auto w-full flex-1 flex-col">
|
||||
{/* Header Section */}
|
||||
<div className="px-6 pb-6 pt-8 flex w-full items-center justify-between">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('setting.search-engine')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section - Google configuration */}
|
||||
<div className="mb-12">
|
||||
<div className="rounded-2xl bg-surface-secondary px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<div className="gap-2 pb-2 flex flex-col">
|
||||
<div className="text-label-lg font-bold">
|
||||
{selectedProvider.name}
|
||||
</div>
|
||||
<div className="text-label-sm font-normal text-text-label">
|
||||
{selectedProvider.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex-1">
|
||||
{selectedProvider.requiresApiKey ? (
|
||||
<div className="space-y-4">
|
||||
{selectedProvider.fields.map((field) => (
|
||||
<div key={field.key}>
|
||||
<Input
|
||||
id={field.key}
|
||||
size="default"
|
||||
title={field.label}
|
||||
type={showKeys[field.key] ? 'text' : 'password'}
|
||||
placeholder={field.placeholder}
|
||||
value={formData[field.key] || ''}
|
||||
onChange={(e) =>
|
||||
handleFieldChange(field.key, e.target.value)
|
||||
}
|
||||
note={field.note}
|
||||
className="mt-1"
|
||||
backIcon={<Eye className="h-5 w-5" />}
|
||||
onBackIconClick={() =>
|
||||
setShowKeys((prev) => ({
|
||||
...prev,
|
||||
[field.key]: !prev[field.key],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg bg-surface-primary p-4">
|
||||
<p className="text-label-sm text-text-label">
|
||||
{selectedProvider.id === 'wiki'
|
||||
? t(
|
||||
'setting.this-service-is-public-and-does-not-require-credentials'
|
||||
)
|
||||
: t('setting.this-service-does-not-require-an-api-key')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!selectedProvider.enabledByDefault && (
|
||||
<div className="gap-3 pt-4 flex items-center justify-end">
|
||||
<Button size="sm" onClick={saveConfiguration} disabled={saving}>
|
||||
{saving
|
||||
? t('setting.saving')
|
||||
: selectedProvider.requiresApiKey
|
||||
? t('setting.save-changes')
|
||||
: `${t('setting.enable')} ${selectedProvider.name} ${t('setting.search')}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -116,7 +116,7 @@ export default function CookieManager() {
|
|||
<div className="rounded-2xl bg-surface-secondary px-6 py-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-base font-bold leading-12 text-text-primary">
|
||||
<div className="gap-2 text-base font-bold leading-12 text-text-primary flex items-center">
|
||||
<Cookie className="h-5 w-5" />
|
||||
{t('setting.cookie-manager')}
|
||||
</div>
|
||||
|
|
@ -124,7 +124,7 @@ export default function CookieManager() {
|
|||
{t('setting.cookie-manager-description')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="gap-2 flex">
|
||||
<Button
|
||||
onClick={loadCookies}
|
||||
variant="outline"
|
||||
|
|
@ -152,8 +152,8 @@ export default function CookieManager() {
|
|||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform text-text-tertiary" />
|
||||
<div className="mb-4 relative">
|
||||
<Search className="left-3 h-4 w-4 text-text-tertiary absolute top-1/2 -translate-y-1/2 transform" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t('setting.search-domains')}
|
||||
|
|
@ -166,13 +166,13 @@ export default function CookieManager() {
|
|||
{/* Cookie List */}
|
||||
<div className="space-y-2">
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center text-text-secondary">
|
||||
<RefreshCw className="mx-auto mb-2 h-6 w-6 animate-spin" />
|
||||
<div className="py-8 text-text-secondary text-center">
|
||||
<RefreshCw className="mb-2 h-6 w-6 animate-spin mx-auto" />
|
||||
{t('setting.loading-cookies')}
|
||||
</div>
|
||||
) : filteredDomains.length === 0 ? (
|
||||
<div className="py-8 text-center text-text-secondary">
|
||||
<Cookie className="mx-auto mb-3 h-12 w-12 opacity-30" />
|
||||
<div className="py-8 text-text-secondary text-center">
|
||||
<Cookie className="mb-3 h-12 w-12 mx-auto opacity-30" />
|
||||
<div className="mb-1 text-base font-medium">
|
||||
{domains.length === 0
|
||||
? t('setting.no-cookies-found')
|
||||
|
|
@ -188,13 +188,13 @@ export default function CookieManager() {
|
|||
filteredDomains.map((item) => (
|
||||
<div
|
||||
key={item.domain}
|
||||
className="flex items-center justify-between rounded-lg border border-border-primary bg-surface-primary p-3 transition-colors hover:border-border-secondary"
|
||||
className="rounded-lg border-border-primary bg-surface-primary p-3 hover:border-border-secondary flex items-center justify-between border transition-colors"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-text-primary">
|
||||
<div className="font-medium text-text-primary truncate">
|
||||
{item.domain}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-3 text-xs text-text-tertiary">
|
||||
<div className="mt-1 gap-3 text-xs text-text-tertiary flex items-center">
|
||||
<span>
|
||||
{t('setting.cookies-count', { count: item.cookie_count })}
|
||||
</span>
|
||||
|
|
@ -222,9 +222,9 @@ export default function CookieManager() {
|
|||
|
||||
{/* Warning */}
|
||||
{domains.length > 0 && (
|
||||
<div className="dark:bg-yellow-900/20 mt-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-600 dark:text-yellow-500" />
|
||||
<div className="dark:bg-yellow-900/20 mt-4 rounded-lg border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 border">
|
||||
<div className="gap-2 flex items-start">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 text-yellow-600 dark:text-yellow-500 flex-shrink-0" />
|
||||
<div className="text-xs text-yellow-800 dark:text-yellow-200">
|
||||
{t('setting.cookie-delete-warning')}
|
||||
</div>
|
||||
|
|
@ -128,7 +128,7 @@ export default function MCPAddDialog({
|
|||
{jsonError}
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-hidden rounded-xl border border-border-primary">
|
||||
<div className="rounded-xl border-border-primary overflow-hidden border">
|
||||
<MonacoEditor
|
||||
height="300px"
|
||||
width="100%"
|
||||
|
|
@ -64,7 +64,7 @@ export default function MCPConfigDialog({
|
|||
<form
|
||||
ref={formRef}
|
||||
onSubmit={onSave}
|
||||
className="flex flex-col gap-4 p-md"
|
||||
className="gap-4 p-md flex flex-col"
|
||||
>
|
||||
<Input
|
||||
title={t('setting.name')}
|
||||
|
|
@ -104,7 +104,7 @@ export default function MCPConfigDialog({
|
|||
)}
|
||||
/>
|
||||
|
||||
<div className="mb-1 block text-label-sm font-normal">
|
||||
<div className="mb-1 text-label-sm font-normal block">
|
||||
Env (key-value)
|
||||
</div>
|
||||
{Object.entries(form.env).map(([k, v], idx) => (
|
||||
|
|
@ -33,8 +33,8 @@ export default function MCPDeleteDialog({
|
|||
const { t } = useTranslation();
|
||||
if (!open || !target) return null;
|
||||
return (
|
||||
<div className="bg-black/30 fixed inset-0 z-30 flex items-center justify-center">
|
||||
<div className="min-w-[320px] max-w-[90vw] rounded-lg bg-white-100% p-6 shadow-lg">
|
||||
<div className="bg-black/30 inset-0 fixed z-30 flex items-center justify-center">
|
||||
<div className="rounded-lg bg-white-100% p-6 shadow-lg max-w-[90vw] min-w-[320px]">
|
||||
<div className="mb-2 font-bold text-red-600">
|
||||
{t('setting.confirm-delete')}
|
||||
</div>
|
||||
|
|
@ -42,7 +42,7 @@ export default function MCPDeleteDialog({
|
|||
{t('setting.are-you-sure-you-want-to-delete')}{' '}
|
||||
<b>{target.mcp_name}</b>?
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="gap-2 flex justify-end">
|
||||
<Button variant="outline" onClick={onCancel} disabled={loading}>
|
||||
{t('setting.cancel')}
|
||||
</Button>
|
||||
|
|
@ -307,8 +307,8 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
})}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3 p-md">
|
||||
<div className="flex items-center gap-md">
|
||||
<div className="gap-3 p-md flex flex-col">
|
||||
<div className="gap-md flex items-center">
|
||||
{getCategoryIcon(activeMcp?.category?.name)}
|
||||
<div>
|
||||
<div className="text-base font-bold leading-9 text-text-action">
|
||||
|
|
@ -328,7 +328,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
<span className="line-clamp-1 items-center justify-center self-stretch overflow-hidden text-ellipsis break-words text-xs font-medium leading-normal">
|
||||
<span className="text-xs font-medium leading-normal line-clamp-1 items-center justify-center self-stretch overflow-hidden break-words text-ellipsis">
|
||||
{getGithubRepoName(activeMcp?.home_page)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -336,7 +336,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-md">
|
||||
<div className="gap-md flex flex-col">
|
||||
{Object.keys(activeMcp?.install_command?.env || {}).map((key) => {
|
||||
const getNoteContent = () => {
|
||||
let noteContent = envValues[key]?.tip || '';
|
||||
|
|
@ -37,9 +37,9 @@ export default function MCPListItem({
|
|||
const [_showMenu, setShowMenu] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-between gap-4 rounded-2xl bg-surface-secondary p-4">
|
||||
<div className="flex items-center gap-xs">
|
||||
<div className="mx-xs h-3 w-3 rounded-full bg-green-500"></div>
|
||||
<div className="mb-4 gap-4 rounded-2xl bg-surface-tertiary p-4 flex items-center justify-between">
|
||||
<div className="gap-xs flex items-center">
|
||||
<div className="mx-xs h-3 w-3 bg-green-500 rounded-full"></div>
|
||||
<div className="text-base font-bold leading-9 text-text-primary">
|
||||
{item.mcp_name}
|
||||
</div>
|
||||
|
|
@ -49,7 +49,7 @@ export default function MCPListItem({
|
|||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="gap-2 flex items-center">
|
||||
{/* <Switch
|
||||
checked={item.status === 1}
|
||||
disabled={loading}
|
||||
|
|
@ -26,7 +26,7 @@ import { useAuthStore } from '@/store/authStore';
|
|||
import { ChevronLeft, CircleAlert, Store } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MCPEnvDialog } from './components/MCPEnvDialog';
|
||||
import { MCPEnvDialog } from './MCPEnvDialog';
|
||||
interface MCPItem {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -273,7 +273,7 @@ export default function MCPMarket({
|
|||
<div className="flex h-full flex-col items-center">
|
||||
{externalKeyword === undefined && (
|
||||
<>
|
||||
<div className="text-body sticky top-0 z-[20] mb-0 flex w-full max-w-4xl items-center justify-between py-2">
|
||||
<div className="text-body top-0 mb-0 max-w-4xl py-2 sticky z-[20] flex w-full items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -296,7 +296,7 @@ export default function MCPMarket({
|
|||
)}
|
||||
|
||||
{/* Category toggle row */}
|
||||
<div className="flex w-full py-2">
|
||||
<div className="py-2 flex w-full">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={categoryId ? String(categoryId) : 'all'}
|
||||
|
|
@ -321,24 +321,24 @@ export default function MCPMarket({
|
|||
onConnect={onConnect}
|
||||
activeMcp={activeMcp}
|
||||
></MCPEnvDialog>
|
||||
<div className="flex w-full flex-col gap-4 pt-4">
|
||||
<div className="gap-4 pt-4 flex w-full flex-col">
|
||||
{isLoading && items.length === 0 && (
|
||||
<div className="py-8 text-center text-text-muted">
|
||||
<div className="py-8 text-text-muted text-center">
|
||||
{t('setting.loading')}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="py-8 text-center text-text-error">{error}</div>
|
||||
<div className="py-8 text-text-error text-center">{error}</div>
|
||||
)}
|
||||
{!isLoading && !error && items.length === 0 && (
|
||||
<div className="py-8 text-center text-text-muted">
|
||||
<div className="py-8 text-text-muted text-center">
|
||||
{t('setting.no-mcp-services')}
|
||||
</div>
|
||||
)}
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center rounded-2xl bg-surface-secondary p-4"
|
||||
className="rounded-2xl bg-surface-secondary p-4 flex items-center"
|
||||
>
|
||||
{/* Left: Icon */}
|
||||
<div className="mr-4 flex items-center">
|
||||
|
|
@ -356,10 +356,10 @@ export default function MCPMarket({
|
|||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-center">
|
||||
<div className="flex w-full items-center gap-xs pb-1">
|
||||
<div className="flex flex-1 items-center gap-xs">
|
||||
<span className="truncate text-base font-bold leading-9 text-text-primary">
|
||||
<div className="min-w-0 flex flex-1 flex-col justify-center">
|
||||
<div className="gap-xs pb-1 flex w-full items-center">
|
||||
<div className="gap-xs flex flex-1 items-center">
|
||||
<span className="text-base font-bold leading-9 text-text-primary truncate">
|
||||
{item.name}
|
||||
</span>
|
||||
<TooltipSimple content={item.description}>
|
||||
|
|
@ -400,7 +400,7 @@ export default function MCPMarket({
|
|||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
<span className="items-center justify-center self-stretch text-xs font-medium leading-3">
|
||||
<span className="text-xs font-medium leading-3 items-center justify-center self-stretch">
|
||||
{(() => {
|
||||
const parts = item.home_page.split('/');
|
||||
return parts.length > 4 ? parts[4] : item.home_page;
|
||||
|
|
@ -408,7 +408,7 @@ export default function MCPMarket({
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 whitespace-pre-line break-words text-sm text-text-muted">
|
||||
<div className="mt-1 text-sm text-text-muted break-words whitespace-pre-line">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -416,12 +416,12 @@ export default function MCPMarket({
|
|||
))}
|
||||
<div ref={loader} />
|
||||
{isLoading && items.length > 0 && (
|
||||
<div className="py-4 text-center text-text-muted">
|
||||
<div className="py-4 text-text-muted text-center">
|
||||
{t('setting.loading-more')}
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && items.length > 0 && (
|
||||
<div className="py-4 text-center text-text-muted">
|
||||
<div className="py-4 text-text-muted text-center">
|
||||
{t('setting.no-more-mcp-servers')}
|
||||
</div>
|
||||
)}
|
||||
72
src/pages/Connectors/index.tsx
Normal file
72
src/pages/Connectors/index.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import VerticalNavigation, {
|
||||
type VerticalNavItem,
|
||||
} from '@/components/Navigation';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MCP from './MCP';
|
||||
import Search from './Search';
|
||||
|
||||
export default function Connectors() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('mcp');
|
||||
|
||||
const menuItems: Array<{ id: string; name: string }> = [
|
||||
{
|
||||
id: 'search',
|
||||
name: t('layout.search'),
|
||||
},
|
||||
{
|
||||
id: 'mcp',
|
||||
name: t('setting.mcp'),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
setActiveTab(tabId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="m-auto flex h-auto max-w-[940px] flex-col">
|
||||
<div className="px-6 flex h-auto w-full">
|
||||
<div className="top-20 w-40 pr-6 pt-8 sticky flex h-full flex-shrink-0 flex-grow-0 flex-col justify-between self-start">
|
||||
<VerticalNavigation
|
||||
items={
|
||||
menuItems.map((menu) => ({
|
||||
value: menu.id,
|
||||
label: (
|
||||
<span className="text-body-sm font-bold">{menu.name}</span>
|
||||
),
|
||||
})) as VerticalNavItem[]
|
||||
}
|
||||
value={activeTab}
|
||||
onValueChange={handleTabChange}
|
||||
className="min-h-0 gap-0 h-full w-full flex-1"
|
||||
listClassName="w-full h-full overflow-y-auto"
|
||||
contentClassName="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-auto w-full flex-1 flex-col">
|
||||
<div className="gap-4 flex flex-col">
|
||||
{activeTab === 'search' && <Search />}
|
||||
{activeTab === 'mcp' && <MCP />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,722 +0,0 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { fetchDelete, fetchGet, fetchPost } from '@/api/http';
|
||||
import VerticalNavigation from '@/components/Navigation';
|
||||
import AlertDialog from '@/components/ui/alertDialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Cookie,
|
||||
Globe,
|
||||
Link2,
|
||||
Loader2,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CookieDomain {
|
||||
domain: string;
|
||||
cookie_count: number;
|
||||
last_access: string;
|
||||
}
|
||||
|
||||
interface GroupedDomain {
|
||||
mainDomain: string;
|
||||
subdomains: CookieDomain[];
|
||||
totalCookies: number;
|
||||
}
|
||||
|
||||
interface CdpBrowser {
|
||||
id: string;
|
||||
port: number;
|
||||
isExternal: boolean;
|
||||
name?: string;
|
||||
addedAt: number;
|
||||
}
|
||||
|
||||
export default function Browser() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('connection');
|
||||
const [loginLoading, setLoginLoading] = useState(false);
|
||||
const [cookiesLoading, setCookiesLoading] = useState(false);
|
||||
const [cookieDomains, setCookieDomains] = useState<CookieDomain[]>([]);
|
||||
const [deletingDomain, setDeletingDomain] = useState<string | null>(null);
|
||||
const [deletingAll, setDeletingAll] = useState(false);
|
||||
const [showRestartDialog, setShowRestartDialog] = useState(false);
|
||||
const [showCookieRestartDialog, setShowCookieRestartDialog] = useState(false);
|
||||
|
||||
// CDP port configuration
|
||||
const [cdpPort, setCdpPort] = useState<number>(9223);
|
||||
|
||||
// CDP Browser Pool
|
||||
const [cdpBrowsers, setCdpBrowsers] = useState<CdpBrowser[]>([]);
|
||||
const [deletingBrowser, setDeletingBrowser] = useState<string | null>(null);
|
||||
const [browserToRemove, setBrowserToRemove] = useState<CdpBrowser | null>(
|
||||
null
|
||||
);
|
||||
|
||||
// Connect Existing Browser dialog
|
||||
const [showConnectDialog, setShowConnectDialog] = useState(false);
|
||||
const [connectPort, setConnectPort] = useState('');
|
||||
const [connectChecking, setConnectChecking] = useState(false);
|
||||
const [connectError, setConnectError] = useState('');
|
||||
|
||||
// Extract main domain (e.g., "aa.bb.cc" -> "bb.cc", "www.google.com" -> "google.com")
|
||||
const getMainDomain = (domain: string): string => {
|
||||
// Remove leading dot if present
|
||||
const cleanDomain = domain.startsWith('.') ? domain.substring(1) : domain;
|
||||
const parts = cleanDomain.split('.');
|
||||
|
||||
// For domains with 2 or fewer parts, return as is
|
||||
if (parts.length <= 2) {
|
||||
return cleanDomain;
|
||||
}
|
||||
|
||||
// For domains with more parts, return last 2 parts (main domain)
|
||||
return parts.slice(-2).join('.');
|
||||
};
|
||||
|
||||
// Group domains by main domain
|
||||
const groupDomainsByMain = (domains: CookieDomain[]): GroupedDomain[] => {
|
||||
const grouped = new Map<string, CookieDomain[]>();
|
||||
|
||||
domains.forEach((item) => {
|
||||
const mainDomain = getMainDomain(item.domain);
|
||||
if (!grouped.has(mainDomain)) {
|
||||
grouped.set(mainDomain, []);
|
||||
}
|
||||
grouped.get(mainDomain)!.push(item);
|
||||
});
|
||||
|
||||
return Array.from(grouped.entries())
|
||||
.map(([mainDomain, subdomains]) => ({
|
||||
mainDomain,
|
||||
subdomains,
|
||||
totalCookies: subdomains.reduce(
|
||||
(sum, item) => sum + item.cookie_count,
|
||||
0
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.mainDomain.localeCompare(b.mainDomain));
|
||||
};
|
||||
|
||||
// Auto-load cookies on component mount
|
||||
useEffect(() => {
|
||||
handleLoadCookies();
|
||||
// Load current browser port on mount
|
||||
loadCurrentBrowserPort();
|
||||
// Load CDP browser pool
|
||||
loadCdpBrowsers();
|
||||
}, []);
|
||||
|
||||
// Listen for CDP pool push updates from main process (health-check removes dead browsers)
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI?.onCdpPoolChanged) return;
|
||||
const cleanup = window.electronAPI.onCdpPoolChanged(
|
||||
(browsers: CdpBrowser[]) => {
|
||||
setCdpBrowsers(browsers);
|
||||
}
|
||||
);
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
const loadCurrentBrowserPort = async () => {
|
||||
if (window.electronAPI?.getBrowserPort) {
|
||||
const port = await window.electronAPI.getBrowserPort();
|
||||
setCdpPort(port);
|
||||
}
|
||||
};
|
||||
|
||||
const loadCdpBrowsers = async () => {
|
||||
if (window.electronAPI?.getCdpBrowsers) {
|
||||
try {
|
||||
const browsers = await window.electronAPI.getCdpBrowsers();
|
||||
setCdpBrowsers(browsers);
|
||||
} catch (error) {
|
||||
console.error('Failed to load CDP browsers:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveBrowser = async (browserId: string) => {
|
||||
setDeletingBrowser(browserId);
|
||||
try {
|
||||
if (window.electronAPI?.removeCdpBrowser) {
|
||||
const result = await window.electronAPI.removeCdpBrowser(browserId);
|
||||
if (result.success) {
|
||||
toast.success(t('layout.browser-removed'));
|
||||
} else {
|
||||
toast.error(result.error || t('layout.failed-to-remove-browser'));
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || t('layout.failed-to-remove-browser'));
|
||||
} finally {
|
||||
setDeletingBrowser(null);
|
||||
setBrowserToRemove(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenNewBrowser = async () => {
|
||||
try {
|
||||
toast.loading(t('layout.launching-browser', { port: '...' }), {
|
||||
id: 'launch-browser',
|
||||
});
|
||||
|
||||
const result = await window.electronAPI?.launchCdpBrowser();
|
||||
|
||||
if (result?.success) {
|
||||
toast.success(t('layout.browser-launched', { port: result.port }), {
|
||||
id: 'launch-browser',
|
||||
});
|
||||
} else {
|
||||
toast.error(result?.error || t('layout.failed-to-launch-browser'), {
|
||||
id: 'launch-browser',
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || t('layout.failed-to-launch-browser'), {
|
||||
id: 'launch-browser',
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleConnectExistingBrowser = () => {
|
||||
setConnectPort('');
|
||||
setConnectError('');
|
||||
setShowConnectDialog(true);
|
||||
};
|
||||
|
||||
const handleCheckAndConnect = async () => {
|
||||
const portNum = parseInt(connectPort, 10);
|
||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
||||
setConnectError(t('layout.invalid-port'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if port is already in the pool
|
||||
if (cdpBrowsers.some((b) => b.port === portNum)) {
|
||||
setConnectError(t('layout.port-already-in-use'));
|
||||
return;
|
||||
}
|
||||
|
||||
setConnectChecking(true);
|
||||
setConnectError('');
|
||||
|
||||
try {
|
||||
// Probe the port to check if a CDP browser is listening
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
const response = await fetch(`http://localhost:${portNum}/json/version`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
setConnectError(t('layout.no-browser-on-port', { port: portNum }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Port is alive — add to CDP pool
|
||||
if (window.electronAPI?.addCdpBrowser) {
|
||||
const addResult = await window.electronAPI.addCdpBrowser(
|
||||
portNum,
|
||||
true,
|
||||
`External Browser (${portNum})`
|
||||
);
|
||||
if (!addResult?.success) {
|
||||
setConnectError(
|
||||
addResult?.error || t('layout.failed-to-add-browser')
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setConnectError(t('layout.failed-to-add-browser'));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(t('layout.connected-browser', { port: portNum }));
|
||||
setShowConnectDialog(false);
|
||||
} catch {
|
||||
setConnectError(t('layout.no-browser-on-port', { port: portNum }));
|
||||
} finally {
|
||||
setConnectChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBrowserLogin = async () => {
|
||||
setLoginLoading(true);
|
||||
const currentCookieCount = cookieDomains.reduce(
|
||||
(sum, item) => sum + item.cookie_count,
|
||||
0
|
||||
);
|
||||
try {
|
||||
const response = await fetchPost('/browser/login');
|
||||
if (response) {
|
||||
toast.success(t('layout.browser-opened'));
|
||||
// Listen for browser close event to reload cookies
|
||||
const checkInterval = setInterval(async () => {
|
||||
try {
|
||||
// Check if browser is still open by making a request
|
||||
// When browser closes, reload cookies
|
||||
const statusResponse = await fetchGet('/browser/status');
|
||||
if (!statusResponse || !statusResponse.is_open) {
|
||||
clearInterval(checkInterval);
|
||||
await handleLoadCookies();
|
||||
// Check if cookies changed
|
||||
const newResponse = await fetchGet('/browser/cookies');
|
||||
if (newResponse && newResponse.success) {
|
||||
const newDomains = newResponse.domains || [];
|
||||
const newCookieCount = newDomains.reduce(
|
||||
(sum: number, item: CookieDomain) => sum + item.cookie_count,
|
||||
0
|
||||
);
|
||||
|
||||
if (newCookieCount > currentCookieCount) {
|
||||
// Cookies were added, show success toast and restart dialog
|
||||
const addedCount = newCookieCount - currentCookieCount;
|
||||
toast.success(
|
||||
t('layout.cookies-added', { count: addedCount })
|
||||
);
|
||||
setShowRestartDialog(true);
|
||||
} else if (newCookieCount < currentCookieCount) {
|
||||
setShowRestartDialog(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Browser might be closed
|
||||
clearInterval(checkInterval);
|
||||
await handleLoadCookies();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || t('layout.failed-to-open-browser'));
|
||||
} finally {
|
||||
setLoginLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadCookies = async () => {
|
||||
setCookiesLoading(true);
|
||||
try {
|
||||
const response = await fetchGet('/browser/cookies');
|
||||
if (response && response.success) {
|
||||
const domains = response.domains || [];
|
||||
setCookieDomains(domains);
|
||||
} else {
|
||||
setCookieDomains([]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || t('layout.failed-to-load-cookies'));
|
||||
setCookieDomains([]);
|
||||
} finally {
|
||||
setCookiesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteMainDomain = async (
|
||||
mainDomain: string,
|
||||
subdomains: CookieDomain[]
|
||||
) => {
|
||||
setDeletingDomain(mainDomain);
|
||||
try {
|
||||
// Delete all subdomains under this main domain
|
||||
const deletePromises = subdomains.map((item) =>
|
||||
fetchDelete(`/browser/cookies/${encodeURIComponent(item.domain)}`)
|
||||
);
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
toast.success(
|
||||
t('layout.deleted-cookies-for-domain', { domain: mainDomain })
|
||||
);
|
||||
// Remove from local state
|
||||
const domainsToRemove = new Set(subdomains.map((item) => item.domain));
|
||||
setCookieDomains((prev) =>
|
||||
prev.filter((item) => !domainsToRemove.has(item.domain))
|
||||
);
|
||||
|
||||
// Show restart dialog after successful deletion
|
||||
setShowRestartDialog(true);
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
error?.message ||
|
||||
t('layout.failed-to-delete-cookies-for-domain', {
|
||||
domain: mainDomain,
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setDeletingDomain(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAll = async () => {
|
||||
setDeletingAll(true);
|
||||
try {
|
||||
await fetchDelete('/browser/cookies');
|
||||
toast.success(t('layout.deleted-all-cookies'));
|
||||
setCookieDomains([]);
|
||||
|
||||
// Show restart dialog after successful deletion
|
||||
setShowRestartDialog(true);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || t('layout.failed-to-delete-all-cookies'));
|
||||
} finally {
|
||||
setDeletingAll(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartApp = () => {
|
||||
if (window.electronAPI && window.electronAPI.restartApp) {
|
||||
window.electronAPI.restartApp();
|
||||
} else {
|
||||
toast.error(t('layout.restart-not-available'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmRestart = () => {
|
||||
setShowRestartDialog(false);
|
||||
handleRestartApp();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="m-auto flex h-auto max-w-[940px] flex-col">
|
||||
{/* Restart Dialog */}
|
||||
<AlertDialog
|
||||
isOpen={showRestartDialog}
|
||||
onClose={() => setShowRestartDialog(false)}
|
||||
onConfirm={handleConfirmRestart}
|
||||
title={t('layout.cookies-updated')}
|
||||
message={t('layout.cookies-updated-message')}
|
||||
confirmText={t('layout.yes-restart')}
|
||||
cancelText={t('layout.no-add-more')}
|
||||
confirmVariant="information"
|
||||
/>
|
||||
|
||||
{/* Cookie Restart Confirm Dialog */}
|
||||
<AlertDialog
|
||||
isOpen={showCookieRestartDialog}
|
||||
onClose={() => setShowCookieRestartDialog(false)}
|
||||
onConfirm={() => {
|
||||
setShowCookieRestartDialog(false);
|
||||
handleRestartApp();
|
||||
}}
|
||||
title={t('layout.restart-required')}
|
||||
message={t('layout.restart-required-message')}
|
||||
confirmText={t('layout.restart')}
|
||||
cancelText={t('layout.cancel')}
|
||||
confirmVariant="information"
|
||||
/>
|
||||
|
||||
{/* Remove Browser Confirmation Dialog */}
|
||||
<AlertDialog
|
||||
isOpen={!!browserToRemove}
|
||||
onClose={() => setBrowserToRemove(null)}
|
||||
onConfirm={() => {
|
||||
if (browserToRemove) {
|
||||
handleRemoveBrowser(browserToRemove.id);
|
||||
}
|
||||
}}
|
||||
title={t('layout.remove-browser')}
|
||||
message={t('layout.remove-browser-confirm', {
|
||||
name: browserToRemove?.name || `Browser ${browserToRemove?.port}`,
|
||||
port: browserToRemove?.port,
|
||||
})}
|
||||
confirmText={t('layout.remove')}
|
||||
cancelText={t('layout.cancel')}
|
||||
confirmVariant="cuation"
|
||||
/>
|
||||
|
||||
{/* Connect Existing Browser Dialog */}
|
||||
{showConnectDialog && (
|
||||
<div className="bg-black/50 fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="w-full max-w-md rounded-xl bg-surface-primary p-6 shadow-lg">
|
||||
<div className="text-body-base mb-2 font-bold text-text-heading">
|
||||
{t('layout.connect-existing-browser')}
|
||||
</div>
|
||||
<p className="mb-4 text-label-xs text-text-label">
|
||||
{t('layout.connect-existing-browser-description')}
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={connectPort}
|
||||
onChange={(e) => {
|
||||
setConnectPort(e.target.value);
|
||||
setConnectError('');
|
||||
}}
|
||||
placeholder={t('layout.enter-port-number')}
|
||||
className="w-full rounded-lg border border-border-disabled bg-surface-secondary px-4 py-2 text-body-sm text-text-body outline-none focus:border-border-focus"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleCheckAndConnect();
|
||||
}}
|
||||
/>
|
||||
{connectError && (
|
||||
<p className="mt-2 text-label-xs text-text-cuation">
|
||||
{connectError}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowConnectDialog(false)}
|
||||
>
|
||||
{t('layout.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleCheckAndConnect}
|
||||
disabled={connectChecking}
|
||||
>
|
||||
{connectChecking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Link2 className="h-4 w-4" />
|
||||
)}
|
||||
{t('layout.check-and-connect')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex h-auto w-full px-6">
|
||||
{/* Left Sidebar */}
|
||||
<div className="sticky top-20 flex h-full w-48 flex-shrink-0 flex-grow-0 flex-col self-start pr-6 pt-8">
|
||||
<VerticalNavigation
|
||||
items={[
|
||||
{
|
||||
value: 'connection',
|
||||
label: (
|
||||
<span className="text-body-sm font-bold">
|
||||
{t('layout.browser-connection')}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'cookies',
|
||||
label: (
|
||||
<span className="text-body-sm font-bold">
|
||||
{t('layout.cookies-management')}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="h-full min-h-0 w-full flex-1 gap-0"
|
||||
listClassName="w-full h-full overflow-y-auto"
|
||||
contentClassName="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Content */}
|
||||
<div className="flex h-auto w-full flex-1 flex-col pb-8 pt-8">
|
||||
{activeTab === 'connection' && (
|
||||
<div className="flex flex-col">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('layout.cdp-browser-connection')}
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleOpenNewBrowser}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('layout.open-new-browser')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleConnectExistingBrowser}
|
||||
>
|
||||
<Link2 className="h-4 w-4 text-button-tertiery-text-default" />
|
||||
{t('layout.connect-existing-browser')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* CDP Browser Pool */}
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center justify-start gap-2">
|
||||
<div className="text-body-base font-bold text-text-body">
|
||||
{t('layout.cdp-browser-pool')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cdpBrowsers.length > 0 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{cdpBrowsers.map((browser) => (
|
||||
<div
|
||||
key={browser.id}
|
||||
className="flex items-center justify-between rounded-xl border-solid border-border-disabled bg-surface-secondary px-4 py-2"
|
||||
>
|
||||
<div className="flex w-full flex-row items-center gap-2">
|
||||
<div className="h-2 w-2 shrink-0 rounded-full bg-text-success" />
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<span className="text-body-sm font-bold text-text-body">
|
||||
{browser.name || `Browser ${browser.port}`}
|
||||
</span>
|
||||
<span className="text-label-xs text-text-label">
|
||||
{t('layout.port')} {browser.port}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setBrowserToRemove(browser)}
|
||||
disabled={deletingBrowser === browser.id}
|
||||
className="ml-3 flex-shrink-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-text-cuation" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-4 py-8">
|
||||
<Globe className="mb-4 h-12 w-12 text-icon-secondary opacity-50" />
|
||||
<div className="text-body-base text-center font-bold text-text-label">
|
||||
{t('layout.no-browsers-in-pool')}
|
||||
</div>
|
||||
<p className="text-center text-label-xs font-medium text-text-label">
|
||||
{t('layout.add-browsers-hint')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'cookies' && (
|
||||
<div className="flex flex-col">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('layout.browser-cookies-management')}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleBrowserLogin}
|
||||
disabled={loginLoading}
|
||||
>
|
||||
<Cookie className="h-4 w-4" />
|
||||
{loginLoading
|
||||
? t('layout.opening')
|
||||
: t('layout.open-browser')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Cookie Domains */}
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center justify-start gap-2">
|
||||
<div className="text-body-base font-bold text-text-body">
|
||||
{t('layout.cookie-domains')}
|
||||
</div>
|
||||
{cookieDomains.length > 0 && (
|
||||
<div className="rounded-lg bg-tag-fill-info px-2 text-label-sm font-bold text-text-information">
|
||||
{groupDomainsByMain(cookieDomains).length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{cookieDomains.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDeleteAll}
|
||||
disabled={deletingAll}
|
||||
className="uppercase !text-text-cuation"
|
||||
>
|
||||
{deletingAll
|
||||
? t('layout.deleting')
|
||||
: t('layout.delete-all')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowCookieRestartDialog(true)}
|
||||
title={t('layout.restart-to-enable-cookies-tooltip')}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 text-text-information" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cookieDomains.length > 0 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{groupDomainsByMain(cookieDomains).map((group, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between rounded-xl border-solid border-border-disabled bg-surface-secondary px-4 py-2"
|
||||
>
|
||||
<div className="flex w-full flex-col items-start justify-start">
|
||||
<span className="truncate text-body-sm font-bold text-text-body">
|
||||
{group.mainDomain}
|
||||
</span>
|
||||
<span className="mt-1 text-label-xs text-text-label">
|
||||
{t('layout.cookie-count', {
|
||||
count: group.totalCookies,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
handleDeleteMainDomain(
|
||||
group.mainDomain,
|
||||
group.subdomains
|
||||
)
|
||||
}
|
||||
disabled={deletingDomain === group.mainDomain}
|
||||
className="ml-3 flex-shrink-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-text-cuation" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-4 py-8">
|
||||
<Cookie className="mb-4 h-12 w-12 text-icon-secondary opacity-50" />
|
||||
<div className="text-body-base text-center font-bold text-text-label">
|
||||
{t('layout.no-cookies-saved-yet')}
|
||||
</div>
|
||||
<p className="text-center text-label-xs font-medium text-text-label">
|
||||
{t('layout.no-cookies-saved-yet-description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
import { Bot } from '@/components/animate-ui/icons/bot';
|
||||
import { Compass } from '@/components/animate-ui/icons/compass';
|
||||
import { Hammer } from '@/components/animate-ui/icons/hammer';
|
||||
import { Radio } from '@/components/animate-ui/icons/radio';
|
||||
import { Settings } from '@/components/animate-ui/icons/settings';
|
||||
import { Sparkle } from '@/components/animate-ui/icons/sparkle';
|
||||
import {
|
||||
|
|
@ -25,7 +26,7 @@ import AlertDialog from '@/components/ui/alertDialog';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import WordCarousel from '@/components/ui/WordCarousel';
|
||||
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
|
||||
import Project from '@/pages/Dashboard/Project';
|
||||
import Project from '@/pages/Projects/Project';
|
||||
import Setting from '@/pages/Setting';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
|
@ -33,21 +34,25 @@ import { useMemo, useRef, useState } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import Agents from './Agents';
|
||||
import Browser from './Dashboard/Browser';
|
||||
import MCP from './Setting/MCP';
|
||||
import Browser from './Browser';
|
||||
import Channels from './Channels';
|
||||
import Connectors from './Connectors';
|
||||
|
||||
const VALID_TABS = [
|
||||
'projects',
|
||||
'workers',
|
||||
'trigger',
|
||||
'settings',
|
||||
'mcp_tools',
|
||||
'browser',
|
||||
'agents',
|
||||
'channels',
|
||||
'connectors',
|
||||
'browser',
|
||||
'settings',
|
||||
] as const;
|
||||
|
||||
type TabType = (typeof VALID_TABS)[number];
|
||||
|
||||
const TAB_ALIASES: Record<string, TabType> = {
|
||||
mcp_tools: 'connectors',
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -61,8 +66,11 @@ export default function Home() {
|
|||
// Compute activeTab from URL, fallback to 'projects' if not in URL or invalid
|
||||
const activeTab = useMemo(() => {
|
||||
const tabFromUrl = searchParams.get('tab');
|
||||
if (tabFromUrl && VALID_TABS.includes(tabFromUrl as TabType)) {
|
||||
return tabFromUrl as TabType;
|
||||
if (tabFromUrl) {
|
||||
const normalizedTab = TAB_ALIASES[tabFromUrl] ?? tabFromUrl;
|
||||
if (VALID_TABS.includes(normalizedTab as TabType)) {
|
||||
return normalizedTab as TabType;
|
||||
}
|
||||
}
|
||||
return 'projects' as TabType;
|
||||
}, [searchParams]);
|
||||
|
|
@ -161,11 +169,19 @@ export default function Home() {
|
|||
iconAnimateOnHover="default"
|
||||
icon={<Bot className="h-4 w-4" />}
|
||||
>
|
||||
{t('setting.agents')}
|
||||
{t('layout.agents')}
|
||||
</MenuToggleItem>
|
||||
<MenuToggleItem
|
||||
size="xs"
|
||||
value="mcp_tools"
|
||||
value="channels"
|
||||
iconAnimateOnHover="default"
|
||||
icon={<Radio className="h-4 w-4" />}
|
||||
>
|
||||
{t('layout.channels')}
|
||||
</MenuToggleItem>
|
||||
<MenuToggleItem
|
||||
size="xs"
|
||||
value="connectors"
|
||||
iconAnimateOnHover="default"
|
||||
icon={<Hammer />}
|
||||
>
|
||||
|
|
@ -175,7 +191,7 @@ export default function Home() {
|
|||
size="xs"
|
||||
value="browser"
|
||||
iconAnimateOnHover="default"
|
||||
icon={<Compass />}
|
||||
icon={<Compass className="h-4 w-4" />}
|
||||
>
|
||||
{t('layout.browser')}
|
||||
</MenuToggleItem>
|
||||
|
|
@ -185,7 +201,7 @@ export default function Home() {
|
|||
iconAnimateOnHover="default"
|
||||
icon={<Settings />}
|
||||
>
|
||||
{t('layout.general')}
|
||||
{t('layout.settings')}
|
||||
</MenuToggleItem>
|
||||
</MenuToggleGroup>
|
||||
</div>
|
||||
|
|
@ -196,10 +212,11 @@ export default function Home() {
|
|||
</div>
|
||||
</div>
|
||||
{activeTab === 'projects' && <Project />}
|
||||
{activeTab === 'mcp_tools' && <MCP />}
|
||||
{activeTab === 'agents' && <Agents />}
|
||||
{activeTab === 'channels' && <Channels />}
|
||||
{activeTab === 'connectors' && <Connectors />}
|
||||
{activeTab === 'browser' && <Browser />}
|
||||
{activeTab === 'settings' && <Setting />}
|
||||
{activeTab === 'agents' && <Agents />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,9 +268,9 @@ export default function Project() {
|
|||
/>
|
||||
|
||||
{/* Header Section */}
|
||||
<div className="flex w-full border-x-0 border-t-0 border-solid border-border-disabled">
|
||||
<div className="mx-auto flex w-full max-w-[900px] items-center justify-between px-6 pb-4 pt-8">
|
||||
<div className="flex w-full flex-row items-center justify-between gap-4">
|
||||
<div className="border-border-disabled flex w-full border-x-0 border-t-0 border-solid">
|
||||
<div className="px-6 pb-4 pt-8 mx-auto flex w-full max-w-[900px] items-center justify-between">
|
||||
<div className="gap-4 flex w-full flex-row items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('layout.projects-hub')}
|
||||
|
|
@ -281,7 +281,7 @@ export default function Project() {
|
|||
</div>
|
||||
|
||||
<div className="flex w-full">
|
||||
<div className="mx-auto flex min-h-[calc(100vh-86px)] w-full max-w-[940px] flex-col items-start justify-start px-6 py-8">
|
||||
<div className="px-6 py-8 mx-auto flex min-h-[calc(100vh-86px)] w-full max-w-[940px] flex-col items-start justify-start">
|
||||
<GroupedHistoryView
|
||||
onTaskSelect={handleSetActive}
|
||||
onTaskDelete={handleDelete}
|
||||
|
|
@ -1,828 +0,0 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import {
|
||||
fetchGet,
|
||||
fetchPost,
|
||||
proxyFetchDelete,
|
||||
proxyFetchGet,
|
||||
proxyFetchPost,
|
||||
proxyFetchPut,
|
||||
} from '@/api/http';
|
||||
import IntegrationList from '@/components/IntegrationList';
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { getProxyBaseURL } from '@/lib';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { ChevronDown, ChevronLeft, ChevronUp, Plus } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import MCPAddDialog from './components/MCPAddDialog';
|
||||
import MCPConfigDialog from './components/MCPConfigDialog';
|
||||
import MCPDeleteDialog from './components/MCPDeleteDialog';
|
||||
import MCPList from './components/MCPList';
|
||||
import SearchEngineConfigDialog from './components/SearchEngineConfigDialog';
|
||||
import type { MCPConfigForm, MCPUserItem } from './components/types';
|
||||
import { arrayToArgsJson, parseArgsToArray } from './components/utils';
|
||||
import MCPMarket from './MCPMarket';
|
||||
|
||||
import { SelectItem, SelectItemWithButton } from '@/components/ui/select';
|
||||
import { Tag as TagComponent } from '@/components/ui/tag';
|
||||
import { ConfigFile } from 'electron/main/utils/mcpConfig';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function SettingMCP() {
|
||||
const _navigate = useNavigate();
|
||||
const { checkAgentTool } = useAuthStore();
|
||||
const { modelType } = useAuthStore();
|
||||
const { t } = useTranslation();
|
||||
const [items, setItems] = useState<MCPUserItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [showConfig, setShowConfig] = useState<MCPUserItem | null>(null);
|
||||
const [configForm, setConfigForm] = useState<MCPConfigForm | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [addType, setAddType] = useState<'local' | 'remote'>('local');
|
||||
const [localJson, setLocalJson] = useState(
|
||||
`{
|
||||
"mcpServers": {
|
||||
"sequential-thinking": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-sequential-thinking"
|
||||
]
|
||||
}
|
||||
}
|
||||
}`
|
||||
);
|
||||
const [remoteName, setRemoteName] = useState('');
|
||||
const [remoteUrl, setRemoteUrl] = useState('');
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<MCPUserItem | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [switchLoading, setSwitchLoading] = useState<Record<number, boolean>>(
|
||||
{}
|
||||
);
|
||||
const [collapsedMCP, setCollapsedMCP] = useState(false);
|
||||
const [collapsedExternal, setCollapsedExternal] = useState(false);
|
||||
const [showMarket, setShowMarket] = useState(false);
|
||||
const [marketKeyword, setMarketKeyword] = useState('');
|
||||
const [showSearchEngineConfig, setShowSearchEngineConfig] = useState(false);
|
||||
|
||||
// add: integrations list
|
||||
const [integrations, setIntegrations] = useState<any[]>([]);
|
||||
const [refreshKey, setRefreshKey] = useState<number>(0);
|
||||
const [essentialIntegrations, _setEssentialIntegrations] = useState<any[]>([
|
||||
{
|
||||
key: 'Search',
|
||||
name: 'Search Engine',
|
||||
env_vars: ['GOOGLE_API_KEY', 'SEARCH_ENGINE_ID'],
|
||||
desc: (
|
||||
<>
|
||||
{t('setting.environmental-variables-required')}: GOOGLE_API_KEY,
|
||||
SEARCH_ENGINE_ID
|
||||
<br />
|
||||
<span
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
marginTop: '0.25rem',
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{t('setting.get-google-search-api')}:{' '}
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.href =
|
||||
'https://developers.google.com/custom-search/v1/overview';
|
||||
}}
|
||||
className="text-text-link underline"
|
||||
>
|
||||
{t('setting.google-custom-search-api')}
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
// default search engine and availability
|
||||
const [_defaultSearchEngine, setDefaultSearchEngine] =
|
||||
useState<string>('google');
|
||||
const [_hasGoogleSearch, setHasGoogleSearch] = useState<boolean>(false);
|
||||
const [configs, setConfigs] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
proxyFetchGet('/api/configs').then((configsRes) => {
|
||||
const configs = Array.isArray(configsRes) ? configsRes : [];
|
||||
setConfigs(configs);
|
||||
const hasGoogleApiKey = !!configs.find(
|
||||
(item: any) => item.config_name === 'GOOGLE_API_KEY'
|
||||
);
|
||||
const hasGoogleCseId = !!configs.find(
|
||||
(item: any) => item.config_name === 'SEARCH_ENGINE_ID'
|
||||
);
|
||||
setHasGoogleSearch(hasGoogleApiKey && hasGoogleCseId);
|
||||
const defaultEngine = configs.find(
|
||||
(item: any) =>
|
||||
item.config_group?.toLowerCase() === 'search' &&
|
||||
item.config_name === 'DEFAULT_SEARCH_ENGINE'
|
||||
)?.config_value;
|
||||
if (defaultEngine) setDefaultSearchEngine(defaultEngine);
|
||||
else setDefaultSearchEngine('google'); // Default to Google
|
||||
});
|
||||
}, []);
|
||||
|
||||
// get list
|
||||
const fetchList = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
proxyFetchGet('/api/mcp/users')
|
||||
.then((res) => {
|
||||
if (Array.isArray(res)) {
|
||||
setItems(res);
|
||||
} else if (Array.isArray(res.items)) {
|
||||
setItems(res.items);
|
||||
} else {
|
||||
setItems([]);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err?.message || t('setting.load-failed'));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
// get integrations
|
||||
useEffect(() => {
|
||||
proxyFetchGet('/api/config/info').then((res) => {
|
||||
if (res && typeof res === 'object') {
|
||||
const baseURL = getProxyBaseURL();
|
||||
const list = Object.entries(res).map(([key, value]: [string, any]) => {
|
||||
let onInstall = null;
|
||||
|
||||
// Special handling for Notion MCP
|
||||
if (key.toLowerCase() === 'notion') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost('/install/tool/notion');
|
||||
if (response.success) {
|
||||
// Check if there's a warning (connection failed but installation marked as complete)
|
||||
if (response.warning) {
|
||||
toast.warning(response.warning, { duration: 5000 });
|
||||
} else {
|
||||
toast.success(
|
||||
t('setting.notion-mcp-installed-successfully')
|
||||
);
|
||||
}
|
||||
// Save to config to mark as installed
|
||||
await proxyFetchPost('/api/configs', {
|
||||
config_group: 'Notion',
|
||||
config_name: 'MCP_REMOTE_CONFIG_DIR',
|
||||
config_value: response.toolkit_name || 'NotionMCPToolkit',
|
||||
});
|
||||
// Refresh the integrations list to show the installed state
|
||||
fetchList();
|
||||
// Force refresh IntegrationList component
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
} else {
|
||||
toast.error(
|
||||
response.error || t('setting.failed-to-install-notion-mcp')
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
error.message || t('setting.failed-to-install-notion-mcp')
|
||||
);
|
||||
}
|
||||
};
|
||||
} else if (key.toLowerCase() === 'google calendar') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost(
|
||||
'/install/tool/google_calendar'
|
||||
);
|
||||
if (response.success) {
|
||||
// Check if there's a warning (connection failed but installation marked as complete)
|
||||
if (response.warning) {
|
||||
toast.warning(response.warning, { duration: 5000 });
|
||||
} else {
|
||||
toast.success(
|
||||
t('setting.google-calendar-installed-successfully')
|
||||
);
|
||||
}
|
||||
try {
|
||||
// Ensure we persist a marker config to indicate installation
|
||||
const existingConfigs = await proxyFetchGet('/api/configs');
|
||||
const existing = Array.isArray(existingConfigs)
|
||||
? existingConfigs.find(
|
||||
(c: any) =>
|
||||
c.config_group?.toLowerCase() ===
|
||||
'google calendar' &&
|
||||
c.config_name === 'GOOGLE_REFRESH_TOKEN'
|
||||
)
|
||||
: null;
|
||||
|
||||
const configPayload = {
|
||||
config_group: 'Google Calendar',
|
||||
config_name: 'GOOGLE_REFRESH_TOKEN',
|
||||
config_value: 'exists',
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await proxyFetchPut(
|
||||
`/api/configs/${existing.id}`,
|
||||
configPayload
|
||||
);
|
||||
} else {
|
||||
await proxyFetchPost('/api/configs', configPayload);
|
||||
}
|
||||
} catch (configError) {
|
||||
console.warn(
|
||||
'Failed to persist Google Calendar config',
|
||||
configError
|
||||
);
|
||||
}
|
||||
// Refresh the integrations list to show the installed state
|
||||
fetchList();
|
||||
// Force refresh IntegrationList component
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
} else if (response.status === 'authorizing') {
|
||||
// Authorization in progress - start polling for completion
|
||||
toast.info(
|
||||
t('setting.please-complete-authorization-in-browser')
|
||||
);
|
||||
|
||||
// Poll for authorization completion via oauth status endpoint
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const statusResp = await fetchGet(
|
||||
'/oauth/status/google_calendar'
|
||||
);
|
||||
if (statusResp?.status === 'success') {
|
||||
clearInterval(pollInterval);
|
||||
// Now that auth succeeded, run install again to initialize toolkit
|
||||
const finalize = await fetchPost(
|
||||
'/install/tool/google_calendar'
|
||||
);
|
||||
if (finalize?.success) {
|
||||
const configs = await proxyFetchGet('/api/configs');
|
||||
const existing = Array.isArray(configs)
|
||||
? configs.find(
|
||||
(c: any) =>
|
||||
c.config_group?.toLowerCase() ===
|
||||
'google calendar' &&
|
||||
c.config_name === 'GOOGLE_REFRESH_TOKEN'
|
||||
)
|
||||
: null;
|
||||
|
||||
const payload = {
|
||||
config_group: 'Google Calendar',
|
||||
config_name: 'GOOGLE_REFRESH_TOKEN',
|
||||
config_value: 'exists',
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await proxyFetchPut(
|
||||
`/api/configs/${existing.id}`,
|
||||
payload
|
||||
);
|
||||
} else {
|
||||
await proxyFetchPost('/api/configs', payload);
|
||||
}
|
||||
|
||||
toast.success(
|
||||
t('setting.google-calendar-installed-successfully')
|
||||
);
|
||||
fetchList();
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
}
|
||||
} else if (
|
||||
statusResp?.status === 'failed' ||
|
||||
statusResp?.status === 'cancelled'
|
||||
) {
|
||||
clearInterval(pollInterval);
|
||||
const msg =
|
||||
statusResp?.error ||
|
||||
(statusResp?.status === 'cancelled'
|
||||
? t('setting.authorization-cancelled')
|
||||
: t('setting.authorization-failed'));
|
||||
toast.error(msg);
|
||||
}
|
||||
// if still authorizing, continue polling
|
||||
} catch (err) {
|
||||
console.error('Polling oauth status failed', err);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Safety timeout
|
||||
setTimeout(() => clearInterval(pollInterval), 5 * 60 * 1000);
|
||||
} else {
|
||||
toast.error(
|
||||
response.error ||
|
||||
response.message ||
|
||||
t('setting.failed-to-install-google-calendar')
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
error.message ||
|
||||
t('setting.failed-to-install-google-calendar')
|
||||
);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
onInstall = () => {
|
||||
const url = `${baseURL}/api/oauth/${key.toLowerCase()}/login`;
|
||||
// Open in a new window to avoid navigating the app/webview
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
name: key,
|
||||
env_vars: value.env_vars,
|
||||
desc:
|
||||
value.env_vars && value.env_vars.length > 0
|
||||
? `${t(
|
||||
'setting.environmental-variables-required'
|
||||
)}: ${value.env_vars.join(', ')}`
|
||||
: key.toLowerCase() === 'notion'
|
||||
? t('setting.notion-workspace-integration')
|
||||
: key.toLowerCase() === 'google calendar'
|
||||
? t('setting.google-calendar-integration')
|
||||
: '',
|
||||
onInstall,
|
||||
};
|
||||
});
|
||||
console.log('API response:', res);
|
||||
console.log('Generated list:', list);
|
||||
console.log('Essential integrations:', essentialIntegrations);
|
||||
|
||||
setIntegrations(
|
||||
list.filter(
|
||||
(item) => !essentialIntegrations.find((i) => i.key === item.key)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [essentialIntegrations, fetchList, t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchList();
|
||||
}, [fetchList]);
|
||||
|
||||
// MCP list switch
|
||||
const handleSwitch = async (id: number, checked: boolean) => {
|
||||
setSwitchLoading((l) => ({ ...l, [id]: true }));
|
||||
try {
|
||||
await proxyFetchPut(`/api/mcp/users/${id}`, { status: checked ? 1 : 2 });
|
||||
fetchList();
|
||||
} finally {
|
||||
setSwitchLoading((l) => ({ ...l, [id]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// config dialog
|
||||
useEffect(() => {
|
||||
if (showConfig) {
|
||||
setConfigForm({
|
||||
mcp_name: showConfig.mcp_name || '',
|
||||
mcp_desc: showConfig.mcp_desc || '',
|
||||
command: showConfig.command || '',
|
||||
argsArr: showConfig.args ? parseArgsToArray(showConfig.args) : [],
|
||||
env: showConfig.env ? { ...showConfig.env } : {},
|
||||
});
|
||||
setErrorMsg(null);
|
||||
} else {
|
||||
setConfigForm(null);
|
||||
setErrorMsg(null);
|
||||
}
|
||||
}, [showConfig]);
|
||||
|
||||
const handleConfigSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!configForm || !showConfig) return;
|
||||
setSaving(true);
|
||||
setErrorMsg(null);
|
||||
try {
|
||||
const mcpData = {
|
||||
mcp_name: configForm.mcp_name,
|
||||
mcp_desc: configForm.mcp_desc,
|
||||
command: configForm.command,
|
||||
args: arrayToArgsJson(configForm.argsArr),
|
||||
env: configForm.env,
|
||||
};
|
||||
await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, mcpData);
|
||||
|
||||
if (window.ipcRenderer) {
|
||||
//Partial payload to empty env {}
|
||||
const payload: any = {
|
||||
description: configForm.mcp_desc,
|
||||
command: configForm.command,
|
||||
args: arrayToArgsJson(configForm.argsArr),
|
||||
};
|
||||
if (configForm.env && Object.keys(configForm.env).length > 0) {
|
||||
payload.env = configForm.env;
|
||||
}
|
||||
window.ipcRenderer.invoke('mcp-update', mcpData.mcp_name, payload);
|
||||
}
|
||||
|
||||
setShowConfig(null);
|
||||
fetchList();
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err?.message || t('setting.save-failed'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
const handleConfigClose = () => {
|
||||
setShowConfig(null);
|
||||
setConfigForm(null);
|
||||
setErrorMsg(null);
|
||||
};
|
||||
const handleConfigSwitch = async (checked: boolean) => {
|
||||
if (!showConfig) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, {
|
||||
status: checked ? 1 : 0,
|
||||
});
|
||||
setShowConfig((prev) =>
|
||||
prev ? { ...prev, status: checked ? 1 : 0 } : prev
|
||||
);
|
||||
fetchList();
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// add MCP dialog
|
||||
const handleInstall = async () => {
|
||||
setInstalling(true);
|
||||
try {
|
||||
if (addType === 'local') {
|
||||
let data: ConfigFile;
|
||||
try {
|
||||
data = JSON.parse(localJson);
|
||||
|
||||
// validate mcpServers structure
|
||||
if (!data.mcpServers || typeof data.mcpServers !== 'object') {
|
||||
throw new Error('Invalid mcpServers');
|
||||
}
|
||||
|
||||
// check for name conflicts with existing items
|
||||
const serverNames = Object.keys(data.mcpServers);
|
||||
const conflict = serverNames.find((name) =>
|
||||
items.some((d) => d.mcp_name === name)
|
||||
);
|
||||
if (conflict) {
|
||||
toast.error(
|
||||
t('setting.mcp-server-already-exists', { name: conflict }),
|
||||
{
|
||||
closeButton: true,
|
||||
}
|
||||
);
|
||||
setInstalling(false);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON:', e);
|
||||
toast.error(t('setting.invalid-json'), { closeButton: true });
|
||||
setInstalling(false);
|
||||
return;
|
||||
}
|
||||
let res = await proxyFetchPost('/api/mcp/import/local', data);
|
||||
if (res.detail) {
|
||||
toast.error(t('setting.invalid-json'), { closeButton: true });
|
||||
setInstalling(false);
|
||||
return;
|
||||
}
|
||||
if (window.ipcRenderer) {
|
||||
const mcpServers = data['mcpServers'];
|
||||
for (const [key, value] of Object.entries(mcpServers)) {
|
||||
await window.ipcRenderer.invoke('mcp-install', key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
setShowAdd(false);
|
||||
setLocalJson(`{
|
||||
"mcpServers": {}
|
||||
}`);
|
||||
setRemoteName('');
|
||||
setRemoteUrl('');
|
||||
fetchList();
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
// delete dialog
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
checkAgentTool(deleteTarget.mcp_name);
|
||||
await proxyFetchDelete(`/api/mcp/users/${deleteTarget.id}`);
|
||||
// notify main process
|
||||
if (window.ipcRenderer) {
|
||||
console.log('deleteTarget', deleteTarget.mcp_key);
|
||||
await window.ipcRenderer.invoke('mcp-remove', deleteTarget.mcp_key);
|
||||
}
|
||||
setDeleteTarget(null);
|
||||
fetchList();
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Generate search engine selection content
|
||||
const generateSearchEngineSelectContent = () => {
|
||||
console.log('Generating search engine select content, configs:', configs);
|
||||
const isCustom = modelType === 'custom';
|
||||
|
||||
// Google Search - requires API key and Search Engine ID in custom mode
|
||||
const hasGoogleApiKey = configs.some(
|
||||
(c: any) => c.config_name === 'GOOGLE_API_KEY'
|
||||
);
|
||||
const hasGoogleCseId = configs.some(
|
||||
(c: any) => c.config_name === 'SEARCH_ENGINE_ID'
|
||||
);
|
||||
const hasGoogle = hasGoogleApiKey && hasGoogleCseId;
|
||||
|
||||
console.log('Search engine status:', { hasGoogle, isCustom });
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Custom mode: require API key configuration */}
|
||||
{isCustom ? (
|
||||
<SelectItemWithButton
|
||||
value="google"
|
||||
label={
|
||||
<span>
|
||||
<span>Google Search </span>
|
||||
<TagComponent asChild>
|
||||
<span>{t('setting.recommended')}</span>
|
||||
</TagComponent>
|
||||
</span>
|
||||
}
|
||||
enabled={hasGoogle}
|
||||
buttonText={t('setting.setting')}
|
||||
onButtonClick={() => setShowSearchEngineConfig(true)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* Cloud or Local mode: Google enabled by default */}
|
||||
<SelectItem value="google">
|
||||
<span>Google Search </span>
|
||||
<TagComponent asChild>
|
||||
<span>{t('setting.recommended')}</span>
|
||||
</TagComponent>
|
||||
</SelectItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="m-auto h-auto flex-1">
|
||||
{/* Header Section */}
|
||||
<div className="flex w-full">
|
||||
<div className="mx-auto flex w-full max-w-[940px] items-center justify-between px-6 pb-4 pt-8">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{showMarket ? (
|
||||
<div className="flex w-full items-center justify-between gap-sm">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowMarket(false)}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('setting.mcp-market')}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="w-full">
|
||||
<SearchInput
|
||||
value={marketKeyword}
|
||||
onChange={(e) => setMarketKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
{t('setting.mcp-and-tools')}
|
||||
</div>
|
||||
<div className="flex items-center gap-sm">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowAdd(true)}
|
||||
>
|
||||
<Plus />
|
||||
<span>{t('setting.add-mcp-server')}</span>
|
||||
</Button>
|
||||
{/* <Button variant="outline" size="sm" onClick={() => setShowMarket(true)}>
|
||||
<Store />
|
||||
<span>{t("setting.market")}</span>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="flex w-full">
|
||||
<div className="mx-auto flex min-h-[calc(100vh-86px)] w-full max-w-[940px] items-start justify-center px-6 py-8">
|
||||
<div className="flex w-full flex-col gap-8">
|
||||
{showMarket ? (
|
||||
<div className="pt-2">
|
||||
<MCPMarket
|
||||
onBack={() => setShowMarket(false)}
|
||||
keyword={marketKeyword}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-full flex-1">
|
||||
<IntegrationList
|
||||
variant="manage"
|
||||
items={essentialIntegrations}
|
||||
showConfigButton={true}
|
||||
showInstallButton={false}
|
||||
showSelect
|
||||
showStatusDot={false}
|
||||
selectPlaceholder="Google Search"
|
||||
selectContent={generateSearchEngineSelectContent()}
|
||||
onSelectChange={async (value) => {
|
||||
try {
|
||||
setDefaultSearchEngine(value);
|
||||
await proxyFetchPost('/api/configs', {
|
||||
config_group: 'Search',
|
||||
config_name: 'DEFAULT_SEARCH_ENGINE',
|
||||
config_value: value,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Error setting default search engine:',
|
||||
e
|
||||
);
|
||||
}
|
||||
}}
|
||||
onConfigClick={(item) => {
|
||||
if (item.key === 'Search') {
|
||||
setShowSearchEngineConfig(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="inline-flex items-center justify-start gap-2 self-stretch py-2">
|
||||
<span className="text-body-md font-bold text-text-body">
|
||||
{t('setting.mcp')}
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="md"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCollapsedMCP((c) => !c);
|
||||
}}
|
||||
>
|
||||
{collapsedMCP ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{!collapsedMCP && (
|
||||
<IntegrationList
|
||||
key={refreshKey}
|
||||
variant="manage"
|
||||
items={integrations}
|
||||
showConfigButton={false}
|
||||
showInstallButton={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="inline-flex items-center justify-start gap-2 self-stretch py-2">
|
||||
<div className="justify-center text-body-md font-bold text-text-body">
|
||||
{t('setting.your-own-mcps')}
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="md"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCollapsedExternal((c) => !c);
|
||||
}}
|
||||
>
|
||||
{collapsedExternal ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{!collapsedExternal && (
|
||||
<>
|
||||
{isLoading && (
|
||||
<div className="py-8 text-center text-text-label">
|
||||
{t('setting.loading')}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="py-8 text-center text-text-error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && items.length === 0 && (
|
||||
<div className="py-8 text-center text-text-label">
|
||||
{t('setting.no-mcp-servers')}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<MCPList
|
||||
items={items}
|
||||
onSetting={setShowConfig}
|
||||
onDelete={setDeleteTarget}
|
||||
onSwitch={handleSwitch}
|
||||
switchLoading={switchLoading}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<MCPConfigDialog
|
||||
open={!!showConfig}
|
||||
form={configForm}
|
||||
mcp={showConfig}
|
||||
onChange={setConfigForm as any}
|
||||
onSave={handleConfigSave}
|
||||
onClose={handleConfigClose}
|
||||
loading={saving}
|
||||
errorMsg={errorMsg}
|
||||
onSwitchStatus={handleConfigSwitch}
|
||||
/>
|
||||
<MCPAddDialog
|
||||
open={showAdd}
|
||||
addType={addType}
|
||||
setAddType={setAddType}
|
||||
localJson={localJson}
|
||||
setLocalJson={setLocalJson}
|
||||
remoteName={remoteName}
|
||||
setRemoteName={setRemoteName}
|
||||
remoteUrl={remoteUrl}
|
||||
setRemoteUrl={setRemoteUrl}
|
||||
installing={installing}
|
||||
onClose={() => setShowAdd(false)}
|
||||
onInstall={handleInstall}
|
||||
/>
|
||||
<MCPDeleteDialog
|
||||
open={!!deleteTarget}
|
||||
target={deleteTarget}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onConfirm={handleDelete}
|
||||
loading={deleting}
|
||||
/>
|
||||
<SearchEngineConfigDialog
|
||||
open={showSearchEngineConfig}
|
||||
onClose={() => setShowSearchEngineConfig(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { proxyFetchGet, proxyFetchPost } from '@/api/http';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogContentSection,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Tag as TagComponent } from '@/components/ui/tag';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { AlertTriangle, Check, Circle, Eye } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface SearchEngineProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
requiresApiKey: boolean;
|
||||
enabledByDefault?: boolean;
|
||||
recommended?: boolean;
|
||||
fields: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
note?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface SearchEngineConfigDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function buildSearchEngines(
|
||||
modelType: 'cloud' | 'local' | 'custom'
|
||||
): SearchEngineProvider[] {
|
||||
// Only Google search engine, with custom mode requiring API key configuration
|
||||
if (modelType === 'custom') {
|
||||
return [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
description:
|
||||
'Connect to Google Custom Search (requires API key and CSE ID).',
|
||||
requiresApiKey: true,
|
||||
fields: [
|
||||
{
|
||||
key: 'GOOGLE_API_KEY',
|
||||
label: 'Google API Key',
|
||||
placeholder: 'Enter your Google API key from Google Cloud Console',
|
||||
note: 'Learn how to get your Google API key → https://developers.google.com/custom-search/v1/overview',
|
||||
},
|
||||
{
|
||||
key: 'SEARCH_ENGINE_ID',
|
||||
label: 'Search Engine ID',
|
||||
placeholder:
|
||||
'Enter the Custom Search Engine ID associated with your API key',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// cloud or local → Google enabled by default, no config required
|
||||
return [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
description:
|
||||
'Google Search integration available. No setup required — enabled by default.',
|
||||
requiresApiKey: false,
|
||||
enabledByDefault: true,
|
||||
recommended: true,
|
||||
fields: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function SearchEngineConfigDialog({
|
||||
open,
|
||||
onClose,
|
||||
}: SearchEngineConfigDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const { modelType } = useAuthStore();
|
||||
const [selectedProvider, setSelectedProvider] =
|
||||
useState<SearchEngineProvider>(buildSearchEngines(modelType)[0]);
|
||||
const [configs, setConfigs] = useState<any[]>([]);
|
||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||
const [showKeys, setShowKeys] = useState<Record<string, boolean>>({});
|
||||
const [_testing, setTesting] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const engines = useMemo(() => buildSearchEngines(modelType), [modelType]);
|
||||
|
||||
// Load existing configurations
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
proxyFetchGet('/api/configs')
|
||||
.then((res) => {
|
||||
setConfigs(Array.isArray(res) ? res : []);
|
||||
// Initialize form data with existing values
|
||||
const existingData: Record<string, string> = {};
|
||||
engines.forEach((engine) => {
|
||||
engine.fields.forEach((field) => {
|
||||
const config = res.find((c: any) => c.config_name === field.key);
|
||||
if (config) {
|
||||
existingData[field.key] = config.config_value || '';
|
||||
}
|
||||
});
|
||||
});
|
||||
setFormData(existingData);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to load configs:', err);
|
||||
setConfigs([]);
|
||||
});
|
||||
}
|
||||
}, [open, engines]);
|
||||
|
||||
const getProviderStatus = (provider: SearchEngineProvider) => {
|
||||
// For providers that are enabled by default, always show as configured
|
||||
if (provider.enabledByDefault) {
|
||||
return 'configured';
|
||||
}
|
||||
|
||||
if (!provider.requiresApiKey) {
|
||||
// For providers that don't require API keys, check if they're enabled
|
||||
const isEnabled = configs.some(
|
||||
(c: any) =>
|
||||
c.config_group?.toLowerCase() === 'search' &&
|
||||
c.config_name === `ENABLE_${provider.id.toUpperCase()}_SEARCH` &&
|
||||
c.config_value === 'true'
|
||||
);
|
||||
return isEnabled ? 'configured' : 'not-configured';
|
||||
}
|
||||
|
||||
// For providers that require API keys, check if all required fields are filled
|
||||
const requiredFields = provider.fields.map((f) => f.key);
|
||||
const filledFields = requiredFields.filter((fieldKey) => {
|
||||
const config = configs.find((c: any) => c.config_name === fieldKey);
|
||||
return config && config.config_value && config.config_value.trim() !== '';
|
||||
});
|
||||
|
||||
if (filledFields.length === 0) {
|
||||
return 'not-configured';
|
||||
} else if (filledFields.length === requiredFields.length) {
|
||||
return 'configured';
|
||||
} else {
|
||||
return 'incomplete';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'configured':
|
||||
return <Check className="h-4 w-4 text-text-success" />;
|
||||
case 'incomplete':
|
||||
return <AlertTriangle className="h-4 w-4 text-text-cuation" />;
|
||||
default:
|
||||
return <Circle className="h-4 w-4 text-text-label" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'configured':
|
||||
return t('setting.configured');
|
||||
case 'incomplete':
|
||||
return t('setting.incomplete');
|
||||
default:
|
||||
return t('setting.not-configured');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFieldChange = (fieldKey: string, value: string) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldKey]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const _testConnection = async () => {
|
||||
if (!selectedProvider.requiresApiKey) {
|
||||
toast.success(t('setting.this-service-does-not-require-an-api-key'));
|
||||
return;
|
||||
}
|
||||
|
||||
setTesting(true);
|
||||
try {
|
||||
// Here you would implement actual connection testing
|
||||
// For now, we'll just simulate a test
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
toast.success(t('setting.connection-test-successful'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(t('setting.connection-test-failed'));
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfiguration = async () => {
|
||||
// Skip saving for engines that are enabled by default
|
||||
if (selectedProvider.enabledByDefault) {
|
||||
toast.info(t('setting.this-service-is-already-enabled-by-default'));
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
if (selectedProvider.requiresApiKey) {
|
||||
// Save API key fields
|
||||
for (const field of selectedProvider.fields) {
|
||||
const value = formData[field.key];
|
||||
if (value && value.trim() !== '') {
|
||||
await proxyFetchPost('/api/configs', {
|
||||
config_group: 'Search',
|
||||
config_name: field.key,
|
||||
config_value: value.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Enable the service
|
||||
await proxyFetchPost('/api/configs', {
|
||||
config_group: 'Search',
|
||||
config_name: `ENABLE_${selectedProvider.id.toUpperCase()}_SEARCH`,
|
||||
config_value: 'true',
|
||||
});
|
||||
}
|
||||
|
||||
toast.success(t('setting.configuration-saved-successfully'));
|
||||
// Refresh configs
|
||||
const res = await proxyFetchGet('/api/configs');
|
||||
setConfigs(Array.isArray(res) ? res : []);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(t('setting.failed-to-save-configuration'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent size="lg">
|
||||
<DialogHeader title={t('setting.search-engine-integrations')} />
|
||||
|
||||
<DialogContentSection className="flex h-full">
|
||||
{/* Left Panel - Provider List */}
|
||||
<div className="w-1/3 border border-y-0 border-l-0 border-solid border-border-secondary pr-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{engines.map((provider) => {
|
||||
const status = getProviderStatus(provider);
|
||||
const isSelected = selectedProvider.id === provider.id;
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="md"
|
||||
key={provider.id}
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className={`w-full justify-between border border-solid border-transparent bg-transparent transition-colors duration-200 ease-in-out ${isSelected ? 'border border-solid border-border-primary bg-surface-secondary' : 'hover:bg-surface-secondary'}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon(status)}
|
||||
<div className="flex items-center gap-2 text-label-sm font-bold">
|
||||
<span>{provider.name}</span>
|
||||
{provider.recommended ? (
|
||||
<TagComponent asChild>
|
||||
<span>{t('setting.recommended')}</span>
|
||||
</TagComponent>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-extralight text-text-label">
|
||||
{getStatusText(status)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Configuration Detail */}
|
||||
<div className="h-[400px] flex-1 pl-4">
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Provider Header */}
|
||||
<div className="flex flex-col gap-2 pb-2">
|
||||
<div className="text-label-lg font-bold">
|
||||
{selectedProvider.name}
|
||||
</div>
|
||||
<div className="text-label-sm font-normal text-text-label">
|
||||
{selectedProvider.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Form */}
|
||||
<div className="flex-1 pt-4">
|
||||
{selectedProvider.requiresApiKey ? (
|
||||
<div className="space-y-4">
|
||||
{selectedProvider.fields.map((field) => (
|
||||
<div key={field.key}>
|
||||
<Input
|
||||
id={field.key}
|
||||
size="default"
|
||||
title={field.label}
|
||||
type={showKeys[field.key] ? 'text' : 'password'}
|
||||
placeholder={field.placeholder}
|
||||
value={formData[field.key] || ''}
|
||||
onChange={(e) =>
|
||||
handleFieldChange(field.key, e.target.value)
|
||||
}
|
||||
note={field.note}
|
||||
className="mt-1"
|
||||
backIcon={<Eye className="h-5 w-5" />}
|
||||
onBackIconClick={() =>
|
||||
setShowKeys((prev) => ({
|
||||
...prev,
|
||||
[field.key]: !prev[field.key],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg bg-surface-primary p-4">
|
||||
<p className="text-label-sm text-text-label">
|
||||
{selectedProvider.id === 'wiki'
|
||||
? t(
|
||||
'setting.this-service-is-public-and-does-not-require-credentials'
|
||||
)
|
||||
: t('setting.this-service-does-not-require-an-api-key')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{!selectedProvider.enabledByDefault && (
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{/* {selectedProvider.requiresApiKey && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={testConnection}
|
||||
disabled={testing}
|
||||
>
|
||||
{testing ? t("setting.testing") : t("setting.test-connection")}
|
||||
</Button>
|
||||
)} */}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={saveConfiguration}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving
|
||||
? t('setting.saving')
|
||||
: selectedProvider.requiresApiKey
|
||||
? t('setting.save-changes')
|
||||
: `${t('setting.enable')} ${selectedProvider.name} ${t('setting.search')}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContentSection>
|
||||
<DialogFooter className="justify-between !rounded-b-xl bg-white-100% p-md">
|
||||
<p className="flex items-center gap-1 text-label-xs text-text-label">
|
||||
{t(
|
||||
'setting.your-api-keys-are-stored-securely-and-never-shared-externally'
|
||||
)}
|
||||
</p>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -69,8 +69,8 @@ export default function Setting() {
|
|||
|
||||
return (
|
||||
<div className="m-auto flex h-auto max-w-[940px] flex-col">
|
||||
<div className="flex h-auto w-full px-6">
|
||||
<div className="sticky top-20 flex h-full w-40 flex-shrink-0 flex-grow-0 flex-col justify-between self-start pr-6 pt-8">
|
||||
<div className="px-6 flex h-auto w-full">
|
||||
<div className="top-20 w-40 pr-6 pt-8 sticky flex h-full flex-shrink-0 flex-grow-0 flex-col justify-between self-start">
|
||||
<VerticalNavigation
|
||||
items={
|
||||
settingMenus.map((menu) => {
|
||||
|
|
@ -84,11 +84,11 @@ export default function Setting() {
|
|||
}
|
||||
value={activeTab}
|
||||
onValueChange={handleTabChange}
|
||||
className="h-full min-h-0 w-full flex-1 gap-0"
|
||||
className="min-h-0 gap-0 h-full w-full flex-1"
|
||||
listClassName="w-full h-full overflow-y-auto"
|
||||
contentClassName="hidden"
|
||||
/>
|
||||
<div className="mt-4 flex w-full flex-shrink-0 flex-grow-0 flex-col items-center justify-center gap-4 border-x-0 border-b-0 border-t-[0.5px] border-solid border-border-secondary py-4">
|
||||
<div className="mt-4 gap-4 border-border-secondary py-4 flex w-full flex-shrink-0 flex-grow-0 flex-col items-center justify-center border-x-0 border-t-[0.5px] border-b-0 border-solid">
|
||||
<button
|
||||
onClick={() =>
|
||||
window.open(
|
||||
|
|
@ -97,7 +97,7 @@ export default function Setting() {
|
|||
'noopener,noreferrer'
|
||||
)
|
||||
}
|
||||
className="flex w-full cursor-pointer flex-row items-center justify-center gap-2 rounded-lg bg-surface-tertiary px-6 py-1.5 transition-opacity duration-200 hover:opacity-60"
|
||||
className="gap-2 rounded-lg bg-surface-tertiary px-6 py-1.5 flex w-full cursor-pointer flex-row items-center justify-center transition-opacity duration-200 hover:opacity-60"
|
||||
>
|
||||
<TagIcon className="h-4 w-4 text-text-success" />
|
||||
<div className="text-label-sm font-semibold text-text-body">
|
||||
|
|
@ -120,7 +120,7 @@ export default function Setting() {
|
|||
</div>
|
||||
|
||||
<div className="flex h-auto w-full flex-1 flex-col">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="gap-4 flex flex-col">
|
||||
{activeTab === 'general' && <General />}
|
||||
{activeTab === 'privacy' && <Privacy />}
|
||||
</div>
|
||||
|
|
@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest';
|
|||
import {
|
||||
arrayToArgsJson,
|
||||
parseArgsToArray,
|
||||
} from '../../../../src/pages/Setting/components/utils';
|
||||
} from '../../../../src/pages/Connectors/components/utils';
|
||||
|
||||
describe('parseArgsToArray', () => {
|
||||
it('should parse JSON array string to array', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue