From 9318dd4e43163294095feb63a385aef292f2ab31 Mon Sep 17 00:00:00 2001 From: Douglas Date: Thu, 12 Feb 2026 21:54:08 +0100 Subject: [PATCH] update skill ui --- src/components/Navigation/index.tsx | 4 +- src/components/SearchInput/index.tsx | 136 ++++++- src/components/WorkFlow/agents.tsx | 106 +++++ src/components/WorkFlow/node.tsx | 54 +-- src/components/ui/alertDialog.tsx | 7 +- src/components/ui/dialog.tsx | 20 +- src/components/ui/tabs.tsx | 200 ++++++++-- src/components/ui/toggle-group.tsx | 1 + .../ar/{capabilities.json => agents.json} | 5 + src/i18n/locales/ar/index.ts | 4 +- .../de/{capabilities.json => agents.json} | 5 + src/i18n/locales/de/index.ts | 4 +- .../en-us/{capabilities.json => agents.json} | 5 + src/i18n/locales/en-us/index.ts | 4 +- src/i18n/locales/en-us/setting.json | 3 +- .../es/{capabilities.json => agents.json} | 5 + src/i18n/locales/es/index.ts | 4 +- .../fr/{capabilities.json => agents.json} | 5 + src/i18n/locales/fr/index.ts | 4 +- .../it/{capabilities.json => agents.json} | 5 + src/i18n/locales/it/index.ts | 4 +- .../ja/{capabilities.json => agents.json} | 5 + src/i18n/locales/ja/index.ts | 4 +- .../ko/{capabilities.json => agents.json} | 5 + src/i18n/locales/ko/index.ts | 4 +- .../ru/{capabilities.json => agents.json} | 5 + src/i18n/locales/ru/index.ts | 4 +- .../{capabilities.json => agents.json} | 5 + src/i18n/locales/zh-Hans/index.ts | 4 +- .../{capabilities.json => agents.json} | 5 + src/i18n/locales/zh-Hant/index.ts | 4 +- src/lib/skillToolkit.ts | 11 +- src/pages/{Capabilities => Agents}/Memory.tsx | 28 +- src/pages/{Setting => Agents}/Models.tsx | 0 src/pages/Agents/Skills.tsx | 192 +++++++++ .../components/SkillDeleteDialog.tsx | 43 +- src/pages/Agents/components/SkillListItem.tsx | 278 +++++++++++++ .../Agents/components/SkillUploadDialog.tsx | 371 ++++++++++++++++++ src/pages/{Capabilities => Agents}/index.tsx | 18 +- src/pages/Capabilities/Skills.tsx | 220 ----------- .../Capabilities/components/SkillListItem.tsx | 136 ------- .../components/SkillScopeSelect.tsx | 181 --------- .../components/SkillUploadDialog.tsx | 298 -------------- src/pages/History.tsx | 15 +- src/pages/Home.tsx | 2 +- src/pages/Setting.tsx | 10 +- 46 files changed, 1407 insertions(+), 1026 deletions(-) create mode 100644 src/components/WorkFlow/agents.tsx rename src/i18n/locales/ar/{capabilities.json => agents.json} (89%) rename src/i18n/locales/de/{capabilities.json => agents.json} (89%) rename src/i18n/locales/en-us/{capabilities.json => agents.json} (87%) rename src/i18n/locales/es/{capabilities.json => agents.json} (88%) rename src/i18n/locales/fr/{capabilities.json => agents.json} (88%) rename src/i18n/locales/it/{capabilities.json => agents.json} (88%) rename src/i18n/locales/ja/{capabilities.json => agents.json} (87%) rename src/i18n/locales/ko/{capabilities.json => agents.json} (87%) rename src/i18n/locales/ru/{capabilities.json => agents.json} (88%) rename src/i18n/locales/zh-Hans/{capabilities.json => agents.json} (87%) rename src/i18n/locales/zh-Hant/{capabilities.json => agents.json} (87%) rename src/pages/{Capabilities => Agents}/Memory.tsx (56%) rename src/pages/{Setting => Agents}/Models.tsx (100%) create mode 100644 src/pages/Agents/Skills.tsx rename src/pages/{Capabilities => Agents}/components/SkillDeleteDialog.tsx (57%) create mode 100644 src/pages/Agents/components/SkillListItem.tsx create mode 100644 src/pages/Agents/components/SkillUploadDialog.tsx rename src/pages/{Capabilities => Agents}/index.tsx (78%) delete mode 100644 src/pages/Capabilities/Skills.tsx delete mode 100644 src/pages/Capabilities/components/SkillListItem.tsx delete mode 100644 src/pages/Capabilities/components/SkillScopeSelect.tsx delete mode 100644 src/pages/Capabilities/components/SkillUploadDialog.tsx diff --git a/src/components/Navigation/index.tsx b/src/components/Navigation/index.tsx index e016821b..ff3f0e15 100644 --- a/src/components/Navigation/index.tsx +++ b/src/components/Navigation/index.tsx @@ -58,11 +58,11 @@ export function VerticalNavigation({ value={value} defaultValue={initial} onValueChange={onValueChange} - className={cn('flex w-full gap-4', className)} + className={cn('flex-1 w-full', className)} > diff --git a/src/components/SearchInput/index.tsx b/src/components/SearchInput/index.tsx index ad13607c..b74f6075 100644 --- a/src/components/SearchInput/index.tsx +++ b/src/components/SearchInput/index.tsx @@ -12,29 +12,161 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Search } from 'lucide-react'; +import { TooltipSimple } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Search, X } from 'lucide-react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +export type SearchInputVariant = 'default' | 'icon'; + interface SearchInputProps { value: string; onChange: (e: React.ChangeEvent) => void; placeholder?: string; + variant?: SearchInputVariant; + /** Optional: called when user presses Enter in the field (e.g. to submit search) */ + onSearch?: () => void; + /** Tooltip for the search icon button (icon variant). Defaults to agents.search-tooltip */ + searchTooltip?: string; + /** Tooltip for the clear (X) button (icon variant). Defaults to agents.clear-search-tooltip */ + clearTooltip?: string; } +const COLLAPSED_WIDTH = 40; +const EXPANDED_WIDTH = 240; + export default function SearchInput({ value, onChange, placeholder, + variant = 'default', + onSearch, + searchTooltip, + clearTooltip, }: SearchInputProps) { const { t } = useTranslation(); + const inputRef = useRef(null); + const [userExpanded, setUserExpanded] = useState(false); + const isExpanded = userExpanded || value.length > 0; + + const expand = useCallback(() => { + setUserExpanded(true); + }, []); + + const collapse = useCallback(() => { + setUserExpanded(false); + onChange({ target: { value: '' } } as React.ChangeEvent); + }, [onChange]); + + useEffect(() => { + if (userExpanded && inputRef.current) { + const id = requestAnimationFrame(() => { + inputRef.current?.focus(); + }); + return () => cancelAnimationFrame(id); + } + }, [userExpanded]); + + const searchLabel = searchTooltip ?? t('agents.search-tooltip'); + const clearLabel = clearTooltip ?? t('agents.clear-search-tooltip'); + const place = placeholder ?? t('setting.search-mcp'); + + if (variant === 'icon') { + return ( + + + {!isExpanded ? ( + + + + + + ) : ( + + + + + { + if (value.length === 0) setUserExpanded(false); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + 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" + /> + + + + + )} + + + ); + } + return (
} />
diff --git a/src/components/WorkFlow/agents.tsx b/src/components/WorkFlow/agents.tsx new file mode 100644 index 00000000..0e62d337 --- /dev/null +++ b/src/components/WorkFlow/agents.tsx @@ -0,0 +1,106 @@ +// ========= 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 { Bird, CodeXml, FileText, Globe, Image } from 'lucide-react'; +import type { ReactNode } from 'react'; + +export type WorkflowAgentType = + | 'developer_agent' + | 'browser_agent' + | 'document_agent' + | 'multi_modal_agent' + | 'social_media_agent'; + +export interface AgentDisplayInfo { + name: string; + icon: ReactNode; + textColor: string; + bgColor: string; + shapeColor: string; + borderColor: string; + bgColorLight: string; +} + +export const agentMap: Record = { + developer_agent: { + name: 'Developer Agent', + icon: , + textColor: 'text-text-developer', + bgColor: 'bg-bg-fill-coding-active', + shapeColor: 'bg-bg-fill-coding-default', + borderColor: 'border-bg-fill-coding-active', + bgColorLight: 'bg-emerald-200', + }, + browser_agent: { + name: 'Browser Agent', + icon: , + textColor: 'text-blue-700', + bgColor: 'bg-bg-fill-browser-active', + shapeColor: 'bg-bg-fill-browser-default', + borderColor: 'border-bg-fill-browser-active', + bgColorLight: 'bg-blue-200', + }, + document_agent: { + name: 'Document Agent', + icon: , + textColor: 'text-yellow-700', + bgColor: 'bg-bg-fill-writing-active', + shapeColor: 'bg-bg-fill-writing-default', + borderColor: 'border-bg-fill-writing-active', + bgColorLight: 'bg-yellow-200', + }, + multi_modal_agent: { + name: 'Multi Modal Agent', + icon: , + textColor: 'text-fuchsia-700', + bgColor: 'bg-bg-fill-multimodal-active', + shapeColor: 'bg-bg-fill-multimodal-default', + borderColor: 'border-bg-fill-multimodal-active', + bgColorLight: 'bg-fuchsia-200', + }, + social_media_agent: { + name: 'Social Media Agent', + icon: , + textColor: 'text-purple-700', + bgColor: 'bg-violet-700', + shapeColor: 'bg-violet-300', + borderColor: 'border-violet-700', + bgColorLight: 'bg-purple-50', + }, +}; + +/** Ordered list of workflow agents (name + icon) for use in skill scope and elsewhere. */ +export const WORKFLOW_AGENT_LIST: { name: string; icon: ReactNode }[] = [ + { name: agentMap.developer_agent.name, icon: agentMap.developer_agent.icon }, + { name: agentMap.browser_agent.name, icon: agentMap.browser_agent.icon }, + { name: agentMap.document_agent.name, icon: agentMap.document_agent.icon }, + { + name: agentMap.multi_modal_agent.name, + icon: agentMap.multi_modal_agent.icon, + }, + { + name: agentMap.social_media_agent.name, + icon: agentMap.social_media_agent.icon, + }, +]; + +/** Get display info (name + icon) by agent name; returns undefined if not a workflow agent. */ +export function getWorkflowAgentDisplay( + agentName: string +): { name: string; icon: ReactNode } | undefined { + const entry = WORKFLOW_AGENT_LIST.find( + (a) => a.name.toLowerCase() === agentName.toLowerCase() + ); + return entry; +} diff --git a/src/components/WorkFlow/node.tsx b/src/components/WorkFlow/node.tsx index eafa4294..288cce69 100644 --- a/src/components/WorkFlow/node.tsx +++ b/src/components/WorkFlow/node.tsx @@ -23,17 +23,12 @@ import { } from '@/types/constants'; import { Handle, NodeResizer, Position, useReactFlow } from '@xyflow/react'; import { - Bird, Bot, Circle, CircleCheckBig, CircleSlash, CircleSlash2, - CodeXml, Ellipsis, - FileText, - Globe, - Image, LoaderCircle, SquareChevronLeft, SquareCode, @@ -52,6 +47,7 @@ import { } from '../ui/popover'; import ShinyText from '../ui/ShinyText/ShinyText'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; +import { agentMap } from './agents'; import { MarkDown } from './MarkDown'; interface NodeProps { @@ -295,54 +291,6 @@ export function Node({ id, data }: NodeProps) { data.onExpandChange(id, !isExpanded); }; - const agentMap = { - developer_agent: { - name: 'Developer Agent', - icon: , - textColor: 'text-text-developer', - bgColor: 'bg-bg-fill-coding-active', - shapeColor: 'bg-bg-fill-coding-default', - borderColor: 'border-bg-fill-coding-active', - bgColorLight: 'bg-emerald-200', - }, - browser_agent: { - name: 'Browser Agent', - icon: , - textColor: 'text-blue-700', - bgColor: 'bg-bg-fill-browser-active', - shapeColor: 'bg-bg-fill-browser-default', - borderColor: 'border-bg-fill-browser-active', - bgColorLight: 'bg-blue-200', - }, - document_agent: { - name: 'Document Agent', - icon: , - textColor: 'text-yellow-700', - bgColor: 'bg-bg-fill-writing-active', - shapeColor: 'bg-bg-fill-writing-default', - borderColor: 'border-bg-fill-writing-active', - bgColorLight: 'bg-yellow-200', - }, - multi_modal_agent: { - name: 'Multi Modal Agent', - icon: , - textColor: 'text-fuchsia-700', - bgColor: 'bg-bg-fill-multimodal-active', - shapeColor: 'bg-bg-fill-multimodal-default', - borderColor: 'border-bg-fill-multimodal-active', - bgColorLight: 'bg-fuchsia-200', - }, - social_media_agent: { - name: 'Social Media Agent', - icon: , - textColor: 'text-purple-700', - bgColor: 'bg-violet-700', - shapeColor: 'bg-violet-300', - borderColor: 'border-violet-700', - bgColorLight: 'bg-purple-50', - }, - }; - const agentToolkits = { developer_agent: [ '# Terminal & Shell ', diff --git a/src/components/ui/alertDialog.tsx b/src/components/ui/alertDialog.tsx index f5bd4f90..354952ad 100644 --- a/src/components/ui/alertDialog.tsx +++ b/src/components/ui/alertDialog.tsx @@ -55,7 +55,8 @@ export default function ConfirmModal({ initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="bg-white/5 z-100 alert-dialog fixed inset-0" + className="alert-dialog fixed inset-0 z-[99] bg-black/20" + style={{ backgroundColor: 'rgba(0, 0, 0, 0.2)' }} onClick={onClose} /> @@ -64,9 +65,9 @@ export default function ConfirmModal({ initial={{ opacity: 0, scale: 0.9, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9, y: 20 }} - className="alert-dialog-wrapper fixed max-w-md rounded-xl shadow-perfect" + className="alert-dialog-wrapper fixed left-1/2 top-1/2 z-[100] max-w-md rounded-xl -translate-x-1/2 -translate-y-1/2" > -
+
{title} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index d45c6c74..240b074c 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -46,6 +46,8 @@ const DialogOverlay = React.forwardRef< )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; +export type DialogOverlayVariant = 'default' | 'dark'; + // Size variants for dialog content const dialogContentVariants = cva( 'fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-0 border border-solid border-popup-border bg-popup-bg shadow-perfect duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-xl', @@ -72,6 +74,8 @@ interface DialogContentProps closeButtonClassName?: string; closeButtonIcon?: React.ReactNode; onClose?: () => void; + /** Overlay behind the dialog: 'default' (transparent) or 'dark' (black overlay) */ + overlayVariant?: DialogOverlayVariant; } const DialogContent = React.forwardRef< @@ -87,15 +91,27 @@ const DialogContent = React.forwardRef< closeButtonClassName, closeButtonIcon, onClose, + overlayVariant = 'default', ...props }, ref ) => ( - + {children} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index f081dfc2..77d93166 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -13,56 +13,190 @@ // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import * as TabsPrimitive from '@radix-ui/react-tabs'; +import { AnimatePresence, motion } from 'framer-motion'; import * as React from 'react'; import { cn } from '@/lib/utils'; +// Context for variant +const TabsContext = React.createContext<{ variant?: 'default' | 'outline' }>({ + variant: 'default', +}); + const Tabs = TabsPrimitive.Root; +type TabsListProps = React.ComponentPropsWithoutRef< + typeof TabsPrimitive.List +> & { + variant?: 'default' | 'outline'; +}; + const TabsList = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); + TabsListProps +>(({ className, variant = 'default', ...props }, ref) => { + const tabsListRef = React.useRef | null>(null) as React.MutableRefObject | null>; + const [sliderStyle, setSliderStyle] = React.useState({ left: 0, width: 0 }); + + // Update slider position when active tab changes + React.useLayoutEffect(() => { + if (variant !== 'outline' || !tabsListRef.current) return; + + const updateSlider = () => { + // Use requestAnimationFrame to ensure DOM has updated + requestAnimationFrame(() => { + const activeTab = tabsListRef.current?.querySelector( + '[data-state="active"][data-variant="outline"]' + ) as HTMLElement; + + if (activeTab && tabsListRef.current) { + const containerRect = tabsListRef.current.getBoundingClientRect(); + const tabRect = activeTab.getBoundingClientRect(); + + setSliderStyle({ + left: tabRect.left - containerRect.left, + width: tabRect.width, + }); + } + }); + }; + + // Initial update + updateSlider(); + + // Watch for changes + const observer = new MutationObserver(updateSlider); + if (tabsListRef.current) { + observer.observe(tabsListRef.current, { + attributes: true, + attributeFilter: ['data-state'], + subtree: true, + }); + } + + // Also listen for resize + window.addEventListener('resize', updateSlider); + + return () => { + observer.disconnect(); + window.removeEventListener('resize', updateSlider); + }; + }, [variant]); + + const combinedRef = React.useCallback( + (node: React.ElementRef | null) => { + if (typeof ref === 'function') { + ref(node); + } else if (ref && 'current' in ref) { + ( + ref as React.MutableRefObject | null> + ).current = node; + } + tabsListRef.current = node; + }, + [ref] + ); + + return ( + +
+ + {variant === 'outline' && sliderStyle.width > 0 && ( + + )} +
+
+ ); +}); TabsList.displayName = TabsPrimitive.List.displayName; +type TabsTriggerProps = React.ComponentPropsWithoutRef< + typeof TabsPrimitive.Trigger +> & { + variant?: 'default' | 'outline'; +}; + const TabsTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); + TabsTriggerProps +>(({ className, variant: propVariant, ...props }, ref) => { + const { variant: contextVariant } = React.useContext(TabsContext); + const variant = propVariant || contextVariant || 'default'; + + return ( + + ); +}); TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +>(({ className, children, ...props }, ref) => { + return ( + + + + {children} + + + + ); +}); TabsContent.displayName = TabsPrimitive.Content.displayName; export { Tabs, TabsContent, TabsList, TabsTrigger }; diff --git a/src/components/ui/toggle-group.tsx b/src/components/ui/toggle-group.tsx index fa4275ff..a50e9b3f 100644 --- a/src/components/ui/toggle-group.tsx +++ b/src/components/ui/toggle-group.tsx @@ -59,6 +59,7 @@ const ToggleGroupItem = React.forwardRef< variant: context.variant || variant, size: context.size || size, }), + 'bg-surface-primary border-border-disabled data-[state=on]:bg-surface-tertiary data-[state=on]:border-border-secondary', className )} {...props} diff --git a/src/i18n/locales/ar/capabilities.json b/src/i18n/locales/ar/agents.json similarity index 89% rename from src/i18n/locales/ar/capabilities.json rename to src/i18n/locales/ar/agents.json index cf201b3f..521624d1 100644 --- a/src/i18n/locales/ar/capabilities.json +++ b/src/i18n/locales/ar/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "ستتيح ميزات الذاكرة لوكلائك تذكر المعلومات المهمة عبر الجلسات.", "learn-more": "معرفة المزيد", "search-skills": "البحث عن المهارات...", + "search-tooltip": "بحث", + "clear-search-tooltip": "مسح البحث", "add": "إضافة", "your-skills": "مهاراتك", "example-skills": "مهارات نموذجية", @@ -36,6 +38,9 @@ "invalid-file-type": "نوع ملف غير صالح. يرجى رفع ملف .skill أو .md أو .txt أو .json.", "file-too-large": "الملف كبير جداً. الحد الأقصى للحجم هو 1 ميجابايت.", "file-read-error": "فشل في قراءة الملف. يرجى المحاولة مرة أخرى.", + "reupload-file": "إعادة رفع ملف", + "upload-error-invalid-format": "يجب أن يكون الملف .zip أو حزمة مهارة (.skill أو .md).", + "upload-error-invalid-yaml": "يجب أن يحدد SKILL.md الاسم والوصف بتنسيق YAML.", "skill-added-success": "تمت إضافة المهارة بنجاح!", "skill-add-error": "فشل في إضافة المهارة. يرجى المحاولة مرة أخرى.", "custom-skill": "مهارة مخصصة", diff --git a/src/i18n/locales/ar/index.ts b/src/i18n/locales/ar/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/ar/index.ts +++ b/src/i18n/locales/ar/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/de/capabilities.json b/src/i18n/locales/de/agents.json similarity index 89% rename from src/i18n/locales/de/capabilities.json rename to src/i18n/locales/de/agents.json index b480f337..18bc2dd2 100644 --- a/src/i18n/locales/de/capabilities.json +++ b/src/i18n/locales/de/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "Speicherfunktionen ermöglichen es Ihren Agenten, wichtige Informationen zwischen Sitzungen zu speichern.", "learn-more": "Mehr erfahren", "search-skills": "Fähigkeiten suchen...", + "search-tooltip": "Suchen", + "clear-search-tooltip": "Suche löschen", "add": "Hinzufügen", "your-skills": "Ihre Fähigkeiten", "example-skills": "Beispiel-Fähigkeiten", @@ -36,6 +38,9 @@ "invalid-file-type": "Ungültiger Dateityp. Bitte laden Sie eine .skill, .md, .txt oder .json Datei hoch.", "file-too-large": "Datei ist zu groß. Maximale Größe ist 1MB.", "file-read-error": "Datei konnte nicht gelesen werden. Bitte versuchen Sie es erneut.", + "reupload-file": "Datei erneut hochladen", + "upload-error-invalid-format": "Datei muss ein .zip- oder Skill-Paket (.skill oder .md) sein.", + "upload-error-invalid-yaml": "SKILL.md muss name und description im YAML-Format definieren.", "skill-added-success": "Fähigkeit erfolgreich hinzugefügt!", "skill-add-error": "Fähigkeit konnte nicht hinzugefügt werden. Bitte versuchen Sie es erneut.", "custom-skill": "Benutzerdefinierte Fähigkeit", diff --git a/src/i18n/locales/de/index.ts b/src/i18n/locales/de/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/de/index.ts +++ b/src/i18n/locales/de/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/en-us/capabilities.json b/src/i18n/locales/en-us/agents.json similarity index 87% rename from src/i18n/locales/en-us/capabilities.json rename to src/i18n/locales/en-us/agents.json index 847bb135..13f21533 100644 --- a/src/i18n/locales/en-us/capabilities.json +++ b/src/i18n/locales/en-us/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "Memory features will allow your agents to remember important information across sessions.", "learn-more": "Learn more", "search-skills": "Search skills...", + "search-tooltip": "Search", + "clear-search-tooltip": "Clear search", "add": "Add", "your-skills": "Your skills", "example-skills": "Example skills", @@ -36,6 +38,9 @@ "invalid-file-type": "Invalid file type. Please upload a .skill, .md, .txt, or .json file.", "file-too-large": "File is too large. Maximum size is 1MB.", "file-read-error": "Failed to read file. Please try again.", + "reupload-file": "Click to reupload a file", + "upload-error-invalid-format": "File must be a .zip or skill package (.skill or .md).", + "upload-error-invalid-yaml": "SKILL.md must define name and description using YAML format.", "skill-added-success": "Skill added successfully!", "skill-add-error": "Failed to add skill. Please try again.", "custom-skill": "Custom skill", diff --git a/src/i18n/locales/en-us/index.ts b/src/i18n/locales/en-us/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/en-us/index.ts +++ b/src/i18n/locales/en-us/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/en-us/setting.json b/src/i18n/locales/en-us/setting.json index cf1f63be..d27ae2f8 100644 --- a/src/i18n/locales/en-us/setting.json +++ b/src/i18n/locales/en-us/setting.json @@ -416,5 +416,6 @@ "preferred-ide": "Preferred IDE", "preferred-ide-description": "Choose which application to use when opening agent project folders.", - "system-file-manager": "System File Manager" + "system-file-manager": "System File Manager", + "agents": "Agents" } diff --git a/src/i18n/locales/es/capabilities.json b/src/i18n/locales/es/agents.json similarity index 88% rename from src/i18n/locales/es/capabilities.json rename to src/i18n/locales/es/agents.json index 35b6f330..c16ad29b 100644 --- a/src/i18n/locales/es/capabilities.json +++ b/src/i18n/locales/es/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "Las funciones de memoria permitirán que sus agentes recuerden información importante entre sesiones.", "learn-more": "Más información", "search-skills": "Buscar habilidades...", + "search-tooltip": "Buscar", + "clear-search-tooltip": "Borrar búsqueda", "add": "Agregar", "your-skills": "Sus habilidades", "example-skills": "Habilidades de ejemplo", @@ -36,6 +38,9 @@ "invalid-file-type": "Tipo de archivo no válido. Por favor cargue un archivo .skill, .md, .txt o .json.", "file-too-large": "El archivo es demasiado grande. El tamaño máximo es 1MB.", "file-read-error": "Error al leer el archivo. Por favor intente de nuevo.", + "reupload-file": "Volver a subir un archivo", + "upload-error-invalid-format": "El archivo debe ser un .zip o paquete de habilidad (.skill o .md).", + "upload-error-invalid-yaml": "SKILL.md debe definir name y description en formato YAML.", "skill-added-success": "¡Habilidad agregada exitosamente!", "skill-add-error": "Error al agregar la habilidad. Por favor intente de nuevo.", "custom-skill": "Habilidad personalizada", diff --git a/src/i18n/locales/es/index.ts b/src/i18n/locales/es/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/es/index.ts +++ b/src/i18n/locales/es/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/fr/capabilities.json b/src/i18n/locales/fr/agents.json similarity index 88% rename from src/i18n/locales/fr/capabilities.json rename to src/i18n/locales/fr/agents.json index 3c046d46..45a45760 100644 --- a/src/i18n/locales/fr/capabilities.json +++ b/src/i18n/locales/fr/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "Les fonctionnalités de mémoire permettront à vos agents de mémoriser des informations importantes entre les sessions.", "learn-more": "En savoir plus", "search-skills": "Rechercher des compétences...", + "search-tooltip": "Rechercher", + "clear-search-tooltip": "Effacer la recherche", "add": "Ajouter", "your-skills": "Vos compétences", "example-skills": "Exemples de compétences", @@ -36,6 +38,9 @@ "invalid-file-type": "Type de fichier non valide. Veuillez télécharger un fichier .skill, .md, .txt ou .json.", "file-too-large": "Le fichier est trop volumineux. La taille maximale est de 1 Mo.", "file-read-error": "Échec de la lecture du fichier. Veuillez réessayer.", + "reupload-file": "Téléverser à nouveau un fichier", + "upload-error-invalid-format": "Le fichier doit être un .zip ou un package de compétence (.skill ou .md).", + "upload-error-invalid-yaml": "SKILL.md doit définir name et description au format YAML.", "skill-added-success": "Compétence ajoutée avec succès !", "skill-add-error": "Échec de l'ajout de la compétence. Veuillez réessayer.", "custom-skill": "Compétence personnalisée", diff --git a/src/i18n/locales/fr/index.ts b/src/i18n/locales/fr/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/fr/index.ts +++ b/src/i18n/locales/fr/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/it/capabilities.json b/src/i18n/locales/it/agents.json similarity index 88% rename from src/i18n/locales/it/capabilities.json rename to src/i18n/locales/it/agents.json index 0e8405f2..b9855f58 100644 --- a/src/i18n/locales/it/capabilities.json +++ b/src/i18n/locales/it/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "Le funzionalità di memoria permetteranno ai tuoi agenti di ricordare informazioni importanti tra le sessioni.", "learn-more": "Scopri di più", "search-skills": "Cerca competenze...", + "search-tooltip": "Cerca", + "clear-search-tooltip": "Cancella ricerca", "add": "Aggiungi", "your-skills": "Le tue competenze", "example-skills": "Competenze di esempio", @@ -36,6 +38,9 @@ "invalid-file-type": "Tipo di file non valido. Carica un file .skill, .md, .txt o .json.", "file-too-large": "Il file è troppo grande. La dimensione massima è 1MB.", "file-read-error": "Impossibile leggere il file. Riprova.", + "reupload-file": "Carica di nuovo un file", + "upload-error-invalid-format": "Il file deve essere un .zip o un pacchetto skill (.skill o .md).", + "upload-error-invalid-yaml": "SKILL.md deve definire name e description in formato YAML.", "skill-added-success": "Competenza aggiunta con successo!", "skill-add-error": "Impossibile aggiungere la competenza. Riprova.", "custom-skill": "Competenza personalizzata", diff --git a/src/i18n/locales/it/index.ts b/src/i18n/locales/it/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/it/index.ts +++ b/src/i18n/locales/it/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/ja/capabilities.json b/src/i18n/locales/ja/agents.json similarity index 87% rename from src/i18n/locales/ja/capabilities.json rename to src/i18n/locales/ja/agents.json index 6d40bc09..eef61ac5 100644 --- a/src/i18n/locales/ja/capabilities.json +++ b/src/i18n/locales/ja/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "メモリ機能により、エージェントがセッション間で重要な情報を記憶できるようになります。", "learn-more": "詳細を見る", "search-skills": "スキルを検索...", + "search-tooltip": "検索", + "clear-search-tooltip": "検索をクリア", "add": "追加", "your-skills": "あなたのスキル", "example-skills": "サンプルスキル", @@ -36,6 +38,9 @@ "invalid-file-type": "無効なファイル形式です。.skill、.md、.txt、または .json ファイルをアップロードしてください。", "file-too-large": "ファイルが大きすぎます。最大サイズは 1MB です。", "file-read-error": "ファイルの読み込みに失敗しました。もう一度お試しください。", + "reupload-file": "ファイルを再アップロード", + "upload-error-invalid-format": "ファイルは .zip またはスキルパッケージ(.skill または .md)である必要があります。", + "upload-error-invalid-yaml": "SKILL.md では YAML 形式で name と description を定義する必要があります。", "skill-added-success": "スキルが正常に追加されました!", "skill-add-error": "スキルの追加に失敗しました。もう一度お試しください。", "custom-skill": "カスタムスキル", diff --git a/src/i18n/locales/ja/index.ts b/src/i18n/locales/ja/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/ja/index.ts +++ b/src/i18n/locales/ja/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/ko/capabilities.json b/src/i18n/locales/ko/agents.json similarity index 87% rename from src/i18n/locales/ko/capabilities.json rename to src/i18n/locales/ko/agents.json index faca71e9..5649fb6d 100644 --- a/src/i18n/locales/ko/capabilities.json +++ b/src/i18n/locales/ko/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "메모리 기능을 통해 에이전트가 세션 간에 중요한 정보를 기억할 수 있습니다.", "learn-more": "자세히 알아보기", "search-skills": "스킬 검색...", + "search-tooltip": "검색", + "clear-search-tooltip": "검색 지우기", "add": "추가", "your-skills": "내 스킬", "example-skills": "예제 스킬", @@ -36,6 +38,9 @@ "invalid-file-type": "잘못된 파일 유형입니다. .skill, .md, .txt 또는 .json 파일을 업로드해 주세요.", "file-too-large": "파일이 너무 큽니다. 최대 크기는 1MB입니다.", "file-read-error": "파일 읽기에 실패했습니다. 다시 시도해 주세요.", + "reupload-file": "파일 다시 업로드", + "upload-error-invalid-format": "파일은 .zip 또는 스킬 패키지(.skill 또는 .md)여야 합니다.", + "upload-error-invalid-yaml": "SKILL.md에는 YAML 형식으로 name과 description이 정의되어 있어야 합니다.", "skill-added-success": "스킬이 성공적으로 추가되었습니다!", "skill-add-error": "스킬 추가에 실패했습니다. 다시 시도해 주세요.", "custom-skill": "사용자 정의 스킬", diff --git a/src/i18n/locales/ko/index.ts b/src/i18n/locales/ko/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/ko/index.ts +++ b/src/i18n/locales/ko/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/ru/capabilities.json b/src/i18n/locales/ru/agents.json similarity index 88% rename from src/i18n/locales/ru/capabilities.json rename to src/i18n/locales/ru/agents.json index 8ea9de71..ef7c027f 100644 --- a/src/i18n/locales/ru/capabilities.json +++ b/src/i18n/locales/ru/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "Функции памяти позволят вашим агентам запоминать важную информацию между сеансами.", "learn-more": "Узнать больше", "search-skills": "Поиск навыков...", + "search-tooltip": "Поиск", + "clear-search-tooltip": "Очистить поиск", "add": "Добавить", "your-skills": "Ваши навыки", "example-skills": "Примеры навыков", @@ -36,6 +38,9 @@ "invalid-file-type": "Неверный тип файла. Пожалуйста, загрузите файл .skill, .md, .txt или .json.", "file-too-large": "Файл слишком большой. Максимальный размер 1MB.", "file-read-error": "Не удалось прочитать файл. Пожалуйста, попробуйте снова.", + "reupload-file": "Загрузить файл снова", + "upload-error-invalid-format": "Файл должен быть .zip или пакетом навыка (.skill или .md).", + "upload-error-invalid-yaml": "В SKILL.md должны быть указаны name и description в формате YAML.", "skill-added-success": "Навык успешно добавлен!", "skill-add-error": "Не удалось добавить навык. Пожалуйста, попробуйте снова.", "custom-skill": "Пользовательский навык", diff --git a/src/i18n/locales/ru/index.ts b/src/i18n/locales/ru/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/ru/index.ts +++ b/src/i18n/locales/ru/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/zh-Hans/capabilities.json b/src/i18n/locales/zh-Hans/agents.json similarity index 87% rename from src/i18n/locales/zh-Hans/capabilities.json rename to src/i18n/locales/zh-Hans/agents.json index 81099d71..adedc198 100644 --- a/src/i18n/locales/zh-Hans/capabilities.json +++ b/src/i18n/locales/zh-Hans/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "记忆功能将允许您的智能体在会话之间记住重要信息。", "learn-more": "了解更多", "search-skills": "搜索技能...", + "search-tooltip": "搜索", + "clear-search-tooltip": "清除搜索", "add": "添加", "your-skills": "您的技能", "example-skills": "示例技能", @@ -36,6 +38,9 @@ "invalid-file-type": "无效的文件类型。请上传 .skill、.md、.txt 或 .json 文件。", "file-too-large": "文件太大。最大大小为 1MB。", "file-read-error": "读取文件失败。请重试。", + "reupload-file": "重新上传文件", + "upload-error-invalid-format": "文件必须为 .zip 或技能包(.skill 或 .md)。", + "upload-error-invalid-yaml": "SKILL.md 必须使用 YAML 格式定义 name 和 description。", "skill-added-success": "技能添加成功!", "skill-add-error": "添加技能失败。请重试。", "custom-skill": "自定义技能", diff --git a/src/i18n/locales/zh-Hans/index.ts b/src/i18n/locales/zh-Hans/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/zh-Hans/index.ts +++ b/src/i18n/locales/zh-Hans/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/i18n/locales/zh-Hant/capabilities.json b/src/i18n/locales/zh-Hant/agents.json similarity index 87% rename from src/i18n/locales/zh-Hant/capabilities.json rename to src/i18n/locales/zh-Hant/agents.json index dff4182b..2884243d 100644 --- a/src/i18n/locales/zh-Hant/capabilities.json +++ b/src/i18n/locales/zh-Hant/agents.json @@ -7,6 +7,8 @@ "memory-coming-soon-description": "記憶功能將允許您的智能體在會話之間記住重要資訊。", "learn-more": "了解更多", "search-skills": "搜尋技能...", + "search-tooltip": "搜尋", + "clear-search-tooltip": "清除搜尋", "add": "新增", "your-skills": "您的技能", "example-skills": "範例技能", @@ -36,6 +38,9 @@ "invalid-file-type": "無效的檔案類型。請上傳 .skill、.md、.txt 或 .json 檔案。", "file-too-large": "檔案太大。最大大小為 1MB。", "file-read-error": "讀取檔案失敗。請重試。", + "reupload-file": "重新上傳檔案", + "upload-error-invalid-format": "檔案必須為 .zip 或技能套件(.skill 或 .md)。", + "upload-error-invalid-yaml": "SKILL.md 必須使用 YAML 格式定義 name 與 description。", "skill-added-success": "技能新增成功!", "skill-add-error": "新增技能失敗。請重試。", "custom-skill": "自訂技能", diff --git a/src/i18n/locales/zh-Hant/index.ts b/src/i18n/locales/zh-Hant/index.ts index f00c65d9..dc2a149a 100644 --- a/src/i18n/locales/zh-Hant/index.ts +++ b/src/i18n/locales/zh-Hant/index.ts @@ -12,7 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import capabilities from './capabilities.json'; +import agents from './agents.json'; import chat from './chat.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; @@ -20,7 +20,7 @@ import setting from './setting.json'; import update from './update.json'; import workforce from './workforce.json'; export default { - capabilities, + agents, layout, dashboard, workforce, diff --git a/src/lib/skillToolkit.ts b/src/lib/skillToolkit.ts index f6cd2c43..3f4ef789 100644 --- a/src/lib/skillToolkit.ts +++ b/src/lib/skillToolkit.ts @@ -46,9 +46,11 @@ export function splitFrontmatter(contents: string): { frontmatter: string | null; body: string; } { - const lines = contents.split('\n'); + // Strip BOM and leading whitespace/newlines so the first `---` is detected + const cleaned = contents.replace(/^\uFEFF/, '').trimStart(); + const lines = cleaned.split('\n'); if (!lines.length || lines[0].trim() !== FRONTMATTER_DELIM) { - return { frontmatter: null, body: contents }; + return { frontmatter: null, body: cleaned }; } for (let i = 1; i < lines.length; i++) { if (lines[i].trim() === FRONTMATTER_DELIM) { @@ -57,7 +59,7 @@ export function splitFrontmatter(contents: string): { return { frontmatter, body }; } } - return { frontmatter: null, body: contents }; + return { frontmatter: null, body: cleaned }; } /** Simple YAML-like parse for "name:" and "description:" (first-level keys only). */ @@ -68,7 +70,8 @@ function parseSimpleYaml(text: string): Record { const match = line.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/); if (match) { const value = match[2].trim(); - out[match[1]] = value.replace(/^['"]|['"]$/g, '').trim(); + // Lowercase key so `Name:` / `name:` / `NAME:` all work + out[match[1].toLowerCase()] = value.replace(/^['"]|['"]$/g, '').trim(); } } return out; diff --git a/src/pages/Capabilities/Memory.tsx b/src/pages/Agents/Memory.tsx similarity index 56% rename from src/pages/Capabilities/Memory.tsx rename to src/pages/Agents/Memory.tsx index 2451d606..2647c67c 100644 --- a/src/pages/Capabilities/Memory.tsx +++ b/src/pages/Agents/Memory.tsx @@ -19,25 +19,27 @@ export default function Memory() { const { t } = useTranslation(); return ( -
+
{/* Header Section */} -
+
- {t('capabilities.memory')} + {t('agents.memory')}
- {/* Coming Soon Card */} -
-
- + {/* Content Section */} +
+
+
+ +
+

