Sfoglia il codice sorgente

Fix: Improve Agent templates functionality and fix some UI style issues (#9129)

### 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
chanx 3 mesi fa
parent
commit
26042343d8
Nessun account collegato all'indirizzo email del committer

+ 1
- 1
web/src/components/message-input/next.tsx Vedi File

onChange={onInputChange} onChange={onInputChange}
placeholder="Type your message here..." 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" 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} onKeyDown={handleKeyDown}
/> />
<div className="flex items-center justify-between gap-1.5"> <div className="flex items-center justify-between gap-1.5">

+ 11
- 1
web/src/components/next-message-item/index.tsx Vedi File

PropsWithChildren, PropsWithChildren,
memo, memo,
useCallback, useCallback,
useContext,
useEffect, useEffect,
useMemo, useMemo,
useState, useState,
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks'; import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { INodeEvent } from '@/hooks/use-send-message'; import { INodeEvent } from '@/hooks/use-send-message';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { AgentChatContext } from '@/pages/agent/context';
import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workFlowTimeline'; import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workFlowTimeline';
import { IMessage } from '@/pages/chat/interface'; import { IMessage } from '@/pages/chat/interface';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
const { visible, hideModal, showModal } = useSetModalState(); const { visible, hideModal, showModal } = useSetModalState();
const [clickedDocumentId, setClickedDocumentId] = useState(''); 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 referenceDocuments = useMemo(() => {
const docs = reference?.doc_aggs ?? {}; const docs = reference?.doc_aggs ?? {};


) : ( ) : (
<AssistantIcon /> <AssistantIcon />
))} ))}

<section className="flex-col gap-2 flex-1"> <section className="flex-col gap-2 flex-1">
<div className="space-x-1"> <div className="space-x-1">
{isAssistant ? ( {isAssistant ? (
)} )}
currentMessageId={item.id} currentMessageId={item.id}
canvasId={conversationId} canvasId={conversationId}
sendLoading={loading}
/> />
</div> </div>
)} )}

+ 9
- 0
web/src/locales/en.ts Vedi File

export: 'Export', export: 'Export',
seconds: 'Seconds', seconds: 'Seconds',
subject: 'Subject', 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: { llmTools: {
bad_calculator: { bad_calculator: {

+ 13
- 3
web/src/pages/agent/canvas/index.tsx Vedi File

} from '@xyflow/react'; } from '@xyflow/react';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import { NotebookPen } from 'lucide-react'; import { NotebookPen } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ChatSheet } from '../chat/chat-sheet'; import { ChatSheet } from '../chat/chat-sheet';
import { AgentBackground } from '../components/background'; import { AgentBackground } from '../components/background';
const { showLogSheet, logSheetVisible, hideLogSheet } = useShowLogSheet({ const { showLogSheet, logSheetVisible, hideLogSheet } = useShowLogSheet({
setCurrentMessageId, setCurrentMessageId,
}); });
const [lastSendLoading, setLastSendLoading] = useState(false);


const { handleBeforeDelete } = useBeforeDelete(); const { handleBeforeDelete } = useBeforeDelete();


clearEventList(); clearEventList();
} }
}, [chatVisible, clearEventList]); }, [chatVisible, clearEventList]);

const setLastSendLoadingFunc = (loading: boolean, messageId: string) => {
if (messageId === currentMessageId) {
setLastSendLoading(loading);
} else {
setLastSendLoading(false);
}
};
return ( return (
<div className={styles.canvasWrapper}> <div className={styles.canvasWrapper}>
<svg <svg
</AgentInstanceContext.Provider> </AgentInstanceContext.Provider>
)} )}
{chatVisible && ( {chatVisible && (
<AgentChatContext.Provider value={{ showLogSheet }}>
<AgentChatContext.Provider
value={{ showLogSheet, setLastSendLoadingFunc }}
>
<AgentChatLogContext.Provider <AgentChatLogContext.Provider
value={{ addEventList, setCurrentMessageId }} value={{ addEventList, setCurrentMessageId }}
> >
currentEventListWithoutMessageById currentEventListWithoutMessageById
} }
currentMessageId={currentMessageId} currentMessageId={currentMessageId}
sendLoading={lastSendLoading}
></LogSheet> ></LogSheet>
)} )}
</div> </div>

