### What problem does this PR solve? Fix: Improve Agent templates functionality and fix some UI style issues #3221 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.20.0
| @@ -132,7 +132,7 @@ export function NextMessageInput({ | |||
| onChange={onInputChange} | |||
| placeholder="Type your message here..." | |||
| className="field-sizing-content min-h-10 w-full resize-none border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 dark:bg-transparent" | |||
| disabled={isUploading || disabled} | |||
| disabled={isUploading || disabled || sendLoading} | |||
| onKeyDown={handleKeyDown} | |||
| /> | |||
| <div className="flex items-center justify-between gap-1.5"> | |||
| @@ -7,6 +7,7 @@ import { | |||
| PropsWithChildren, | |||
| memo, | |||
| useCallback, | |||
| useContext, | |||
| useEffect, | |||
| useMemo, | |||
| useState, | |||
| @@ -15,6 +16,7 @@ import { | |||
| import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks'; | |||
| import { INodeEvent } from '@/hooks/use-send-message'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { AgentChatContext } from '@/pages/agent/context'; | |||
| import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workFlowTimeline'; | |||
| import { IMessage } from '@/pages/chat/interface'; | |||
| import { isEmpty } from 'lodash'; | |||
| @@ -74,6 +76,14 @@ function MessageItem({ | |||
| const { visible, hideModal, showModal } = useSetModalState(); | |||
| const [clickedDocumentId, setClickedDocumentId] = useState(''); | |||
| const { setLastSendLoadingFunc } = useContext(AgentChatContext); | |||
| useEffect(() => { | |||
| if (typeof setLastSendLoadingFunc === 'function') { | |||
| setLastSendLoadingFunc(loading, item.id); | |||
| } | |||
| }, [loading, setLastSendLoadingFunc, item.id]); | |||
| const referenceDocuments = useMemo(() => { | |||
| const docs = reference?.doc_aggs ?? {}; | |||
| @@ -115,7 +125,6 @@ function MessageItem({ | |||
| ) : ( | |||
| <AssistantIcon /> | |||
| ))} | |||
| <section className="flex-col gap-2 flex-1"> | |||
| <div className="space-x-1"> | |||
| {isAssistant ? ( | |||
| @@ -177,6 +186,7 @@ function MessageItem({ | |||
| )} | |||
| currentMessageId={item.id} | |||
| canvasId={conversationId} | |||
| sendLoading={loading} | |||
| /> | |||
| </div> | |||
| )} | |||
| @@ -1308,6 +1308,15 @@ This delimiter is used to split the input text into several text pieces echo of | |||
| export: 'Export', | |||
| seconds: 'Seconds', | |||
| subject: 'Subject', | |||
| tag: 'Tag', | |||
| tagPlaceholder: 'Please enter tag', | |||
| descriptionPlaceholder: 'Please enter description', | |||
| line: 'Single-line text', | |||
| paragraph: 'Paragraph text', | |||
| options: 'Dropdown options', | |||
| file: 'File upload', | |||
| integer: 'Number', | |||
| boolean: 'Boolean', | |||
| }, | |||
| llmTools: { | |||
| bad_calculator: { | |||
| @@ -13,7 +13,7 @@ import { | |||
| } from '@xyflow/react'; | |||
| import '@xyflow/react/dist/style.css'; | |||
| import { NotebookPen } from 'lucide-react'; | |||
| import { useCallback, useEffect } from 'react'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { ChatSheet } from '../chat/chat-sheet'; | |||
| import { AgentBackground } from '../components/background'; | |||
| @@ -132,6 +132,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { | |||
| const { showLogSheet, logSheetVisible, hideLogSheet } = useShowLogSheet({ | |||
| setCurrentMessageId, | |||
| }); | |||
| const [lastSendLoading, setLastSendLoading] = useState(false); | |||
| const { handleBeforeDelete } = useBeforeDelete(); | |||
| @@ -152,7 +153,13 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { | |||
| clearEventList(); | |||
| } | |||
| }, [chatVisible, clearEventList]); | |||
| const setLastSendLoadingFunc = (loading: boolean, messageId: string) => { | |||
| if (messageId === currentMessageId) { | |||
| setLastSendLoading(loading); | |||
| } else { | |||
| setLastSendLoading(false); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles.canvasWrapper}> | |||
| <svg | |||
| @@ -243,7 +250,9 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { | |||
| </AgentInstanceContext.Provider> | |||
| )} | |||
| {chatVisible && ( | |||
| <AgentChatContext.Provider value={{ showLogSheet }}> | |||
| <AgentChatContext.Provider | |||
| value={{ showLogSheet, setLastSendLoadingFunc }} | |||
| > | |||
| <AgentChatLogContext.Provider | |||
| value={{ addEventList, setCurrentMessageId }} | |||
| > | |||
| @@ -264,6 +273,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { | |||
| currentEventListWithoutMessageById | |||
| } | |||
| currentMessageId={currentMessageId} | |||
| sendLoading={lastSendLoading} | |||
| ></LogSheet> | |||
| )} | |||
| </div> | |||
| @@ -251,12 +251,12 @@ export const useSendAgentMessage = ( | |||
| }, | |||
| [ | |||
| agentId, | |||
| sessionId, | |||
| send, | |||
| clearUploadResponseList, | |||
| inputs, | |||
| beginParams, | |||
| uploadResponseList, | |||
| sessionId, | |||
| setValue, | |||
| removeLatestMessage, | |||
| ], | |||
| @@ -22,7 +22,7 @@ export const AgentInstanceContext = createContext<AgentInstanceContextType>( | |||
| type AgentChatContextType = Pick< | |||
| ReturnType<typeof useShowLogSheet>, | |||
| 'showLogSheet' | |||
| >; | |||
| > & { setLastSendLoadingFunc: (loading: boolean, messageId: string) => void }; | |||
| export const AgentChatContext = createContext<AgentChatContextType>( | |||
| {} as AgentChatContextType, | |||
| @@ -123,7 +123,7 @@ function BeginForm({ node }: INextOperatorForm) { | |||
| )} | |||
| /> | |||
| )} | |||
| {enablePrologue && ( | |||
| {mode === AgentDialogueMode.Conversational && enablePrologue && ( | |||
| <FormField | |||
| control={form.control} | |||
| name={'prologue'} | |||
| @@ -175,7 +175,6 @@ function BeginForm({ node }: INextOperatorForm) { | |||
| deleteRecord={handleDeleteRecord} | |||
| ></QueryTable> | |||
| </Collapse> | |||
| {visible && ( | |||
| <ParameterDialog | |||
| hideModal={hideModal} | |||
| @@ -17,10 +17,11 @@ import { | |||
| import { Input } from '@/components/ui/input'; | |||
| import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import { ChangeEvent, useEffect, useMemo } from 'react'; | |||
| import { useForm, useWatch } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| @@ -41,6 +42,7 @@ function ParameterForm({ | |||
| otherThanCurrentQuery, | |||
| submit, | |||
| }: ModalFormProps) { | |||
| const { t } = useTranslate('flow'); | |||
| const FormSchema = z.object({ | |||
| type: z.string(), | |||
| key: z | |||
| @@ -84,7 +86,7 @@ function ParameterForm({ | |||
| <Icon | |||
| className={`size-${cur === BeginQueryType.Options ? 4 : 5}`} | |||
| ></Icon> | |||
| {cur} | |||
| {t(cur.toLowerCase())} | |||
| </div> | |||
| ), | |||
| value: cur, | |||
| @@ -116,6 +118,13 @@ function ParameterForm({ | |||
| submit(values); | |||
| } | |||
| const handleKeyChange = (e: ChangeEvent<HTMLInputElement>) => { | |||
| const name = form.getValues().name || ''; | |||
| form.setValue('key', e.target.value.trim()); | |||
| if (!name) { | |||
| form.setValue('name', e.target.value.trim()); | |||
| } | |||
| }; | |||
| return ( | |||
| <Form {...form}> | |||
| <form | |||
| @@ -144,7 +153,7 @@ function ParameterForm({ | |||
| <FormItem> | |||
| <FormLabel>Key</FormLabel> | |||
| <FormControl> | |||
| <Input {...field} autoComplete="off" /> | |||
| <Input {...field} autoComplete="off" onBlur={handleKeyChange} /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| @@ -53,7 +53,7 @@ export function QueryTable({ data = [], deleteRecord, showModal }: IProps) { | |||
| const columns: ColumnDef<BeginQuery>[] = [ | |||
| { | |||
| accessorKey: 'key', | |||
| header: 'key', | |||
| header: 'Key', | |||
| meta: { cellClassName: 'max-w-30' }, | |||
| cell: ({ row }) => { | |||
| const key: string = row.getValue('key'); | |||
| @@ -90,7 +90,11 @@ export function QueryTable({ data = [], deleteRecord, showModal }: IProps) { | |||
| { | |||
| accessorKey: 'type', | |||
| header: t('flow.type'), | |||
| cell: ({ row }) => <div>{row.getValue('type')}</div>, | |||
| cell: ({ row }) => ( | |||
| <div> | |||
| {t(`flow.${(row.getValue('type')?.toString() || '').toLowerCase()}`)} | |||
| </div> | |||
| ), | |||
| }, | |||
| { | |||
| accessorKey: 'optional', | |||
| @@ -14,12 +14,13 @@ type LogSheetProps = IModalProps<any> & | |||
| Pick< | |||
| ReturnType<typeof useCacheChatLog>, | |||
| 'currentEventListWithoutMessageById' | 'currentMessageId' | |||
| >; | |||
| > & { sendLoading: boolean }; | |||
| export function LogSheet({ | |||
| hideModal, | |||
| currentEventListWithoutMessageById, | |||
| currentMessageId, | |||
| sendLoading, | |||
| }: LogSheetProps) { | |||
| return ( | |||
| <Sheet open onOpenChange={hideModal} modal={false}> | |||
| @@ -36,6 +37,7 @@ export function LogSheet({ | |||
| currentMessageId, | |||
| )} | |||
| currentMessageId={currentMessageId} | |||
| sendLoading={sendLoading} | |||
| /> | |||
| </section> | |||
| </SheetContent> | |||
| @@ -16,7 +16,13 @@ import { Operator } from '../constant'; | |||
| import OperatorIcon from '../operator-icon'; | |||
| import { JsonViewer } from './workFlowTimeline'; | |||
| const ToolTimelineItem = ({ tools }: { tools: Record<string, any>[] }) => { | |||
| const ToolTimelineItem = ({ | |||
| tools, | |||
| sendLoading = false, | |||
| }: { | |||
| tools: Record<string, any>[]; | |||
| sendLoading: boolean; | |||
| }) => { | |||
| if (!tools || tools.length === 0 || !Array.isArray(tools)) return null; | |||
| const blackList = ['add_memory', 'gen_citations']; | |||
| const filteredTools = tools.filter( | |||
| @@ -32,6 +38,15 @@ const ToolTimelineItem = ({ tools }: { tools: Record<string, any>[] }) => { | |||
| }) | |||
| .join(' '); | |||
| }; | |||
| const parentName = (str: string, separator: string = '-->') => { | |||
| if (!str) return ''; | |||
| const strs = str.split(separator); | |||
| if (strs.length > 1) { | |||
| return strs[strs.length - 1]; | |||
| } else { | |||
| return str; | |||
| } | |||
| }; | |||
| return ( | |||
| <> | |||
| {filteredTools?.map((tool, idx) => { | |||
| @@ -58,7 +73,9 @@ const ToolTimelineItem = ({ tools }: { tools: Record<string, any>[] }) => { | |||
| 'group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-6 p-1 items-center justify-center group-data-[orientation=vertical]/timeline:-left-7', | |||
| { | |||
| 'border border-blue-500': !( | |||
| idx >= filteredTools.length - 1 && tool.result === '...' | |||
| idx >= filteredTools.length - 1 && | |||
| tool.result === '...' && | |||
| sendLoading | |||
| ), | |||
| }, | |||
| )} | |||
| @@ -69,7 +86,8 @@ const ToolTimelineItem = ({ tools }: { tools: Record<string, any>[] }) => { | |||
| className={cn('rounded-full w-6 h-6', { | |||
| ' border-muted-foreground border-2 border-t-transparent animate-spin ': | |||
| idx >= filteredTools.length - 1 && | |||
| tool.result === '...', | |||
| tool.result === '...' && | |||
| sendLoading, | |||
| })} | |||
| ></div> | |||
| </div> | |||
| @@ -93,7 +111,7 @@ const ToolTimelineItem = ({ tools }: { tools: Record<string, any>[] }) => { | |||
| <AccordionTrigger> | |||
| <div className="flex gap-2 items-center"> | |||
| <span> | |||
| {tool.path + ' '} | |||
| {parentName(tool.path) + ' '} | |||
| {capitalizeWords(tool.tool_name, '_')} | |||
| </span> | |||
| <span className="text-text-sub-title text-xs"> | |||
| @@ -20,6 +20,7 @@ import { | |||
| } from '@/hooks/use-send-message'; | |||
| import { ITraceData } from '@/interfaces/database/agent'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { t } from 'i18next'; | |||
| import { get } from 'lodash'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| import JsonView from 'react18-json-view'; | |||
| @@ -30,7 +31,7 @@ import ToolTimelineItem from './toolTimelineItem'; | |||
| type LogFlowTimelineProps = Pick< | |||
| ReturnType<typeof useCacheChatLog>, | |||
| 'currentEventListWithoutMessage' | 'currentMessageId' | |||
| > & { canvasId?: string }; | |||
| > & { canvasId?: string; sendLoading: boolean }; | |||
| export function JsonViewer({ | |||
| data, | |||
| title, | |||
| @@ -67,6 +68,7 @@ export const WorkFlowTimeline = ({ | |||
| currentEventListWithoutMessage, | |||
| currentMessageId, | |||
| canvasId, | |||
| sendLoading, | |||
| }: LogFlowTimelineProps) => { | |||
| // const getNode = useGraphStore((state) => state.getNode); | |||
| const [isStopFetchTrace, setISStopFetchTrace] = useState(false); | |||
| @@ -79,31 +81,20 @@ export const WorkFlowTimeline = ({ | |||
| useEffect(() => { | |||
| setMessageId(currentMessageId); | |||
| }, [currentMessageId, setMessageId]); | |||
| // const getNodeName = useCallback( | |||
| // (nodeId: string) => { | |||
| // if ('begin' === nodeId) return t('flow.begin'); | |||
| // return getNode(nodeId)?.data.name; | |||
| // }, | |||
| // [getNode], | |||
| // ); | |||
| // const getNodeById = useCallback( | |||
| // (nodeId: string) => { | |||
| // const data = currentEventListWithoutMessage | |||
| // .map((x) => x.data) | |||
| // .filter((x) => x.component_id === nodeId); | |||
| // if ('begin' === nodeId) return t('flow.begin'); | |||
| // if (data && data.length) { | |||
| // return data[0]; | |||
| // } | |||
| // return {}; | |||
| // }, | |||
| // [currentEventListWithoutMessage], | |||
| // ); | |||
| const getNodeName = (nodeId: string) => { | |||
| if ('begin' === nodeId) return t('flow.begin'); | |||
| return nodeId; | |||
| }; | |||
| useEffect(() => { | |||
| setISStopFetchTrace(!sendLoading); | |||
| }, [sendLoading]); | |||
| const startedNodeList = useMemo(() => { | |||
| const finish = currentEventListWithoutMessage?.some( | |||
| (item) => item.event === MessageEventType.WorkflowFinished, | |||
| ); | |||
| setISStopFetchTrace(finish); | |||
| setISStopFetchTrace(finish || !sendLoading); | |||
| const duplicateList = currentEventListWithoutMessage?.filter( | |||
| (x) => x.event === MessageEventType.NodeStarted, | |||
| ) as INodeEvent[]; | |||
| @@ -115,7 +106,7 @@ export const WorkFlowTimeline = ({ | |||
| } | |||
| return pre; | |||
| }, []); | |||
| }, [currentEventListWithoutMessage]); | |||
| }, [currentEventListWithoutMessage, sendLoading]); | |||
| const hasTrace = useCallback( | |||
| (componentId: string) => { | |||
| @@ -198,7 +189,8 @@ export const WorkFlowTimeline = ({ | |||
| <div | |||
| className={cn('rounded-full w-6 h-6', { | |||
| ' border-muted-foreground border-2 border-t-transparent animate-spin ': | |||
| !finishNodeIds.includes(x.data.component_id), | |||
| !finishNodeIds.includes(x.data.component_id) && | |||
| sendLoading, | |||
| })} | |||
| ></div> | |||
| </div> | |||
| @@ -212,7 +204,7 @@ export const WorkFlowTimeline = ({ | |||
| </TimelineIndicator> | |||
| </TimelineHeader> | |||
| <TimelineContent className="text-foreground rounded-lg border mb-5"> | |||
| <section key={idx}> | |||
| <section key={'content_' + idx}> | |||
| <Accordion | |||
| type="single" | |||
| collapsible | |||
| @@ -221,7 +213,7 @@ export const WorkFlowTimeline = ({ | |||
| <AccordionItem value={idx.toString()}> | |||
| <AccordionTrigger> | |||
| <div className="flex gap-2 items-center"> | |||
| <span>{x.data?.component_name}</span> | |||
| <span>{getNodeName(x.data?.component_name)}</span> | |||
| <span className="text-text-sub-title text-xs"> | |||
| {x.data.elapsed_time?.toString().slice(0, 6)} | |||
| </span> | |||
| @@ -253,7 +245,9 @@ export const WorkFlowTimeline = ({ | |||
| </TimelineItem> | |||
| {hasTrace(x.data.component_id) && ( | |||
| <ToolTimelineItem | |||
| key={'tool_' + idx} | |||
| tools={filterTrace(x.data.component_id)} | |||
| sendLoading={sendLoading} | |||
| ></ToolTimelineItem> | |||
| )} | |||
| </> | |||
| @@ -15,7 +15,7 @@ import { useCallback, useEffect, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { CreateAgentDialog } from './create-agent-dialog'; | |||
| import { TemplateCard } from './template-card'; | |||
| import { SideBar } from './template-sidebar'; | |||
| import { MenuItemKey, SideBar } from './template-sidebar'; | |||
| export default function AgentTemplates() { | |||
| const { navigateToAgentList } = useNavigatePage(); | |||
| @@ -23,7 +23,9 @@ export default function AgentTemplates() { | |||
| const list = useFetchAgentTemplates(); | |||
| const { loading, setAgent } = useSetAgent(); | |||
| const [templateList, setTemplateList] = useState<IFlowTemplate[]>([]); | |||
| const [selectMenuItem, setSelectMenuItem] = useState<string>( | |||
| MenuItemKey.Recommended, | |||
| ); | |||
| useEffect(() => { | |||
| setTemplateList(list); | |||
| }, [list]); | |||
| @@ -70,10 +72,12 @@ export default function AgentTemplates() { | |||
| const handleSiderBarChange = (keyword: string) => { | |||
| const tempList = list.filter( | |||
| (item, index) => | |||
| item.title.toLocaleLowerCase().includes(keyword?.toLocaleLowerCase()) || | |||
| index === 0, | |||
| item.canvas_type | |||
| ?.toLocaleLowerCase() | |||
| .includes(keyword?.toLocaleLowerCase()) || index === 0, | |||
| ); | |||
| setTemplateList(tempList); | |||
| setSelectMenuItem(keyword); | |||
| }; | |||
| return ( | |||
| <section> | |||
| @@ -93,9 +97,12 @@ export default function AgentTemplates() { | |||
| </Breadcrumb> | |||
| </PageHeader> | |||
| <div className="flex flex-1 h-dvh"> | |||
| <SideBar change={handleSiderBarChange}></SideBar> | |||
| <SideBar | |||
| change={handleSiderBarChange} | |||
| selected={selectMenuItem} | |||
| ></SideBar> | |||
| <main className="flex-1 bg-muted/50 h-dvh"> | |||
| <main className="flex-1 bg-text-title-invert/50 h-dvh"> | |||
| <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[94vh] overflow-auto px-8 pt-8"> | |||
| {templateList?.map((x, index) => { | |||
| return ( | |||
| @@ -1,44 +1,85 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { useSecondPathName } from '@/hooks/route-hook'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { Banknote, LayoutGrid, User } from 'lucide-react'; | |||
| import { | |||
| Box, | |||
| ChartPie, | |||
| Component, | |||
| MessageCircleCode, | |||
| PencilRuler, | |||
| Sparkle, | |||
| } from 'lucide-react'; | |||
| export enum MenuItemKey { | |||
| Recommended = 'Recommended', | |||
| Agent = 'Agent', | |||
| CustomerSupport = 'Customer Support', | |||
| Marketing = 'Marketing', | |||
| ConsumerApp = 'Consumer App', | |||
| Other = 'Other', | |||
| } | |||
| const menuItems = [ | |||
| { | |||
| section: 'All Templates', | |||
| // section: 'All Templates', | |||
| section: '', | |||
| items: [ | |||
| { icon: User, label: 'Assistant', key: 'Assistant' }, | |||
| { icon: LayoutGrid, label: 'chatbot', key: 'chatbot' }, | |||
| { icon: Banknote, label: 'generator', key: 'generator' }, | |||
| { icon: Banknote, label: 'Intel', key: 'Intel' }, | |||
| { | |||
| icon: Sparkle, | |||
| label: MenuItemKey.Recommended, | |||
| key: MenuItemKey.Recommended, | |||
| }, | |||
| { icon: Box, label: MenuItemKey.Agent, key: MenuItemKey.Agent }, | |||
| { | |||
| icon: MessageCircleCode, | |||
| label: MenuItemKey.CustomerSupport, | |||
| key: MenuItemKey.CustomerSupport, | |||
| }, | |||
| { | |||
| icon: ChartPie, | |||
| label: MenuItemKey.Marketing, | |||
| key: MenuItemKey.Marketing, | |||
| }, | |||
| { | |||
| icon: Component, | |||
| label: MenuItemKey.ConsumerApp, | |||
| key: MenuItemKey.ConsumerApp, | |||
| }, | |||
| { icon: PencilRuler, label: MenuItemKey.Other, key: MenuItemKey.Other }, | |||
| ], | |||
| }, | |||
| ]; | |||
| export function SideBar({ change }: { change: (keyword: string) => void }) { | |||
| const pathName = useSecondPathName(); | |||
| export function SideBar({ | |||
| change, | |||
| selected = MenuItemKey.Recommended, | |||
| }: { | |||
| change: (keyword: string) => void; | |||
| selected?: string; | |||
| }) { | |||
| const handleMenuClick = (key: string) => { | |||
| change(key); | |||
| }; | |||
| return ( | |||
| <aside className="w-[303px] bg-background border-r flex flex-col"> | |||
| <aside className="w-[303px] bg-text-title-invert border-r flex flex-col"> | |||
| <div className="flex-1 overflow-auto"> | |||
| {menuItems.map((section, idx) => ( | |||
| <div key={idx}> | |||
| <h2 | |||
| className="p-6 text-sm font-semibold hover:bg-muted/50 cursor-pointer" | |||
| onClick={() => handleMenuClick('')} | |||
| > | |||
| {section.section} | |||
| </h2> | |||
| {section.section && ( | |||
| <h2 | |||
| className="p-6 text-sm font-semibold hover:bg-muted/50 cursor-pointer" | |||
| onClick={() => handleMenuClick('')} | |||
| > | |||
| {section.section} | |||
| </h2> | |||
| )} | |||
| {section.items.map((item, itemIdx) => { | |||
| const active = pathName === item.key; | |||
| const active = selected === item.key; | |||
| return ( | |||
| <Button | |||
| key={itemIdx} | |||
| variant={active ? 'secondary' : 'ghost'} | |||
| className={cn('w-full justify-start gap-2.5 p-6 relative')} | |||
| className={cn( | |||
| 'w-full justify-start gap-4 px-6 py-8 relative rounded-none', | |||
| )} | |||
| onClick={() => handleMenuClick(item.key)} | |||
| > | |||
| <item.icon className="w-6 h-6" /> | |||