Explorar el Código

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 hace 3 meses
padre
commit
26042343d8
No account linked to committer's email address

+ 1
- 1
web/src/components/message-input/next.tsx Ver fichero

@@ -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">

+ 11
- 1
web/src/components/next-message-item/index.tsx Ver fichero

@@ -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>
)}

+ 9
- 0
web/src/locales/en.ts Ver fichero

@@ -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
- 3
web/src/pages/agent/canvas/index.tsx Ver fichero

@@ -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>

+ 1
- 1
web/src/pages/agent/chat/use-send-agent-message.ts Ver fichero

@@ -251,12 +251,12 @@ export const useSendAgentMessage = (
},
[
agentId,
sessionId,
send,
clearUploadResponseList,
inputs,
beginParams,
uploadResponseList,
sessionId,
setValue,
removeLatestMessage,
],

+ 1
- 1
web/src/pages/agent/context.ts Ver fichero

@@ -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,

+ 1
- 2
web/src/pages/agent/form/begin-form/index.tsx Ver fichero

@@ -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}

+ 12
- 3
web/src/pages/agent/form/begin-form/parameter-dialog.tsx Ver fichero

@@ -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>

+ 6
- 2
web/src/pages/agent/form/begin-form/query-table.tsx Ver fichero

@@ -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',

+ 3
- 1
web/src/pages/agent/log-sheet/index.tsx Ver fichero

@@ -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>

+ 22
- 4
web/src/pages/agent/log-sheet/toolTimelineItem.tsx Ver fichero

@@ -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
- 26
web/src/pages/agent/log-sheet/workFlowTimeline.tsx Ver fichero

@@ -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>
)}
</>

+ 13
- 6
web/src/pages/agents/agent-templates.tsx Ver fichero

@@ -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 (

+ 60
- 19
web/src/pages/agents/template-sidebar.tsx Ver fichero

@@ -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" />

Cargando…
Cancelar
Guardar