+ 1
- 1
web/src/pages/agent/chat/use-send-agent-message.ts Vedi File

}, },
[ [
agentId, agentId,
sessionId,
send, send,
clearUploadResponseList, clearUploadResponseList,
inputs, inputs,
beginParams, beginParams,
uploadResponseList, uploadResponseList,
sessionId,
setValue, setValue,
removeLatestMessage, removeLatestMessage,
], ],

+ 1
- 1
web/src/pages/agent/context.ts Vedi File

type AgentChatContextType = Pick< type AgentChatContextType = Pick<
ReturnType<typeof useShowLogSheet>, ReturnType<typeof useShowLogSheet>,
'showLogSheet' 'showLogSheet'
>;
> & { setLastSendLoadingFunc: (loading: boolean, messageId: string) => void };


export const AgentChatContext = createContext<AgentChatContextType>( export const AgentChatContext = createContext<AgentChatContextType>(
{} as AgentChatContextType, {} as AgentChatContextType,

+ 1
- 2
web/src/pages/agent/form/begin-form/index.tsx Vedi File

)} )}
/> />
)} )}
{enablePrologue && (
{mode === AgentDialogueMode.Conversational && enablePrologue && (
<FormField <FormField
control={form.control} control={form.control}
name={'prologue'} name={'prologue'}
deleteRecord={handleDeleteRecord} deleteRecord={handleDeleteRecord}
></QueryTable> ></QueryTable>
</Collapse> </Collapse>

{visible && ( {visible && (
<ParameterDialog <ParameterDialog
hideModal={hideModal} hideModal={hideModal}

+ 12
- 3
web/src/pages/agent/form/begin-form/parameter-dialog.tsx Vedi File

import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select'; import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useEffect, useMemo } from 'react';
import { ChangeEvent, useEffect, useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod'; import { z } from 'zod';
otherThanCurrentQuery, otherThanCurrentQuery,
submit, submit,
}: ModalFormProps) { }: ModalFormProps) {
const { t } = useTranslate('flow');
const FormSchema = z.object({ const FormSchema = z.object({
type: z.string(), type: z.string(),
key: z key: z
<Icon <Icon
className={`size-${cur === BeginQueryType.Options ? 4 : 5}`} className={`size-${cur === BeginQueryType.Options ? 4 : 5}`}
></Icon> ></Icon>
{cur}
{t(cur.toLowerCase())}
</div> </div>
), ),
value: cur, value: cur,
submit(values); 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 ( return (
<Form {...form}> <Form {...form}>
<form <form
<FormItem> <FormItem>
<FormLabel>Key</FormLabel> <FormLabel>Key</FormLabel>
<FormControl> <FormControl>
<Input {...field} autoComplete="off" />
<Input {...field} autoComplete="off" onBlur={handleKeyChange} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

+ 6
- 2
web/src/pages/agent/form/begin-form/query-table.tsx Vedi File

const columns: ColumnDef<BeginQuery>[] = [ const columns: ColumnDef<BeginQuery>[] = [
{ {
accessorKey: 'key', accessorKey: 'key',
header: 'key',
header: 'Key',
meta: { cellClassName: 'max-w-30' }, meta: { cellClassName: 'max-w-30' },
cell: ({ row }) => { cell: ({ row }) => {
const key: string = row.getValue('key'); const key: string = row.getValue('key');
{ {
accessorKey: 'type', accessorKey: 'type',
header: t('flow.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', accessorKey: 'optional',

+ 3
- 1
web/src/pages/agent/log-sheet/index.tsx Vedi File

Pick< Pick<
ReturnType<typeof useCacheChatLog>, ReturnType<typeof useCacheChatLog>,
'currentEventListWithoutMessageById' | 'currentMessageId' 'currentEventListWithoutMessageById' | 'currentMessageId'
>;
> & { sendLoading: boolean };


export function LogSheet({ export function LogSheet({
hideModal, hideModal,
currentEventListWithoutMessageById, currentEventListWithoutMessageById,
currentMessageId, currentMessageId,
sendLoading,
}: LogSheetProps) { }: LogSheetProps) {
return ( return (
<Sheet open onOpenChange={hideModal} modal={false}> <Sheet open onOpenChange={hideModal} modal={false}>
currentMessageId, currentMessageId,
)} )}
currentMessageId={currentMessageId} currentMessageId={currentMessageId}
sendLoading={sendLoading}
/> />
</section> </section>
</SheetContent> </SheetContent>

+ 22
- 4
web/src/pages/agent/log-sheet/toolTimelineItem.tsx Vedi File

import OperatorIcon from '../operator-icon'; import OperatorIcon from '../operator-icon';
import { JsonViewer } from './workFlowTimeline'; 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; if (!tools || tools.length === 0 || !Array.isArray(tools)) return null;
const blackList = ['add_memory', 'gen_citations']; const blackList = ['add_memory', 'gen_citations'];
const filteredTools = tools.filter( const filteredTools = tools.filter(
}) })
.join(' '); .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 ( return (
<> <>
{filteredTools?.map((tool, idx) => { {filteredTools?.map((tool, idx) => {
'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', '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': !( 'border border-blue-500': !(
idx >= filteredTools.length - 1 && tool.result === '...'
idx >= filteredTools.length - 1 &&
tool.result === '...' &&
sendLoading
), ),
}, },
)} )}
className={cn('rounded-full w-6 h-6', { className={cn('rounded-full w-6 h-6', {
' border-muted-foreground border-2 border-t-transparent animate-spin ': ' border-muted-foreground border-2 border-t-transparent animate-spin ':
idx >= filteredTools.length - 1 && idx >= filteredTools.length - 1 &&
tool.result === '...',
tool.result === '...' &&
sendLoading,
})} })}
></div> ></div>
</div> </div>
<AccordionTrigger> <AccordionTrigger>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<span> <span>
{tool.path + ' '}
{parentName(tool.path) + ' '}
{capitalizeWords(tool.tool_name, '_')} {capitalizeWords(tool.tool_name, '_')}
</span> </span>
<span className="text-text-sub-title text-xs"> <span className="text-text-sub-title text-xs">

+ 20
- 26
web/src/pages/agent/log-sheet/workFlowTimeline.tsx Vedi File

} from '@/hooks/use-send-message'; } from '@/hooks/use-send-message';
import { ITraceData } from '@/interfaces/database/agent'; import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { get } from 'lodash'; import { get } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import JsonView from 'react18-json-view'; import JsonView from 'react18-json-view';
type LogFlowTimelineProps = Pick< type LogFlowTimelineProps = Pick<
ReturnType<typeof useCacheChatLog>, ReturnType<typeof useCacheChatLog>,
'currentEventListWithoutMessage' | 'currentMessageId' 'currentEventListWithoutMessage' | 'currentMessageId'
> & { canvasId?: string };
> & { canvasId?: string; sendLoading: boolean };
export function JsonViewer({ export function JsonViewer({
data, data,
title, title,
currentEventListWithoutMessage, currentEventListWithoutMessage,
currentMessageId, currentMessageId,
canvasId, canvasId,
sendLoading,
}: LogFlowTimelineProps) => { }: LogFlowTimelineProps) => {
// const getNode = useGraphStore((state) => state.getNode); // const getNode = useGraphStore((state) => state.getNode);
const [isStopFetchTrace, setISStopFetchTrace] = useState(false); const [isStopFetchTrace, setISStopFetchTrace] = useState(false);
useEffect(() => { useEffect(() => {
setMessageId(currentMessageId); setMessageId(currentMessageId);
}, [currentMessageId, setMessageId]); }, [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 startedNodeList = useMemo(() => {
const finish = currentEventListWithoutMessage?.some( const finish = currentEventListWithoutMessage?.some(
(item) => item.event === MessageEventType.WorkflowFinished, (item) => item.event === MessageEventType.WorkflowFinished,
); );
setISStopFetchTrace(finish);
setISStopFetchTrace(finish || !sendLoading);
const duplicateList = currentEventListWithoutMessage?.filter( const duplicateList = currentEventListWithoutMessage?.filter(
(x) => x.event === MessageEventType.NodeStarted, (x) => x.event === MessageEventType.NodeStarted,
) as INodeEvent[]; ) as INodeEvent[];
} }
return pre; return pre;
}, []); }, []);
}, [currentEventListWithoutMessage]);
}, [currentEventListWithoutMessage, sendLoading]);


const hasTrace = useCallback( const hasTrace = useCallback(
(componentId: string) => { (componentId: string) => {
<div <div
className={cn('rounded-full w-6 h-6', { className={cn('rounded-full w-6 h-6', {
' border-muted-foreground border-2 border-t-transparent animate-spin ': ' 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>
</div> </div>
</TimelineIndicator> </TimelineIndicator>
</TimelineHeader> </TimelineHeader>
<TimelineContent className="text-foreground rounded-lg border mb-5"> <TimelineContent className="text-foreground rounded-lg border mb-5">
<section key={idx}>
<section key={'content_' + idx}>
<Accordion <Accordion
type="single" type="single"
collapsible collapsible
<AccordionItem value={idx.toString()}> <AccordionItem value={idx.toString()}>
<AccordionTrigger> <AccordionTrigger>
<div className="flex gap-2 items-center"> <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"> <span className="text-text-sub-title text-xs">
{x.data.elapsed_time?.toString().slice(0, 6)} {x.data.elapsed_time?.toString().slice(0, 6)}
</span> </span>
</TimelineItem> </TimelineItem>
{hasTrace(x.data.component_id) && ( {hasTrace(x.data.component_id) && (
<ToolTimelineItem <ToolTimelineItem
key={'tool_' + idx}
tools={filterTrace(x.data.component_id)} tools={filterTrace(x.data.component_id)}
sendLoading={sendLoading}
></ToolTimelineItem> ></ToolTimelineItem>
)} )}
</> </>

+ 13
- 6
web/src/pages/agents/agent-templates.tsx Vedi File

import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CreateAgentDialog } from './create-agent-dialog'; import { CreateAgentDialog } from './create-agent-dialog';
import { TemplateCard } from './template-card'; import { TemplateCard } from './template-card';
import { SideBar } from './template-sidebar';
import { MenuItemKey, SideBar } from './template-sidebar';


export default function AgentTemplates() { export default function AgentTemplates() {
const { navigateToAgentList } = useNavigatePage(); const { navigateToAgentList } = useNavigatePage();
const list = useFetchAgentTemplates(); const list = useFetchAgentTemplates();
const { loading, setAgent } = useSetAgent(); const { loading, setAgent } = useSetAgent();
const [templateList, setTemplateList] = useState<IFlowTemplate[]>([]); const [templateList, setTemplateList] = useState<IFlowTemplate[]>([]);

const [selectMenuItem, setSelectMenuItem] = useState<string>(
MenuItemKey.Recommended,
);
useEffect(() => { useEffect(() => {
setTemplateList(list); setTemplateList(list);
}, [list]); }, [list]);
const handleSiderBarChange = (keyword: string) => { const handleSiderBarChange = (keyword: string) => {
const tempList = list.filter( const tempList = list.filter(
(item, index) => (item, index) =>
item.title.toLocaleLowerCase().includes(keyword?.toLocaleLowerCase()) ||
index === 0,
item.canvas_type
?.toLocaleLowerCase()
.includes(keyword?.toLocaleLowerCase()) || index === 0,
); );
setTemplateList(tempList); setTemplateList(tempList);
setSelectMenuItem(keyword);
}; };
return ( return (
<section> <section>
</Breadcrumb> </Breadcrumb>
</PageHeader> </PageHeader>
<div className="flex flex-1 h-dvh"> <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"> <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) => { {templateList?.map((x, index) => {
return ( return (

+ 60
- 19
web/src/pages/agents/template-sidebar.tsx Vedi File

import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useSecondPathName } from '@/hooks/route-hook';
import { cn } from '@/lib/utils'; 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 = [ const menuItems = [
{ {
section: 'All Templates',
// section: 'All Templates',
section: '',
items: [ 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) => { const handleMenuClick = (key: string) => {
change(key); change(key);
}; };


return ( 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"> <div className="flex-1 overflow-auto">
{menuItems.map((section, idx) => ( {menuItems.map((section, idx) => (
<div key={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) => { {section.items.map((item, itemIdx) => {
const active = pathName === item.key;
const active = selected === item.key;
return ( return (
<Button <Button
key={itemIdx} key={itemIdx}
variant={active ? 'secondary' : 'ghost'} 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)} onClick={() => handleMenuClick(item.key)}
> >
<item.icon className="w-6 h-6" /> <item.icon className="w-6 h-6" />

Loading…
Annulla
Salva