import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen' import type { ComboboxChangeEventDetails } from '@langgenius/dify-ui/combobox' import type { NodeDefault } from '../types' import type { AgentRosterNodeData } from './types' import { cn } from '@langgenius/dify-ui/cn' import { Combobox, ComboboxInput, ComboboxInputGroup, ComboboxItem, ComboboxItemText, ComboboxList, ComboboxStatus, } from '@langgenius/dify-ui/combobox' import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover' import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' import { useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import Badge from '@/app/components/base/badge' import { useHooksStore } from '@/app/components/workflow/hooks-store' import Link from '@/next/link' import { consoleQuery } from '@/service/client' import BlockIcon from '../block-icon' const AGENT_SELECTOR_PAGE_SIZE = 8 type AgentSelectorOption = AgentInviteOptionResponse | AgentSelectorActionOption type AgentSelectorActionOption = 'start-from-scratch' | 'manage-in-agent-console' export function AgentSelectorContent({ open, onOpenChange, onSelect, onStartFromScratch, }: { open: boolean onOpenChange: (open: boolean) => void onSelect: (agent: AgentRosterNodeData) => void onStartFromScratch?: () => void }) { const { t } = useTranslation(['agentV2', 'common', 'workflow']) const appId = useHooksStore((s) => s.configsMap?.flowId) const [searchText, setSearchText] = useState('') const debouncedSearchText = useDebounce(searchText.trim(), { wait: 300 }) const agentsQuery = useQuery({ ...consoleQuery.agent.inviteOptions.get.queryOptions({ input: { query: { limit: AGENT_SELECTOR_PAGE_SIZE, page: 1, ...(appId ? { app_id: appId } : {}), ...(debouncedSearchText ? { keyword: debouncedSearchText } : {}), }, }, }), staleTime: 0, }) const agents = agentsQuery.data?.data ?? [] const actionOptions: AgentSelectorActionOption[] = onStartFromScratch ? ['start-from-scratch', 'manage-in-agent-console'] : ['manage-in-agent-console'] const options: AgentSelectorOption[] = [...agents, ...actionOptions] const getOptionLabel = (option: AgentSelectorOption) => { if (isAgentSelectorActionOption(option)) { if (option === 'start-from-scratch') return t(($) => $['roster.nodeSelector.startFromScratch'], { ns: 'agentV2' }) return t(($) => $['roster.nodeSelector.manageInAgentConsole'], { ns: 'agentV2' }) } return option.name } const handleInputValueChange = (nextSearchText: string, details: ComboboxChangeEventDetails) => { if (details.reason !== 'item-press') setSearchText(nextSearchText) } const handleValueChange = (option: AgentSelectorOption | null) => { if (!option) return if (isAgentSelectorActionOption(option)) { if (option === 'start-from-scratch') onStartFromScratch?.() return } if (!option.active_config_snapshot_id) { toast.error(t(($) => $['nodes.agent.modelNotSelected'], { ns: 'workflow' })) return } onSelect(toAgentRosterNodeData(option)) } const handleOpenChange = (nextOpen: boolean) => { if (!nextOpen) onOpenChange(false) } const isLoading = agentsQuery.isPending return (
filter={null} inputValue={searchText} items={options} itemToStringLabel={getOptionLabel} itemToStringValue={getAgentSelectorOptionValue} open={open} value={null} onInputValueChange={handleInputValueChange} onOpenChange={handleOpenChange} onValueChange={handleValueChange} >
$['roster.searchLabel'], { ns: 'agentV2' })} placeholder={t(($) => $['roster.nodeSelector.searchPlaceholder'], { ns: 'agentV2' })} className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled" />
{isLoading && ( $.loading, { ns: 'common' })} /> )} {!isLoading && agentsQuery.isError && ( {t(($) => $['roster.loadingError'], { ns: 'agentV2' })} )} {!isLoading && !agentsQuery.isError && ( <> {agents.length === 0 && ( {debouncedSearchText ? t(($) => $['roster.emptySearch'], { ns: 'agentV2' }) : t(($) => $['roster.empty'], { ns: 'agentV2' })} )} {agents.map((agent) => ( ))} )}
{actionOptions.map((option) => ( ))}
) } function AgentSelectorLoadingSkeleton({ label }: { label: string }) { return ( {label}
{['skeleton-1', 'skeleton-2', 'skeleton-3', 'skeleton-4'].map((key, index) => (
))}
) } function getAgentSelectorOptionValue(option: AgentSelectorOption) { if (isAgentSelectorActionOption(option)) return option return option.id } function isAgentSelectorActionOption( option: AgentSelectorOption, ): option is AgentSelectorActionOption { return typeof option === 'string' } function toAgentRosterNodeData(agent: AgentInviteOptionResponse): AgentRosterNodeData { return { description: agent.description, icon: agent.icon, icon_background: agent.icon_background, icon_type: agent.icon_type, id: agent.id, name: agent.name, role: agent.role, } } function AgentSelectorAvatar({ agent }: { agent: AgentInviteOptionResponse }) { return ( ) } function AgentSelectorItem({ agent }: { agent: AgentInviteOptionResponse }) { return ( {agent.name} {agent.role || agent.description} ) } function AgentSelectorActionItem({ option }: { option: AgentSelectorActionOption }) { const { t } = useTranslation('agentV2') const isStartFromScratch = option === 'start-from-scratch' return ( ) } className="flex min-h-7 w-full grid-cols-none items-center gap-2 rounded-md px-2 py-1.5 text-left system-sm-regular text-text-secondary hover:bg-state-base-hover hover:text-text-secondary focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid focus-visible:outline-hidden data-highlighted:bg-state-base-hover data-highlighted:text-text-secondary" > {isStartFromScratch ? t(($) => $['roster.nodeSelector.startFromScratch']) : t(($) => $['roster.nodeSelector.manageInAgentConsole'])} ) } export function AgentBlockItem({ block, onSelect, onStartFromScratch, }: { block: NodeDefault onSelect: (agent: AgentRosterNodeData) => void onStartFromScratch: () => void }) { const { t } = useTranslation(['agentV2', 'common']) const [open, setOpen] = useState(false) const handleSelect = (agent: AgentRosterNodeData) => { setOpen(false) onSelect(agent) } return ( {block.metaData.title} $['menus.status'], { ns: 'common' })} className="ml-2 shrink-0" /> } /> {t(($) => $['roster.nodeSelector.dialogLabel'], { ns: 'agentV2' })} { setOpen(false) onStartFromScratch() }} /> ) }