+ {t('layout.coming-soon')} +

+

+ {t('agents.memory-coming-soon-description')} +

-

- {t('layout.coming-soon')} -

-

- {t('capabilities.memory-coming-soon-description')} -

); diff --git a/src/pages/Setting/Models.tsx b/src/pages/Agents/Models.tsx similarity index 100% rename from src/pages/Setting/Models.tsx rename to src/pages/Agents/Models.tsx diff --git a/src/pages/Agents/Skills.tsx b/src/pages/Agents/Skills.tsx new file mode 100644 index 00000000..1f982a2a --- /dev/null +++ b/src/pages/Agents/Skills.tsx @@ -0,0 +1,192 @@ +// ========= 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 SearchInput from '@/components/SearchInput'; +import { Button } from '@/components/ui/button'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { useSkillsStore, type Skill } from '@/store/skillsStore'; +import { Plus } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import SkillDeleteDialog from './components/SkillDeleteDialog'; +import SkillListItem from './components/SkillListItem'; +import SkillUploadDialog from './components/SkillUploadDialog'; + +export default function Skills() { + const { t } = useTranslation(); + const { skills, syncFromDisk } = useSkillsStore(); + const [searchQuery, setSearchQuery] = useState(''); + const [uploadDialogOpen, setUploadDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [skillToDelete, setSkillToDelete] = useState(null); + + // On first mount, sync skills from local SKILL.md files + useEffect(() => { + // No-op on web; in Electron this will scan ~/.eigent/skills + syncFromDisk(); + }, [syncFromDisk]); + + const yourSkills = useMemo(() => { + return skills + .filter((skill) => !skill.isExample) + .filter( + (skill) => + skill.name.toLowerCase().includes(searchQuery.toLowerCase()) || + skill.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [skills, searchQuery]); + + const exampleSkills = useMemo(() => { + return skills + .filter((skill) => skill.isExample) + .filter( + (skill) => + skill.name.toLowerCase().includes(searchQuery.toLowerCase()) || + skill.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [skills, searchQuery]); + + const handleDeleteClick = (skill: Skill) => { + setSkillToDelete(skill); + setDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = () => { + setDeleteDialogOpen(false); + setSkillToDelete(null); + }; + + const handleDeleteCancel = () => { + setDeleteDialogOpen(false); + setSkillToDelete(null); + }; + + return ( +
+ {/* Header Section */} +
+
+ {t('agents.skills')} +
+
+ + {/* Content Section */} +
+
+ +
+ + + {t('agents.your-skills')} + + + {t('agents.example-skills')} + + +
+ setSearchQuery(e.target.value)} + placeholder={t('agents.search-skills')} + /> + +
+
+ + {yourSkills.length === 0 ? ( + setUploadDialogOpen(true) : undefined + } + /> + ) : ( +
+ {yourSkills.map((skill) => ( + handleDeleteClick(skill)} + /> + ))} +
+ )} +
+ + {exampleSkills.length === 0 ? ( + + ) : ( +
+ {exampleSkills.map((skill) => ( + handleDeleteClick(skill)} + /> + ))} +
+ )} +
+
+
+
+ + {/* Upload Dialog */} + setUploadDialogOpen(false)} + /> + + {/* Delete Dialog */} + +
+ ); +} diff --git a/src/pages/Capabilities/components/SkillDeleteDialog.tsx b/src/pages/Agents/components/SkillDeleteDialog.tsx similarity index 57% rename from src/pages/Capabilities/components/SkillDeleteDialog.tsx rename to src/pages/Agents/components/SkillDeleteDialog.tsx index 19c39764..1aec0f88 100644 --- a/src/pages/Capabilities/components/SkillDeleteDialog.tsx +++ b/src/pages/Agents/components/SkillDeleteDialog.tsx @@ -12,13 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import { - Dialog, - DialogContent, - DialogContentSection, - DialogFooter, - DialogHeader, -} from '@/components/ui/dialog'; +import ConfirmModal from '@/components/ui/alertDialog'; import { useSkillsStore, type Skill } from '@/store/skillsStore'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; @@ -42,32 +36,23 @@ export default function SkillDeleteDialog({ const handleDelete = () => { if (skill) { deleteSkill(skill.id); - toast.success(t('capabilities.skill-deleted-success')); + toast.success(t('agents.skill-deleted-success')); } onConfirm(); }; return ( - !isOpen && onCancel()}> - - - -

- {t('capabilities.delete-skill-confirmation', { - name: skill?.name || '', - })} -

-
- -
-
+ ); } diff --git a/src/pages/Agents/components/SkillListItem.tsx b/src/pages/Agents/components/SkillListItem.tsx new file mode 100644 index 00000000..d4446741 --- /dev/null +++ b/src/pages/Agents/components/SkillListItem.tsx @@ -0,0 +1,278 @@ +// ========= 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 { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + getWorkflowAgentDisplay, + WORKFLOW_AGENT_LIST, +} from '@/components/WorkFlow/agents'; +import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; +import { useWorkerList } from '@/store/authStore'; +import { useSkillsStore, type Skill } from '@/store/skillsStore'; +import { + Bot, + Check, + ChevronRight, + Ellipsis, + MessageSquare, + Plus, + Trash2, + Users, +} from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +interface SkillListItemDefaultProps { + variant?: 'default'; + skill: Skill; + onDelete: () => void; + message?: never; + addButtonText?: never; + onAddClick?: never; +} + +interface SkillListItemPlaceholderProps { + variant: 'placeholder'; + skill?: never; + onDelete?: never; + message: string; + addButtonText?: string; + onAddClick?: () => void; +} + +type SkillListItemProps = + | SkillListItemDefaultProps + | SkillListItemPlaceholderProps; + +export default function SkillListItem(props: SkillListItemProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { updateSkill } = useSkillsStore(); + const { projectStore } = useChatStoreAdapter(); + const workerList = useWorkerList(); + const [scopeOpen, setScopeOpen] = useState(false); + + const allAgents = useMemo(() => { + const workflowNames = WORKFLOW_AGENT_LIST.map((a) => a.name); + const workerNames = workerList.map((w) => w.name); + const combined = [...workflowNames]; + workerNames.forEach((name) => { + if (!combined.includes(name)) { + combined.push(name); + } + }); + return combined; + }, [workerList]); + + if (props.variant === 'placeholder') { + const isClickable = props.onAddClick != null; + return ( +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + props.onAddClick?.(); + } + } + : undefined + } + aria-label={isClickable ? props.addButtonText : undefined} + > +

