### 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
| 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"> |
| 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> | ||||
| )} | )} |
| 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: { |
| } 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> |
| }, | }, | ||||
| [ | [ | ||||
| agentId, | agentId, | ||||
| sessionId, | |||||
| send, | send, | ||||
| clearUploadResponseList, | clearUploadResponseList, | ||||
| inputs, | inputs, | ||||
| beginParams, | beginParams, | ||||
| uploadResponseList, | uploadResponseList, | ||||
| sessionId, | |||||
| setValue, | setValue, | ||||
| removeLatestMessage, | removeLatestMessage, | ||||
| ], | ], |
| 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, |
| )} | )} | ||||
| /> | /> | ||||
| )} | )} | ||||
| {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} |
| 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> |
| 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', |
| 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> |
| 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"> |
| } 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> | ||||
| )} | )} | ||||
| </> | </> |
| 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 ( |
| 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" /> |