From bc3199bf29504309d1906e08c80022d89e854cc4 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 27 Mar 2026 14:51:15 +0800 Subject: [PATCH] feat(web): add icons/emoji to selectors, sync bot enable status and plugin list in sidebar - Bot adapter selector: show adapter icon in trigger and dropdown items - Knowledge engine selector: show plugin icon derived from plugin_id - Pipeline binding selector: show pipeline emoji in trigger and dropdown items - Knowledge base selectors (single/multi): show KB emoji in all views - Sidebar bot entries: show green/gray status dot on adapter icon for enable/disable state - Sidebar plugin list: sync after install/uninstall from all entry points (PluginInstalledComponent, plugins page, marketplace page) - Pipeline form: add cursor-pointer to left-side tab list buttons - Clean up unused onBotDeleted prop from BotForm --- web/src/app/home/bots/BotDetailContent.tsx | 2 - .../home/bots/components/bot-form/BotForm.tsx | 60 ++++++++++++++++-- .../bots/components/bot-form/ChooseEntity.ts | 1 + .../dynamic-form/DynamicFormItemComponent.tsx | 41 ++++++++++++- .../components/home-sidebar/HomeSidebar.tsx | 30 ++++++--- .../home-sidebar/SidebarDataContext.tsx | 3 + .../knowledge/components/kb-form/KBForm.tsx | 61 +++++++++++++++---- web/src/app/home/market/page.tsx | 3 + .../pipeline-form/PipelineFormComponent.tsx | 2 +- .../PluginInstalledComponent.tsx | 3 + web/src/app/home/plugins/page.tsx | 3 + 11 files changed, 178 insertions(+), 31 deletions(-) diff --git a/web/src/app/home/bots/BotDetailContent.tsx b/web/src/app/home/bots/BotDetailContent.tsx index a613f1f7..8b1ad9cd 100644 --- a/web/src/app/home/bots/BotDetailContent.tsx +++ b/web/src/app/home/bots/BotDetailContent.tsx @@ -144,7 +144,6 @@ export default function BotDetailContent({ id }: { id: string }) { @@ -238,7 +237,6 @@ export default function BotDetailContent({ id }: { id: string }) { diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index b464d0fc..36e4bcf9 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -65,13 +65,11 @@ const getFormSchema = (t: (key: string) => string) => export default function BotForm({ initBotId, onFormSubmit, - onBotDeleted, onNewBotCreated, onDirtyChange, }: { initBotId?: string; onFormSubmit: (value: z.infer>) => void; - onBotDeleted: () => void; onNewBotCreated: (botId: string) => void; onDirtyChange?: (dirty: boolean) => void; }) { @@ -234,6 +232,7 @@ export default function BotForm({ return { label: item.name, value: item.uuid ?? '', + emoji: item.emoji, }; }), ); @@ -461,13 +460,40 @@ export default function BotForm({ - + {field.value && field.value !== '__none__' ? ( + (() => { + const selectedKb = knowledgeBases.find( + (kb) => kb.uuid === field.value, + ); + return ( +
+ {selectedKb?.emoji && ( + + {selectedKb.emoji} + + )} + {selectedKb?.name ?? field.value} +
+ ); + })() + ) : ( + + )}
@@ -553,7 +571,12 @@ export default function DynamicFormItemComponent({ {engineName} {kbs.map((base) => ( - {base.name} +
+ {base.emoji && ( + {base.emoji} + )} + {base.name} +
))}
@@ -597,6 +620,11 @@ export default function DynamicFormItemComponent({
+ {currentKb.emoji && ( + + {currentKb.emoji} + + )} {currentKb.name} {currentKb.knowledge_engine?.name && ( @@ -686,7 +714,14 @@ export default function DynamicFormItemComponent({ aria-label={`Select ${base.name}`} />
-
{base.name}
+
+ {base.emoji && ( + + {base.emoji} + + )} + {base.name} +
{base.description && (
{base.description} diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index 64bb1b2b..ffc27354 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -77,6 +77,7 @@ import { CollapsibleTrigger, } from '@/components/ui/collapsible'; import { ChevronRight, Plus } from 'lucide-react'; +import { cn } from '@/lib/utils'; import { useSidebarData, SidebarEntityItem } from './SidebarDataContext'; // Compare two version strings, returns true if v1 > v2 @@ -316,6 +317,7 @@ function NavItems({ const canCreate = CREATABLE_CATEGORIES.includes(config.id); const isCollapseOnly = COLLAPSIBLE_ONLY_CATEGORIES.includes(config.id); const isPlugin = config.id === 'plugins'; + const isBot = config.id === 'bots'; const isActive = selectedChild?.id === config.id || pathname === routePrefix || @@ -391,9 +393,9 @@ function NavItems({ > { e.preventDefault(); router.push(itemRoute); @@ -404,11 +406,23 @@ function NavItems({ {item.emoji} ) : item.iconURL ? ( - + + + {isBot && ( + + )} + ) : null} {item.name} {item.debug && ( diff --git a/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx b/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx index 0351d626..5d562c73 100644 --- a/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx +++ b/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx @@ -18,6 +18,8 @@ export interface SidebarEntityItem { emoji?: string; iconURL?: string; updatedAt?: string; // ISO timestamp for sorting by most recently edited + // Bot-specific fields + enabled?: boolean; // Plugin-specific fields installSource?: string; installInfo?: Record; @@ -63,6 +65,7 @@ export function SidebarDataProvider({ name: bot.name, iconURL: httpClient.getAdapterIconURL(bot.adapter), updatedAt: bot.updated_at, + enabled: bot.enable ?? true, })), ); } catch (error) { diff --git a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx index 4f6b52fe..26279db0 100644 --- a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx +++ b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx @@ -310,19 +310,58 @@ export default function KBForm({ value={field.value} > - + {field.value ? ( + (() => { + const [author, name] = field.value.split('/'); + const engine = ragEngines.find( + (e) => e.plugin_id === field.value, + ); + return ( +
+ + + {engine + ? extractI18nObject(engine.name) + : field.value} + +
+ ); + })() + ) : ( + + )}
- {ragEngines.map((engine) => ( - - {extractI18nObject(engine.name)} - - ))} + {ragEngines.map((engine) => { + const [author, name] = engine.plugin_id.split('/'); + return ( + +
+ + {extractI18nObject(engine.name)} +
+
+ ); + })}
diff --git a/web/src/app/home/market/page.tsx b/web/src/app/home/market/page.tsx index e6867222..01cb7cd7 100644 --- a/web/src/app/home/market/page.tsx +++ b/web/src/app/home/market/page.tsx @@ -16,6 +16,7 @@ import { systemInfo } from '@/app/infra/http/HttpClient'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { PluginV4 } from '@/app/infra/entities/plugin'; +import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; enum PluginInstallStatus { ASK_CONFIRM = 'ask_confirm', @@ -39,6 +40,7 @@ export default function MarketplacePage() { function MarketplaceContent() { const { t } = useTranslation(); + const { refreshPlugins } = useSidebarData(); const [modalOpen, setModalOpen] = useState(false); const [installInfo, setInstallInfo] = useState>({}); const [pluginInstallStatus, setPluginInstallStatus] = @@ -83,6 +85,7 @@ function MarketplaceContent() { alreadySuccess = true; } setModalOpen(false); + refreshPlugins(); } } }); diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx index 5ed87dd4..eb42dcb3 100644 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -456,7 +456,7 @@ export default function PipelineFormComponent({ type="button" onClick={() => setActiveSection(section.name)} className={cn( - 'w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors text-left', + 'w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors text-left cursor-pointer', activeSection === section.name ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-muted hover:text-foreground', diff --git a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx index dce443f0..7c496d8e 100644 --- a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx @@ -22,6 +22,7 @@ import { useTranslation } from 'react-i18next'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { toast } from 'sonner'; import { useAsyncTask, AsyncTaskStatus } from '@/hooks/useAsyncTask'; +import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; export interface PluginInstalledComponentRef { refreshPluginList: () => void; @@ -37,6 +38,7 @@ const PluginInstalledComponent = forwardRef( (props, ref) => { const { t } = useTranslation(); const router = useRouter(); + const { refreshPlugins } = useSidebarData(); const [pluginList, setPluginList] = useState([]); const [showOperationModal, setShowOperationModal] = useState(false); const [operationType, setOperationType] = useState( @@ -54,6 +56,7 @@ const PluginInstalledComponent = forwardRef( toast.success(successMessage); setShowOperationModal(false); getPluginList(); + refreshPlugins(); }, onError: () => { // Error is already handled in the hook state diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 8e2faf29..bc85fdfc 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -51,6 +51,7 @@ import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { systemInfo } from '@/app/infra/http/HttpClient'; import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api'; +import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; enum PluginInstallStatus { WAIT_INPUT = 'wait_input', @@ -93,6 +94,7 @@ export default function PluginConfigPage() { function PluginListView() { const { t } = useTranslation(); const router = useRouter(); + const { refreshPlugins } = useSidebarData(); const [modalOpen, setModalOpen] = useState(false); const [installSource, setInstallSource] = useState('local'); const [installInfo] = useState>({}); // eslint-disable-line @typescript-eslint/no-explicit-any @@ -167,6 +169,7 @@ function PluginListView() { resetGithubState(); setModalOpen(false); pluginInstalledRef.current?.refreshPluginList(); + refreshPlugins(); } } });