{props.message}

+ {isClickable && } +
+ ); + } + + const { skill, onDelete } = props; + + const formatDate = (timestamp: number) => { + const date = new Date(timestamp); + const now = new Date(); + const diffDays = Math.floor( + (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24) + ); + + if (diffDays === 0) { + return t('layout.today'); + } else if (diffDays === 1) { + return t('layout.yesterday'); + } else if (diffDays < 30) { + return `${diffDays} ${t('layout.days-ago')}`; + } else { + return date.toLocaleDateString(); + } + }; + + const handleScopeChange = (scope: { + isGlobal: boolean; + selectedAgents: string[]; + }) => { + updateSkill(skill.id, { scope }); + }; + + const isAllAgentsSelected = skill.scope.isGlobal; + + const handleToggleAllAgents = () => { + if (isAllAgentsSelected) { + // When user unselects "All agents", clear all individual selections + handleScopeChange({ + isGlobal: false, + selectedAgents: [], + }); + } else { + // When user selects "All agents", select every available agent + handleScopeChange({ + isGlobal: true, + selectedAgents: allAgents, + }); + } + }; + + const handleToggleAgent = (agentName: string) => { + const isSelected = skill.scope.selectedAgents.includes(agentName); + const newSelectedAgents = isSelected + ? skill.scope.selectedAgents.filter((a) => a !== agentName) + : [...skill.scope.selectedAgents, agentName]; + handleScopeChange({ + isGlobal: false, + selectedAgents: newSelectedAgents, + }); + }; + + const handleTryInChat = () => { + projectStore?.createProject('new project'); + const prompt = `I just added the {{${skill.name}}} skill for Eigent, can you make something amazing with this skill?`; + navigate(`/?skill_prompt=${encodeURIComponent(prompt)}`); + }; + + return ( +
+ {/* Row 1: Name / Actions */} +
+
+ + {skill.name} + +
+ +
+ + {t('agents.added')} {formatDate(skill.addedAt)} + + + + + + + + + {t('agents.try-in-chat')} + + + + {t('layout.delete')} + + + +
+
+ + {/* Row 2: Description full width / wrapped */} +
+

+ {skill.description} +

+
+ + {/* Row 3: Added time / Skill scope */} +
+ + + {scopeOpen && ( +
+ {/* All agents as first tab; then each agent toggle */} + + + {allAgents.map((agentName) => { + const isSelected = skill.scope.selectedAgents.includes(agentName); + const display = getWorkflowAgentDisplay(agentName); + const icon = display?.icon ?? ( + + ); + return ( + + ); + })} +
+ )} +
+
+ ); +} diff --git a/src/pages/Agents/components/SkillUploadDialog.tsx b/src/pages/Agents/components/SkillUploadDialog.tsx new file mode 100644 index 00000000..3a50e0a1 --- /dev/null +++ b/src/pages/Agents/components/SkillUploadDialog.tsx @@ -0,0 +1,371 @@ +// ========= 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 { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogContentSection, + DialogHeader, +} from '@/components/ui/dialog'; +import { parseSkillMd } from '@/lib/skillToolkit'; +import { useSkillsStore } from '@/store/skillsStore'; +import { AlertCircle, File, Upload, X } from 'lucide-react'; +import { useCallback, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; + +interface SkillUploadDialogProps { + open: boolean; + onClose: () => void; +} + +export default function SkillUploadDialog({ + open, + onClose, +}: SkillUploadDialogProps) { + const { t } = useTranslation(); + const { addSkill, syncFromDisk } = useSkillsStore(); + const [isDragging, setIsDragging] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [fileContent, setFileContent] = useState(''); + const [_isUploading, setIsUploading] = useState(false); + const fileInputRef = useRef(null); + const [isZip, setIsZip] = useState(false); + const [uploadError, setUploadError] = useState< + 'invalid_format' | 'invalid_yaml' | null + >(null); + + const handleClose = useCallback(() => { + setSelectedFile(null); + setFileContent(''); + setIsDragging(false); + setIsZip(false); + setUploadError(null); + onClose(); + }, [onClose]); + + const handleUpload = useCallback( + async ( + fileArg?: File, + options?: { isZipOverride?: boolean; contentOverride?: string } + ) => { + const fileToUse = fileArg ?? selectedFile; + if (!fileToUse) return; + + const isZipToUse = options?.isZipOverride ?? isZip; + const fileContentToUse = options?.contentOverride ?? fileContent; + + setIsUploading(true); + try { + // Zip import: read file in renderer and send buffer to main (no path in sandbox) + if (isZipToUse) { + if (!(window as any).electronAPI?.skillImportZip) { + toast.error(t('agents.skill-add-error')); + return; + } + let buffer: ArrayBuffer; + try { + buffer = await fileToUse.arrayBuffer(); + } catch { + toast.error(t('agents.file-read-error')); + return; + } + const result = await (window as any).electronAPI.skillImportZip( + buffer + ); + if (!result?.success) { + toast.error(result?.error || t('agents.skill-add-error')); + return; + } + await syncFromDisk(); + toast.success(t('agents.skill-added-success')); + handleClose(); + return; + } + + if (!fileContentToUse) return; + + const fileName = fileToUse.name.replace(/\.[^/.]+$/, ''); + + // Prefer SKILL.md frontmatter (name + description) at upload time + const meta = parseSkillMd(fileContentToUse); + let name = meta?.name ?? fileName; + let description = meta?.description ?? ''; + + // Fallback: no frontmatter — use first heading and first paragraph + if (!meta && fileContentToUse.startsWith('#')) { + const lines = fileContentToUse.split('\n'); + const headingMatch = lines[0].match(/^#\s+(.+)/); + if (headingMatch) name = headingMatch[1]; + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (line && !line.startsWith('#')) { + description = line; + break; + } + } + } + + addSkill({ + name, + description: description || t('agents.custom-skill'), + filePath: fileToUse.name, + fileContent: fileContentToUse, + scope: { isGlobal: true, selectedAgents: [] }, + enabled: true, + }); + + toast.success(t('agents.skill-added-success')); + handleClose(); + } catch (_error) { + toast.error(t('agents.skill-add-error')); + } finally { + setIsUploading(false); + } + }, + [addSkill, fileContent, handleClose, isZip, selectedFile, syncFromDisk, t] + ); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }, []); + + const processFile = useCallback( + async (file: File) => { + // Only .zip or skill package (.skill, .md) are valid + const skillPackageExtensions = ['.zip', '.skill', '.md']; + const extension = file.name + .substring(file.name.lastIndexOf('.')) + .toLowerCase(); + + if (!skillPackageExtensions.includes(extension)) { + setSelectedFile(file); + setUploadError('invalid_format'); + return; + } + + // Validate file size (max 5MB to allow small zip bundles) + if (file.size > 5 * 1024 * 1024) { + toast.error(t('agents.file-too-large')); + return; + } + + try { + setUploadError(null); + setSelectedFile(file); + if (extension === '.zip') { + setIsZip(true); + setFileContent(''); + await handleUpload(file, { + isZipOverride: true, + contentOverride: '', + }); + } else { + const content = await file.text(); + setIsZip(false); + setFileContent(content); + // .skill / .md must have YAML frontmatter (name + description) + const meta = parseSkillMd(content); + if (!meta) { + setUploadError('invalid_yaml'); + return; + } + await handleUpload(file, { + isZipOverride: false, + contentOverride: content, + }); + } + } catch (_error) { + toast.error(t('agents.file-read-error')); + } + }, + [handleUpload, t] + ); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const files = e.dataTransfer.files; + if (files.length > 0) { + processFile(files[0]); + } + }, + [processFile] + ); + + const handleFileSelect = (e: React.ChangeEvent) => { + const files = e.target.files; + if (files && files.length > 0) { + processFile(files[0]); + } + }; + + const handleRemoveFile = () => { + setSelectedFile(null); + setFileContent(''); + setUploadError(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const errorMessage = + uploadError === 'invalid_format' + ? t('agents.upload-error-invalid-format') + : uploadError === 'invalid_yaml' + ? t('agents.upload-error-invalid-yaml') + : null; + + return ( + !isOpen && handleClose()}> + + + +
+ {/* Drop Zone */} +
fileInputRef.current?.click()} + > + + + {selectedFile ? ( +
+
+
+ +
+
+ + {selectedFile.name} + +
+ +
+ + + {uploadError + ? t('agents.reupload-file') + : `${(selectedFile.size / 1024).toFixed(1)} KB`} + +
+ ) : ( +
+
+ +
+
+ + {t('agents.drag-and-drop')} + + + {t('agents.or-click-to-browse')} + +
+
+ )} +
+ + {/* Error notice */} + {uploadError && errorMessage && ( +
+ + + {errorMessage} + +
+ )} + + {/* File Requirements */} +
+ + {t('agents.file-requirements')} + + + + {t('agents.file-requirements-detail-1')} + + + + {t('agents.file-requirements-detail-2')} + +
+
+
+
+
+ ); +} diff --git a/src/pages/Capabilities/index.tsx b/src/pages/Agents/index.tsx similarity index 78% rename from src/pages/Capabilities/index.tsx rename to src/pages/Agents/index.tsx index e2db2bc1..d8543f9d 100644 --- a/src/pages/Capabilities/index.tsx +++ b/src/pages/Agents/index.tsx @@ -15,10 +15,10 @@ import VerticalNavigation, { type VerticalNavItem, } from '@/components/Navigation'; -import Models from '@/pages/Setting/Models'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import Memory from './Memory'; +import Models from './Models'; import Skills from './Skills'; export default function Capabilities() { @@ -32,11 +32,11 @@ export default function Capabilities() { }, { id: 'skills', - name: t('capabilities.skills'), + name: t('agents.skills'), }, { id: 'memory', - name: t('capabilities.memory'), + name: t('agents.memory'), }, ]; @@ -45,17 +45,15 @@ export default function Capabilities() { }; return ( -
-
-
+
+
+
({ value: menu.id, label: ( - - {menu.name} - + {menu.name} ), })) as VerticalNavItem[] } @@ -68,7 +66,7 @@ export default function Capabilities() {
-
+
{activeTab === 'models' && } {activeTab === 'skills' && } {activeTab === 'memory' && } diff --git a/src/pages/Capabilities/Skills.tsx b/src/pages/Capabilities/Skills.tsx deleted file mode 100644 index aadf1ba1..00000000 --- a/src/pages/Capabilities/Skills.tsx +++ /dev/null @@ -1,220 +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 SearchInput from '@/components/SearchInput'; -import { Button } from '@/components/ui/button'; -import { useSkillsStore, type Skill } from '@/store/skillsStore'; -import { ChevronDown, ChevronUp, Plus } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import SkillDeleteDialog from './components/SkillDeleteDialog'; -import SkillListItem from './components/SkillListItem'; -import SkillUploadDialog from './components/SkillUploadDialog'; - -export default function Skills() { - const { t } = useTranslation(); - const { skills, syncFromDisk } = useSkillsStore(); - const [searchQuery, setSearchQuery] = useState(''); - const [uploadDialogOpen, setUploadDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [skillToDelete, setSkillToDelete] = useState(null); - const [collapsedYourSkills, setCollapsedYourSkills] = useState(false); - const [collapsedExampleSkills, setCollapsedExampleSkills] = useState(false); - - // On first mount, sync skills from local SKILL.md files - useEffect(() => { - // No-op on web; in Electron this will scan ~/.eigent/skills - syncFromDisk(); - }, [syncFromDisk]); - - const yourSkills = useMemo(() => { - return skills - .filter((skill) => !skill.isExample) - .filter( - (skill) => - skill.name.toLowerCase().includes(searchQuery.toLowerCase()) || - skill.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); - }, [skills, searchQuery]); - - const exampleSkills = useMemo(() => { - return skills - .filter((skill) => skill.isExample) - .filter( - (skill) => - skill.name.toLowerCase().includes(searchQuery.toLowerCase()) || - skill.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); - }, [skills, searchQuery]); - - const handleDeleteClick = (skill: Skill) => { - setSkillToDelete(skill); - setDeleteDialogOpen(true); - }; - - const handleDeleteConfirm = () => { - setDeleteDialogOpen(false); - setSkillToDelete(null); - }; - - const handleDeleteCancel = () => { - setDeleteDialogOpen(false); - setSkillToDelete(null); - }; - - return ( -
- {/* Header Section */} -
-
- {t('capabilities.skills')} -
-
- setSearchQuery(e.target.value)} - placeholder={t('capabilities.search-skills')} - /> - -
-
- - {/* Your Skills Section */} -
-
- - {t('capabilities.your-skills')} - - -
- {!collapsedYourSkills && ( - <> - {yourSkills.length === 0 ? ( -
-

- {searchQuery - ? t('capabilities.no-skills-found') - : t('capabilities.no-your-skills')} -

- {!searchQuery && ( - - )} -
- ) : ( -
- {yourSkills.map((skill) => ( - handleDeleteClick(skill)} - /> - ))} -
- )} - - )} -
- - {/* Example Skills Section */} -
-
- - {t('capabilities.example-skills')} - - -
- {!collapsedExampleSkills && ( - <> - {exampleSkills.length === 0 ? ( -
-

- {searchQuery - ? t('capabilities.no-skills-found') - : t('capabilities.no-example-skills')} -

-
- ) : ( -
- {exampleSkills.map((skill) => ( - handleDeleteClick(skill)} - /> - ))} -
- )} - - )} -
- - {/* Upload Dialog */} - setUploadDialogOpen(false)} - /> - - {/* Delete Dialog */} - -
- ); -} diff --git a/src/pages/Capabilities/components/SkillListItem.tsx b/src/pages/Capabilities/components/SkillListItem.tsx deleted file mode 100644 index 9c14b990..00000000 --- a/src/pages/Capabilities/components/SkillListItem.tsx +++ /dev/null @@ -1,136 +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 { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Switch } from '@/components/ui/switch'; -import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; -import { useSkillsStore, type Skill } from '@/store/skillsStore'; -import { Ellipsis, MessageSquare, Trash2 } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import SkillScopeSelect from './SkillScopeSelect'; - -interface SkillListItemProps { - skill: Skill; - onDelete: () => void; -} - -export default function SkillListItem({ skill, onDelete }: SkillListItemProps) { - const { t } = useTranslation(); - const navigate = useNavigate(); - const { toggleSkill, updateSkill } = useSkillsStore(); - const { projectStore } = useChatStoreAdapter(); - - const formatDate = (timestamp: number) => { - const date = new Date(timestamp); - const now = new Date(); - const diffDays = Math.floor( - (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24) - ); - - if (diffDays === 0) { - return t('layout.today'); - } else if (diffDays === 1) { - return t('layout.yesterday'); - } else if (diffDays < 30) { - return `${diffDays} ${t('layout.days-ago')}`; - } else { - return date.toLocaleDateString(); - } - }; - - const handleToggle = () => { - toggleSkill(skill.id); - }; - - const handleScopeChange = (scope: { - isGlobal: boolean; - selectedAgents: string[]; - }) => { - updateSkill(skill.id, { scope }); - }; - - const handleTryInChat = () => { - projectStore?.createProject('new project'); - const prompt = `I just added the {{${skill.name}}} skill for Eigent, can you make something amazing with this skill?`; - navigate(`/?skill_prompt=${encodeURIComponent(prompt)}`); - }; - - return ( -
- {/* Left side: Status dot + Info */} -
- {/* Status indicator dot */} -
- {/* Name and description */} -
- - {skill.name} - -

- {skill.description} -

- - {t('capabilities.added')} {formatDate(skill.addedAt)} - -
-
- - {/* Right side: Controls */} -
- {/* Scope Select */} - - - {/* Enable/Disable Switch */} - - - {/* More Actions Menu (三个点) */} - - - - - - - - {t('capabilities.try-in-chat')} - - - - {t('layout.delete')} - - - -
-
- ); -} diff --git a/src/pages/Capabilities/components/SkillScopeSelect.tsx b/src/pages/Capabilities/components/SkillScopeSelect.tsx deleted file mode 100644 index ea32bc1e..00000000 --- a/src/pages/Capabilities/components/SkillScopeSelect.tsx +++ /dev/null @@ -1,181 +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 { Button } from '@/components/ui/button'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { useWorkerList } from '@/store/authStore'; -import type { SkillScope } from '@/store/skillsStore'; -import { Check, ChevronDown, Globe, Users } from 'lucide-react'; -import { useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -// Default agent types that are always available -const DEFAULT_AGENTS = [ - 'Developer Agent', - 'Browser Agent', - 'Multi-modal Agent', - 'Document Agent', -]; - -// Special identifier for Global option -const GLOBAL_OPTION = '__GLOBAL__'; - -interface SkillScopeSelectProps { - scope: SkillScope; - onChange: (scope: SkillScope) => void; - disabled?: boolean; -} - -export default function SkillScopeSelect({ - scope, - onChange, - disabled = false, -}: SkillScopeSelectProps) { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const workerList = useWorkerList(); - - // Combine default agents with user-configured workers - // New workers will automatically appear since workerList is reactive - const allAgents = useMemo(() => { - const workerNames = workerList.map((w) => w.name); - // Combine default agents with workers, avoiding duplicates - const combined = [...DEFAULT_AGENTS]; - workerNames.forEach((name) => { - if (!combined.includes(name)) { - combined.push(name); - } - }); - return combined; - }, [workerList]); - - // Handle toggle for any option (including Global) - const handleToggle = (optionName: string) => { - if (optionName === GLOBAL_OPTION) { - // Toggle Global - onChange({ - isGlobal: !scope.isGlobal, - selectedAgents: scope.selectedAgents, - }); - } else { - // Toggle agent - const isSelected = scope.selectedAgents.includes(optionName); - let newSelectedAgents: string[]; - - if (isSelected) { - newSelectedAgents = scope.selectedAgents.filter( - (a) => a !== optionName - ); - } else { - newSelectedAgents = [...scope.selectedAgents, optionName]; - } - - onChange({ - isGlobal: scope.isGlobal, - selectedAgents: newSelectedAgents, - }); - } - }; - - const getDisplayText = () => { - const selections: string[] = []; - - if (scope.isGlobal) { - selections.push(t('capabilities.global')); - } - - selections.push(...scope.selectedAgents); - - if (selections.length === 0) { - return t('capabilities.select-scope'); - } - if (selections.length === 1) { - return selections[0]; - } - return `${selections.length} ${t('capabilities.selected')}`; - }; - - const hasSelection = scope.isGlobal || scope.selectedAgents.length > 0; - - return ( -
- - {t('capabilities.skill-scope')} - - - - - - -
- {/* Global Option - same level as agents */} - - - {/* Agent/Worker List - Multi-select, same level as Global */} - {allAgents.map((agentName) => { - const isSelected = scope.selectedAgents.includes(agentName); - return ( - - ); - })} -
-
-
-
- ); -} diff --git a/src/pages/Capabilities/components/SkillUploadDialog.tsx b/src/pages/Capabilities/components/SkillUploadDialog.tsx deleted file mode 100644 index e8e0a576..00000000 --- a/src/pages/Capabilities/components/SkillUploadDialog.tsx +++ /dev/null @@ -1,298 +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 { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogContentSection, - DialogFooter, - DialogHeader, -} from '@/components/ui/dialog'; -import { parseSkillMd } from '@/lib/skillToolkit'; -import { useSkillsStore } from '@/store/skillsStore'; -import { File, Upload, X } from 'lucide-react'; -import { useCallback, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; - -interface SkillUploadDialogProps { - open: boolean; - onClose: () => void; -} - -export default function SkillUploadDialog({ - open, - onClose, -}: SkillUploadDialogProps) { - const { t } = useTranslation(); - const { addSkill, syncFromDisk } = useSkillsStore(); - const [isDragging, setIsDragging] = useState(false); - const [selectedFile, setSelectedFile] = useState(null); - const [fileContent, setFileContent] = useState(''); - const [isUploading, setIsUploading] = useState(false); - const fileInputRef = useRef(null); - const [isZip, setIsZip] = useState(false); - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(true); - }, []); - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - }, []); - - const processFile = useCallback( - async (file: File) => { - // Validate file type - const validExtensions = ['.skill', '.md', '.txt', '.json', '.zip']; - const extension = file.name - .substring(file.name.lastIndexOf('.')) - .toLowerCase(); - - if (!validExtensions.includes(extension)) { - toast.error(t('capabilities.invalid-file-type')); - return; - } - - // Validate file size (max 5MB to allow small zip bundles) - if (file.size > 5 * 1024 * 1024) { - toast.error(t('capabilities.file-too-large')); - return; - } - - try { - setSelectedFile(file); - if (extension === '.zip') { - // For zip, we don't read content in renderer; main process will import - setIsZip(true); - setFileContent(''); - } else { - const content = await file.text(); - setIsZip(false); - setFileContent(content); - } - } catch (_error) { - toast.error(t('capabilities.file-read-error')); - } - }, - [t] - ); - - const handleDrop = useCallback( - (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - const files = e.dataTransfer.files; - if (files.length > 0) { - processFile(files[0]); - } - }, - [processFile] - ); - - const handleFileSelect = (e: React.ChangeEvent) => { - const files = e.target.files; - if (files && files.length > 0) { - processFile(files[0]); - } - }; - - const handleUpload = async () => { - if (!selectedFile) return; - - setIsUploading(true); - try { - // Zip import: read file in renderer and send buffer to main (no path in sandbox) - if (isZip) { - if (!(window as any).electronAPI?.skillImportZip) { - toast.error(t('capabilities.skill-add-error')); - return; - } - let buffer: ArrayBuffer; - try { - buffer = await selectedFile.arrayBuffer(); - } catch { - toast.error(t('capabilities.file-read-error')); - return; - } - const result = await (window as any).electronAPI.skillImportZip(buffer); - if (!result?.success) { - toast.error(result?.error || t('capabilities.skill-add-error')); - return; - } - await syncFromDisk(); - toast.success(t('capabilities.skill-added-success')); - handleClose(); - return; - } - - if (!fileContent) return; - - const fileName = selectedFile.name.replace(/\.[^/.]+$/, ''); - - // Prefer SKILL.md frontmatter (name + description) at upload time - const meta = parseSkillMd(fileContent); - let name = meta?.name ?? fileName; - let description = meta?.description ?? ''; - - // Fallback: no frontmatter — use first heading and first paragraph - if (!meta && fileContent.startsWith('#')) { - const lines = fileContent.split('\n'); - const headingMatch = lines[0].match(/^#\s+(.+)/); - if (headingMatch) name = headingMatch[1]; - for (let i = 1; i < lines.length; i++) { - const line = lines[i].trim(); - if (line && !line.startsWith('#')) { - description = line; - break; - } - } - } - - addSkill({ - name, - description: description || t('capabilities.custom-skill'), - filePath: selectedFile.name, - fileContent, - scope: { isGlobal: true, selectedAgents: [] }, - enabled: true, - }); - - toast.success(t('capabilities.skill-added-success')); - handleClose(); - } catch (_error) { - toast.error(t('capabilities.skill-add-error')); - } finally { - setIsUploading(false); - } - }; - - const handleClose = () => { - setSelectedFile(null); - setFileContent(''); - setIsDragging(false); - setIsZip(false); - onClose(); - }; - - const handleRemoveFile = () => { - setSelectedFile(null); - setFileContent(''); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - }; - - return ( - !isOpen && handleClose()}> - - - -
- {/* Drop Zone */} -
fileInputRef.current?.click()} - > - - - {selectedFile ? ( -
-
- -
-
-

- {selectedFile.name} -

-

- {(selectedFile.size / 1024).toFixed(1)} KB -

-
- -
- ) : ( -
-
- -
-
-

- {t('capabilities.drag-and-drop')} -

-

- {t('capabilities.or-click-to-browse')} -

-
-
- )} -
- - {/* File Requirements */} -
-

- {t('capabilities.file-requirements')} -

-
    -
  • - - {t('capabilities.file-requirements-detail-1')} -
  • -
  • - - {t('capabilities.file-requirements-detail-2')} -
  • -
-
-
-
- -
-
- ); -} diff --git a/src/pages/History.tsx b/src/pages/History.tsx index 934e8f2d..c2786b0c 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -12,6 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +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 { Settings } from '@/components/animate-ui/icons/settings'; @@ -27,11 +28,11 @@ import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import Project from '@/pages/Dashboard/Project'; import Setting from '@/pages/Setting'; import { useAuthStore } from '@/store/authStore'; -import { Layers, Plus } from 'lucide-react'; +import { Plus } from 'lucide-react'; import { useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import Capabilities from './Capabilities'; +import Agents from './Agents'; import Browser from './Dashboard/Browser'; import MCP from './Setting/MCP'; @@ -42,7 +43,7 @@ const VALID_TABS = [ 'settings', 'mcp_tools', 'browser', - 'capabilities', + 'agents', ] as const; type TabType = (typeof VALID_TABS)[number]; @@ -172,11 +173,11 @@ export default function Home() { } + icon={} > - {t('layout.capabilities')} + {t('setting.agents')} } {activeTab === 'browser' && } {activeTab === 'settings' && } - {activeTab === 'capabilities' && } + {activeTab === 'agents' && }
); } diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index c20f8303..7f070829 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -144,7 +144,7 @@ export default function Home() { // }); // chatStore.setSnapshots(chatStore.activeTaskId as string, list); }) - .catch((error) => { + .catch((error: unknown) => { console.error('capture webview error:', error); }); }); diff --git a/src/pages/Setting.tsx b/src/pages/Setting.tsx index cc097fa0..768bde7b 100644 --- a/src/pages/Setting.tsx +++ b/src/pages/Setting.tsx @@ -20,10 +20,9 @@ import VerticalNavigation, { } from '@/components/Navigation'; import useAppVersion from '@/hooks/use-app-version'; import General from '@/pages/Setting/General'; -import Models from '@/pages/Setting/Models'; import Privacy from '@/pages/Setting/Privacy'; import { useAuthStore } from '@/store/authStore'; -import { Fingerprint, Settings, TagIcon, TextSelect } from 'lucide-react'; +import { Fingerprint, Settings, TagIcon } from 'lucide-react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -49,12 +48,6 @@ export default function Setting() { icon: Fingerprint, path: '/setting/privacy', }, - { - id: 'models', - name: t('setting.models'), - icon: TextSelect, - path: '/setting/models', - }, ]; // Initialize tab from URL once, then manage locally without routing const getCurrentTab = () => { @@ -131,7 +124,6 @@ export default function Setting() {
{activeTab === 'general' && } {activeTab === 'privacy' && } - {activeTab === 'models' && }