Browse Source

Feat: Display agent operator call log #3221 (#8169)

### What problem does this PR solve?

Feat: Display agent operator call log #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.19.1
balibabu 4 months ago
parent
commit
f0a3d91171
No account linked to committer's email address

+ 368
- 0
web/package-lock.json View File

@@ -38,6 +38,8 @@
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.1.4",
"@tailwindcss/line-clamp": "^0.4.4",
"@tanstack/react-query": "^5.40.0",
@@ -7591,6 +7593,372 @@
}
}
},
"node_modules/@radix-ui/react-toggle": {
"version": "1.1.9",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-toggle/-/react-toggle-1.1.9.tgz",
"integrity": "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group": {
"version": "1.1.10",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.10.tgz",
"integrity": "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.10",
"@radix-ui/react-toggle": "1.1.9",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.10",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
"integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz",

+ 2
- 0
web/package.json View File

@@ -49,6 +49,8 @@
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.1.4",
"@tailwindcss/line-clamp": "^0.4.4",
"@tanstack/react-query": "^5.40.0",

+ 51
- 0
web/src/components/next-message-item/feedback-modal.tsx View File

@@ -0,0 +1,51 @@
import { Form, Input, Modal } from 'antd';

import { IModalProps } from '@/interfaces/common';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { useCallback } from 'react';

type FieldType = {
feedback?: string;
};

const FeedbackModal = ({
visible,
hideModal,
onOk,
loading,
}: IModalProps<IFeedbackRequestBody>) => {
const [form] = Form.useForm();

const handleOk = useCallback(async () => {
const ret = await form.validateFields();
return onOk?.({ thumbup: false, feedback: ret.feedback });
}, [onOk, form]);

return (
<Modal
title="Feedback"
open={visible}
onOk={handleOk}
onCancel={hideModal}
confirmLoading={loading}
>
<Form
name="basic"
labelCol={{ span: 0 }}
wrapperCol={{ span: 24 }}
style={{ maxWidth: 600 }}
autoComplete="off"
form={form}
>
<Form.Item<FieldType>
name="feedback"
rules={[{ required: true, message: 'Please input your feedback!' }]}
>
<Input.TextArea rows={8} placeholder="Please input your feedback!" />
</Form.Item>
</Form>
</Modal>
);
};

export default FeedbackModal;

+ 220
- 0
web/src/components/next-message-item/group-button.tsx View File

@@ -0,0 +1,220 @@
import { PromptIcon } from '@/assets/icon/Icon';
import CopyToClipboard from '@/components/copy-to-clipboard';
import { useSetModalState } from '@/hooks/common-hooks';
import { IRemoveMessageById } from '@/hooks/logic-hooks';
import { AgentChatContext } from '@/pages/agent/context';
import {
DeleteOutlined,
DislikeOutlined,
LikeOutlined,
PauseCircleOutlined,
SoundOutlined,
SyncOutlined,
} from '@ant-design/icons';
import { Radio, Tooltip } from 'antd';
import { NotebookText } from 'lucide-react';
import { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group';
import FeedbackModal from './feedback-modal';
import { useRemoveMessage, useSendFeedback, useSpeech } from './hooks';
import PromptModal from './prompt-modal';

interface IProps {
messageId: string;
content: string;
prompt?: string;
showLikeButton: boolean;
audioBinary?: string;
showLoudspeaker?: boolean;
}

export const AssistantGroupButton = ({
messageId,
content,
prompt,
audioBinary,
showLikeButton,
showLoudspeaker = true,
}: IProps) => {
const { visible, hideModal, showModal, onFeedbackOk, loading } =
useSendFeedback(messageId);
const {
visible: promptVisible,
hideModal: hidePromptModal,
showModal: showPromptModal,
} = useSetModalState();
const { t } = useTranslation();
const { handleRead, ref, isPlaying } = useSpeech(content, audioBinary);

const handleLike = useCallback(() => {
onFeedbackOk({ thumbup: true });
}, [onFeedbackOk]);

const { showLogSheet } = useContext(AgentChatContext);

const handleShowLogSheet = useCallback(() => {
showLogSheet(messageId);
}, [messageId, showLogSheet]);

return (
<>
<ToggleGroup
type={'single'}
size="sm"
variant="outline"
className="space-x-1"
>
<ToggleGroupItem value="a">
<CopyToClipboard text={content}></CopyToClipboard>
</ToggleGroupItem>
{showLoudspeaker && (
<ToggleGroupItem value="b" onClick={handleRead}>
<Tooltip title={t('chat.read')}>
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
</Tooltip>
<audio src="" ref={ref}></audio>
</ToggleGroupItem>
)}
{showLikeButton && (
<>
<ToggleGroupItem value="c" onClick={handleLike}>
<LikeOutlined />
</ToggleGroupItem>
<ToggleGroupItem value="d" onClick={showModal}>
<DislikeOutlined />
</ToggleGroupItem>
</>
)}
{prompt && (
<Radio.Button value="e" onClick={showPromptModal}>
<PromptIcon style={{ fontSize: '16px' }} />
</Radio.Button>
)}
<ToggleGroupItem value="f" onClick={handleShowLogSheet}>
<NotebookText className="size-4" />
</ToggleGroupItem>
</ToggleGroup>
{visible && (
<FeedbackModal
visible={visible}
hideModal={hideModal}
onOk={onFeedbackOk}
loading={loading}
></FeedbackModal>
)}
{promptVisible && (
<PromptModal
visible={promptVisible}
hideModal={hidePromptModal}
prompt={prompt}
></PromptModal>
)}
</>
);

return (
<>
<Radio.Group size="small">
<Radio.Button value="a">
<CopyToClipboard text={content}></CopyToClipboard>
</Radio.Button>
{showLoudspeaker && (
<Radio.Button value="b" onClick={handleRead}>
<Tooltip title={t('chat.read')}>
{isPlaying ? <PauseCircleOutlined /> : <SoundOutlined />}
</Tooltip>
<audio src="" ref={ref}></audio>
</Radio.Button>
)}
{showLikeButton && (
<>
<Radio.Button value="c" onClick={handleLike}>
<LikeOutlined />
</Radio.Button>
<Radio.Button value="d" onClick={showModal}>
<DislikeOutlined />
</Radio.Button>
</>
)}
{prompt && (
<Radio.Button value="e" onClick={showPromptModal}>
<PromptIcon style={{ fontSize: '16px' }} />
</Radio.Button>
)}
<Radio.Button
value="f"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleShowLogSheet();
}}
>
<NotebookText className="size-4" />
</Radio.Button>
</Radio.Group>
{visible && (
<FeedbackModal
visible={visible}
hideModal={hideModal}
onOk={onFeedbackOk}
loading={loading}
></FeedbackModal>
)}
{promptVisible && (
<PromptModal
visible={promptVisible}
hideModal={hidePromptModal}
prompt={prompt}
></PromptModal>
)}
</>
);
};

interface UserGroupButtonProps extends Partial<IRemoveMessageById> {
messageId: string;
content: string;
regenerateMessage?: () => void;
sendLoading: boolean;
}

export const UserGroupButton = ({
content,
messageId,
sendLoading,
removeMessageById,
regenerateMessage,
}: UserGroupButtonProps) => {
const { onRemoveMessage, loading } = useRemoveMessage(
messageId,
removeMessageById,
);
const { t } = useTranslation();

return (
<Radio.Group size="small">
<Radio.Button value="a">
<CopyToClipboard text={content}></CopyToClipboard>
</Radio.Button>
{regenerateMessage && (
<Radio.Button
value="b"
onClick={regenerateMessage}
disabled={sendLoading}
>
<Tooltip title={t('chat.regenerate')}>
<SyncOutlined spin={sendLoading} />
</Tooltip>
</Radio.Button>
)}
{removeMessageById && (
<Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}>
<Tooltip title={t('common.delete')}>
<DeleteOutlined spin={loading} />
</Tooltip>
</Radio.Button>
)}
</Radio.Group>
);
};

+ 116
- 0
web/src/components/next-message-item/hooks.ts View File

@@ -0,0 +1,116 @@
import { useDeleteMessage, useFeedback } from '@/hooks/chat-hooks';
import { useSetModalState } from '@/hooks/common-hooks';
import { IRemoveMessageById, useSpeechWithSse } from '@/hooks/logic-hooks';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { hexStringToUint8Array } from '@/utils/common-util';
import { SpeechPlayer } from 'openai-speech-stream-player';
import { useCallback, useEffect, useRef, useState } from 'react';

export const useSendFeedback = (messageId: string) => {
const { visible, hideModal, showModal } = useSetModalState();
const { feedback, loading } = useFeedback();

const onFeedbackOk = useCallback(
async (params: IFeedbackRequestBody) => {
const ret = await feedback({
...params,
messageId: messageId,
});

if (ret === 0) {
hideModal();
}
},
[feedback, hideModal, messageId],
);

return {
loading,
onFeedbackOk,
visible,
hideModal,
showModal,
};
};

export const useRemoveMessage = (
messageId: string,
removeMessageById?: IRemoveMessageById['removeMessageById'],
) => {
const { deleteMessage, loading } = useDeleteMessage();

const onRemoveMessage = useCallback(async () => {
if (messageId) {
const code = await deleteMessage(messageId);
if (code === 0) {
removeMessageById?.(messageId);
}
}
}, [deleteMessage, messageId, removeMessageById]);

return { onRemoveMessage, loading };
};

export const useSpeech = (content: string, audioBinary?: string) => {
const ref = useRef<HTMLAudioElement>(null);
const { read } = useSpeechWithSse();
const player = useRef<SpeechPlayer>();
const [isPlaying, setIsPlaying] = useState<boolean>(false);

const initialize = useCallback(async () => {
player.current = new SpeechPlayer({
audio: ref.current!,
onPlaying: () => {
setIsPlaying(true);
},
onPause: () => {
setIsPlaying(false);
},
onChunkEnd: () => {},
mimeType: MediaSource.isTypeSupported('audio/mpeg')
? 'audio/mpeg'
: 'audio/mp4; codecs="mp4a.40.2"', // https://stackoverflow.com/questions/64079424/cannot-replay-mp3-in-firefox-using-mediasource-even-though-it-works-in-chrome
});
await player.current.init();
}, []);

const pause = useCallback(() => {
player.current?.pause();
}, []);

const speech = useCallback(async () => {
const response = await read({ text: content });
if (response) {
player?.current?.feedWithResponse(response);
}
}, [read, content]);

const handleRead = useCallback(async () => {
if (isPlaying) {
setIsPlaying(false);
pause();
} else {
setIsPlaying(true);
speech();
}
}, [setIsPlaying, speech, isPlaying, pause]);

useEffect(() => {
if (audioBinary) {
const units = hexStringToUint8Array(audioBinary);
if (units) {
try {
player.current?.feed(units);
} catch (error) {
console.warn(error);
}
}
}
}, [audioBinary]);

useEffect(() => {
initialize();
}, [initialize]);

return { ref, handleRead, isPlaying };
};

+ 63
- 0
web/src/components/next-message-item/index.less View File

@@ -0,0 +1,63 @@
.messageItem {
padding: 24px 0;
.messageItemSection {
display: inline-block;
}
.messageItemSectionLeft {
width: 80%;
}
.messageItemContent {
display: inline-flex;
gap: 20px;
}
.messageItemContentReverse {
flex-direction: row-reverse;
}

.messageTextBase() {
padding: 6px 10px;
border-radius: 8px;
& > p {
margin: 0;
}
}
.messageText {
.chunkText();
.messageTextBase();
background-color: #e6f4ff;
word-break: break-word;
}
.messageTextDark {
.chunkText();
.messageTextBase();
background-color: #1668dc;
word-break: break-word;
:global(section.think) {
color: rgb(166, 166, 166);
border-left-color: rgb(78, 78, 86);
}
}

.messageUserText {
.chunkText();
.messageTextBase();
background-color: rgba(255, 255, 255, 0.3);
word-break: break-word;
text-align: justify;
}
.messageEmpty {
width: 300px;
}

.thumbnailImg {
max-width: 20px;
}
}

.messageItemLeft {
text-align: left;
}

.messageItemRight {
text-align: right;
}

+ 244
- 0
web/src/components/next-message-item/index.tsx View File

@@ -0,0 +1,244 @@
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import { MessageType } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
import classNames from 'classnames';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';

import {
useFetchDocumentInfosByIds,
useFetchDocumentThumbnailsByIds,
} from '@/hooks/document-hooks';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { IMessage } from '@/pages/chat/interface';
import MarkdownContent from '@/pages/chat/markdown-content';
import { getExtension, isImage } from '@/utils/document-util';
import { Avatar, Button, Flex, List, Space, Typography } from 'antd';
import FileIcon from '../file-icon';
import IndentedTreeModal from '../indented-tree/modal';
import NewDocumentLink from '../new-document-link';
import { useTheme } from '../theme-provider';
import { AssistantGroupButton, UserGroupButton } from './group-button';
import styles from './index.less';

const { Text } = Typography;

interface IProps extends Partial<IRemoveMessageById>, IRegenerateMessage {
item: IMessage;
reference: IReference;
loading?: boolean;
sendLoading?: boolean;
visibleAvatar?: boolean;
nickname?: string;
avatar?: string;
avatarDialog?: string | null;
clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void;
index: number;
showLikeButton?: boolean;
showLoudspeaker?: boolean;
}

const MessageItem = ({
item,
reference,
loading = false,
avatar,
avatarDialog,
sendLoading = false,
clickDocumentButton,
index,
removeMessageById,
regenerateMessage,
showLikeButton = true,
showLoudspeaker = true,
visibleAvatar = true,
}: IProps) => {
const { theme } = useTheme();
const isAssistant = item.role === MessageType.Assistant;
const isUser = item.role === MessageType.User;
const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds();
const { data: documentThumbnails, setDocumentIds: setIds } =
useFetchDocumentThumbnailsByIds();
const { visible, hideModal, showModal } = useSetModalState();
const [clickedDocumentId, setClickedDocumentId] = useState('');

const referenceDocumentList = useMemo(() => {
return reference?.doc_aggs ?? [];
}, [reference?.doc_aggs]);

const handleUserDocumentClick = useCallback(
(id: string) => () => {
setClickedDocumentId(id);
showModal();
},
[showModal],
);

const handleRegenerateMessage = useCallback(() => {
regenerateMessage?.(item);
}, [regenerateMessage, item]);

useEffect(() => {
const ids = item?.doc_ids ?? [];
if (ids.length) {
setDocumentIds(ids);
const documentIds = ids.filter((x) => !(x in documentThumbnails));
if (documentIds.length) {
setIds(documentIds);
}
}
}, [item.doc_ids, setDocumentIds, setIds, documentThumbnails]);

return (
<div
className={classNames(styles.messageItem, {
[styles.messageItemLeft]: item.role === MessageType.Assistant,
[styles.messageItemRight]: item.role === MessageType.User,
})}
>
<section
className={classNames(styles.messageItemSection, {
[styles.messageItemSectionLeft]: item.role === MessageType.Assistant,
[styles.messageItemSectionRight]: item.role === MessageType.User,
})}
>
<div
className={classNames(styles.messageItemContent, {
[styles.messageItemContentReverse]: item.role === MessageType.User,
})}
>
{visibleAvatar &&
(item.role === MessageType.User ? (
<Avatar size={40} src={avatar ?? '/logo.svg'} />
) : avatarDialog ? (
<Avatar size={40} src={avatarDialog} />
) : (
<AssistantIcon />
))}

<Flex vertical gap={8} flex={1}>
<Space>
{isAssistant ? (
index !== 0 && (
<AssistantGroupButton
messageId={item.id}
content={item.content}
prompt={item.prompt}
showLikeButton={showLikeButton}
audioBinary={item.audio_binary}
showLoudspeaker={showLoudspeaker}
></AssistantGroupButton>
)
) : (
<UserGroupButton
content={item.content}
messageId={item.id}
removeMessageById={removeMessageById}
regenerateMessage={
regenerateMessage && handleRegenerateMessage
}
sendLoading={sendLoading}
></UserGroupButton>
)}

{/* <b>{isAssistant ? '' : nickname}</b> */}
</Space>
<div
className={
isAssistant
? theme === 'dark'
? styles.messageTextDark
: styles.messageText
: styles.messageUserText
}
>
<MarkdownContent
loading={loading}
content={item.content}
reference={reference}
clickDocumentButton={clickDocumentButton}
></MarkdownContent>
</div>
{isAssistant && referenceDocumentList.length > 0 && (
<List
bordered
dataSource={referenceDocumentList}
renderItem={(item) => {
return (
<List.Item>
<Flex gap={'small'} align="center">
<FileIcon
id={item.doc_id}
name={item.doc_name}
></FileIcon>

<NewDocumentLink
documentId={item.doc_id}
documentName={item.doc_name}
prefix="document"
link={item.url}
>
{item.doc_name}
</NewDocumentLink>
</Flex>
</List.Item>
);
}}
/>
)}
{isUser && documentList.length > 0 && (
<List
bordered
dataSource={documentList}
renderItem={(item) => {
// TODO:
// const fileThumbnail =
// documentThumbnails[item.id] || documentThumbnails[item.id];
const fileExtension = getExtension(item.name);
return (
<List.Item>
<Flex gap={'small'} align="center">
<FileIcon id={item.id} name={item.name}></FileIcon>

{isImage(fileExtension) ? (
<NewDocumentLink
documentId={item.id}
documentName={item.name}
prefix="document"
>
{item.name}
</NewDocumentLink>
) : (
<Button
type={'text'}
onClick={handleUserDocumentClick(item.id)}
>
<Text
style={{ maxWidth: '40vw' }}
ellipsis={{ tooltip: item.name }}
>
{item.name}
</Text>
</Button>
)}
</Flex>
</List.Item>
);
}}
/>
)}
</Flex>
</div>
</section>
{visible && (
<IndentedTreeModal
visible={visible}
hideModal={hideModal}
documentId={clickedDocumentId}
></IndentedTreeModal>
)}
</div>
);
};

export default memo(MessageItem);

+ 30
- 0
web/src/components/next-message-item/prompt-modal.tsx View File

@@ -0,0 +1,30 @@
import { IModalProps } from '@/interfaces/common';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { Modal, Space } from 'antd';
import HightLightMarkdown from '../highlight-markdown';
import SvgIcon from '../svg-icon';

const PromptModal = ({
visible,
hideModal,
prompt,
}: IModalProps<IFeedbackRequestBody> & { prompt?: string }) => {
return (
<Modal
title={
<Space>
<SvgIcon name={`prompt`} width={18}></SvgIcon>
Prompt
</Space>
}
width={'80%'}
open={visible}
onCancel={hideModal}
footer={null}
>
<HightLightMarkdown>{prompt}</HightLightMarkdown>
</Modal>
);
};

export default PromptModal;

+ 53
- 45
web/src/components/ui/accordion.tsx View File

@@ -1,58 +1,66 @@
'use client';

import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDown } from 'lucide-react';
import { ChevronDownIcon } from 'lucide-react';
import * as React from 'react';

import { cn } from '@/lib/utils';

const Accordion = AccordionPrimitive.Root;
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}

const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn('border-b', className)}
{...props}
/>
));
AccordionItem.displayName = 'AccordionItem';

const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className,
)}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn('border-b last:border-b-0', className)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
/>
);
}

const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
));
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}

AccordionContent.displayName = AccordionPrimitive.Content.displayName;
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn('pt-0 pb-4', className)}>{children}</div>
</AccordionPrimitive.Content>
);
}

export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

+ 73
- 0
web/src/components/ui/toggle-group.tsx View File

@@ -0,0 +1,73 @@
'use client';

import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import { type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { toggleVariants } from '@/components/ui/toggle';
import { cn } from '@/lib/utils';

const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: 'default',
variant: 'default',
});

function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
'group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs',
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}

function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);

return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l',
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
}

export { ToggleGroup, ToggleGroupItem };

+ 47
- 0
web/src/components/ui/toggle.tsx View File

@@ -0,0 +1,47 @@
'use client';

import * as TogglePrimitive from '@radix-ui/react-toggle';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/lib/utils';

const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-9 px-2 min-w-9',
sm: 'h-8 px-1.5 min-w-8',
lg: 'h-10 px-2.5 min-w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);

function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
}

export { Toggle, toggleVariants };

+ 3
- 1
web/src/hooks/use-send-message.ts View File

@@ -38,7 +38,9 @@ export type INodeEvent = IAnswerEvent<INodeData>;

export type IMessageEvent = IAnswerEvent<IMessageData>;

export type IEventList = Array<INodeEvent | IMessageEvent>;
export type IChatEvent = INodeEvent | IMessageEvent;

export type IEventList = Array<IChatEvent>;

export const useSendMessageBySSE = (url: string = api.completeConversation) => {
const [answerList, setAnswerList] = useState<IEventList>([]);

+ 23
- 7
web/src/pages/agent/canvas/index.tsx View File

@@ -6,7 +6,11 @@ import {
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { ChatSheet } from '../chat/chat-sheet';
import { AgentInstanceContext } from '../context';
import {
AgentChatContext,
AgentChatLogContext,
AgentInstanceContext,
} from '../context';
import FormSheet from '../form-sheet/next';
import {
useHandleDrop,
@@ -16,6 +20,7 @@ import {
} from '../hooks';
import { useAddNode } from '../hooks/use-add-node';
import { useBeforeDelete } from '../hooks/use-before-delete';
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
import { useShowDrawer, useShowLogSheet } from '../hooks/use-show-drawer';
import { LogSheet } from '../log-sheet';
import RunSheet from '../run-sheet';
@@ -101,7 +106,12 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
hideDrawer,
});

const { showLogSheet, logSheetVisible, hideLogSheet } = useShowLogSheet();
const { addEventList, setCurrentMessageId, currentEventListWithoutMessage } =
useCacheChatLog();

const { showLogSheet, logSheetVisible, hideLogSheet } = useShowLogSheet({
setCurrentMessageId,
});

const { handleBeforeDelete } = useBeforeDelete();

@@ -176,10 +186,13 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
</AgentInstanceContext.Provider>
)}
{chatVisible && (
<ChatSheet
visible={chatVisible}
hideModal={hideRunOrChatDrawer}
></ChatSheet>
<AgentChatContext.Provider value={{ showLogSheet }}>
<AgentChatLogContext.Provider
value={{ addEventList, setCurrentMessageId }}
>
<ChatSheet hideModal={hideRunOrChatDrawer}></ChatSheet>
</AgentChatLogContext.Provider>
</AgentChatContext.Provider>
)}
{runVisible && (
<RunSheet
@@ -188,7 +201,10 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
></RunSheet>
)}
{logSheetVisible && (
<LogSheet hideModal={hideLogSheet} showModal={showLogSheet}></LogSheet>
<LogSheet
hideModal={hideLogSheet}
currentEventListWithoutMessage={currentEventListWithoutMessage}
></LogSheet>
)}
</div>
);

+ 1
- 1
web/src/pages/agent/chat/box.tsx View File

@@ -1,4 +1,3 @@
import MessageItem from '@/components/message-item';
import { MessageType } from '@/constants/chat';
import { useGetFileIcon } from '@/pages/chat/hooks';
import { buildMessageItemReference } from '@/pages/chat/utils';
@@ -7,6 +6,7 @@ import { Spin } from 'antd';
import { useSendNextMessage } from './hooks';

import MessageInput from '@/components/message-input';
import MessageItem from '@/components/next-message-item';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { useFetchAgent } from '@/hooks/use-agent-request';

+ 9
- 2
web/src/pages/agent/chat/chat-sheet.tsx View File

@@ -8,9 +8,16 @@ import { IModalProps } from '@/interfaces/common';
import { cn } from '@/lib/utils';
import AgentChatBox from './box';

export function ChatSheet({ visible, hideModal }: IModalProps<any>) {
export function ChatSheet({ hideModal }: IModalProps<any>) {
return (
<Sheet open={visible} modal={false} onOpenChange={hideModal}>
<Sheet
open
modal={false}
onOpenChange={(open) => {
console.log('🚀 ~ ChatSheet ~ open:', open);
hideModal();
}}
>
<SheetTitle className="hidden"></SheetTitle>
<SheetContent className={cn('top-20 p-0')}>
<SheetHeader>

+ 7
- 1
web/src/pages/agent/chat/hooks.ts View File

@@ -16,10 +16,11 @@ import api from '@/utils/api';
import { message } from 'antd';
import { get } from 'lodash';
import trim from 'lodash/trim';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useContext, useEffect, useMemo } from 'react';
import { useParams } from 'umi';
import { v4 as uuid } from 'uuid';
import { BeginId } from '../constant';
import { AgentChatLogContext } from '../context';
import useGraphStore from '../store';
import { receiveMessageError } from '../utils';

@@ -86,6 +87,7 @@ export const useSendNextMessage = () => {
const { id: agentId } = useParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { refetch } = useFetchAgent();
const { addEventList } = useContext(AgentChatLogContext);

const { send, answerList, done, stopOutputMessage } = useSendMessageBySSE(
api.runCanvas,
@@ -160,6 +162,10 @@ export const useSendNextMessage = () => {
}
}, [addNewestAnswer, prologue]);

useEffect(() => {
addEventList(answerList);
}, [addEventList, answerList]);

return {
handlePressEnter,
handleInputChange,

+ 20
- 0
web/src/pages/agent/context.ts View File

@@ -1,6 +1,8 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { createContext } from 'react';
import { useAddNode } from './hooks/use-add-node';
import { useCacheChatLog } from './hooks/use-cache-chat-log';
import { useShowLogSheet } from './hooks/use-show-drawer';

export const AgentFormContext = createContext<RAGFlowNodeType | undefined>(
undefined,
@@ -14,3 +16,21 @@ type AgentInstanceContextType = Pick<
export const AgentInstanceContext = createContext<AgentInstanceContextType>(
{} as AgentInstanceContextType,
);

type AgentChatContextType = Pick<
ReturnType<typeof useShowLogSheet>,
'showLogSheet'
>;

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

type AgentChatLogContextType = Pick<
ReturnType<typeof useCacheChatLog>,
'addEventList' | 'setCurrentMessageId'
>;

export const AgentChatLogContext = createContext<AgentChatLogContextType>(
{} as AgentChatLogContextType,
);

+ 61
- 0
web/src/pages/agent/hooks/use-cache-chat-log.ts View File

@@ -0,0 +1,61 @@
import { IEventList, MessageEventType } from '@/hooks/use-send-message';
import { useCallback, useMemo, useState } from 'react';

export const ExcludeTypes = [
MessageEventType.Message,
MessageEventType.MessageEnd,
];

export function useCacheChatLog() {
const [eventList, setEventList] = useState<IEventList>([]);
const [currentMessageId, setCurrentMessageId] = useState('');

const filterEventListByMessageId = useCallback(
(messageId: string) => {
return eventList.filter((x) => x.message_id === messageId);
},
[eventList],
);

const filterEventListByEventType = useCallback(
(eventType: string) => {
return eventList.filter((x) => x.event === eventType);
},
[eventList],
);

const clearEventList = useCallback(() => {
setEventList([]);
}, []);

const addEventList = useCallback((events: IEventList) => {
setEventList((list) => {
const nextList = [...list];
events.forEach((x) => {
if (nextList.every((y) => y !== x)) {
nextList.push(x);
}
});
return nextList;
});
}, []);

const currentEventListWithoutMessage = useMemo(() => {
return eventList.filter(
(x) =>
x.message_id === currentMessageId &&
ExcludeTypes.every((y) => y !== x.event),
);
}, [currentMessageId, eventList]);

return {
eventList,
currentEventListWithoutMessage,
setEventList,
clearEventList,
addEventList,
filterEventListByEventType,
filterEventListByMessageId,
setCurrentMessageId,
};
}

+ 13
- 2
web/src/pages/agent/hooks/use-show-drawer.tsx View File

@@ -5,6 +5,7 @@ import { useCallback, useEffect } from 'react';
import { Operator } from '../constant';
import { BeginQuery } from '../interface';
import useGraphStore from '../store';
import { useCacheChatLog } from './use-cache-chat-log';
import { useGetBeginNodeDataQuery } from './use-get-begin-query';
import { useSaveGraph } from './use-save-graph';

@@ -152,12 +153,22 @@ export function useShowDrawer({
};
}

export function useShowLogSheet() {
export function useShowLogSheet({
setCurrentMessageId,
}: Pick<ReturnType<typeof useCacheChatLog>, 'setCurrentMessageId'>) {
const { visible, showModal, hideModal } = useSetModalState();

const handleShow = useCallback(
(messageId: string) => {
setCurrentMessageId(messageId);
showModal();
},
[setCurrentMessageId, showModal],
);

return {
logSheetVisible: visible,
hideLogSheet: hideModal,
showLogSheet: showModal,
showLogSheet: handleShow,
};
}

+ 102
- 9
web/src/pages/agent/log-sheet/index.tsx View File

@@ -1,23 +1,116 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { INodeEvent, MessageEventType } from '@/hooks/use-send-message';
import { IModalProps } from '@/interfaces/common';
import { cn } from '@/lib/utils';
import { NotebookText } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import JsonView from 'react18-json-view';
import 'react18-json-view/src/style.css';
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
import useGraphStore from '../store';

type LogSheetProps = IModalProps<any> &
Pick<ReturnType<typeof useCacheChatLog>, 'currentEventListWithoutMessage'>;

export function LogSheet({ hideModal }: IModalProps<any>) {
function JsonViewer({
data,
title,
}: {
data: Record<string, any>;
title: string;
}) {
return (
<Sheet open onOpenChange={hideModal}>
<SheetContent>
<section className="space-y-2">
<div>{title}</div>
<JsonView
src={data}
displaySize
collapseStringsAfterLength={100000000000}
className="w-full h-[200px] break-words overflow-auto p-2 bg-slate-800"
/>
</section>
);
}

export function LogSheet({
hideModal,
currentEventListWithoutMessage,
}: LogSheetProps) {
const getNode = useGraphStore((state) => state.getNode);

const getNodeName = useCallback(
(nodeId: string) => {
return getNode(nodeId)?.data.name;
},
[getNode],
);

const finishedNodeList = useMemo(() => {
return currentEventListWithoutMessage.filter(
(x) => x.event === MessageEventType.NodeFinished,
) as INodeEvent[];
}, [currentEventListWithoutMessage]);

return (
<Sheet open onOpenChange={hideModal} modal={false}>
<SheetContent className="top-20 right-96">
<SheetHeader>
<SheetTitle>Are you absolutely sure?</SheetTitle>
<SheetDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</SheetDescription>
<SheetTitle className="flex items-center gap-1">
<NotebookText className="size-4" />
Log
</SheetTitle>
</SheetHeader>
<section className="max-h-[82vh] overflow-auto">
{finishedNodeList.map((x, idx) => (
<section key={idx}>
<Accordion type="single" collapsible>
<AccordionItem value={idx.toString()}>
<AccordionTrigger>
<div className="flex gap-2 items-center">
<span>{getNodeName(x.data?.component_id)}</span>
<span className="text-text-sub-title text-xs">
{x.data.elapsed_time?.toString().slice(0, 6)}
</span>
<span
className={cn(
'border-background -end-1 -top-1 size-2 rounded-full border-2 bg-dot-green',
{ 'text-dot-green': x.data.error === null },
{ 'text-dot-red': x.data.error !== null },
)}
>
<span className="sr-only">Online</span>
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-2">
<JsonViewer
data={x.data.inputs}
title="Input"
></JsonViewer>

<JsonViewer
data={x.data.outputs}
title="Output"
></JsonViewer>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
))}
</section>
</SheetContent>
</Sheet>
);

+ 0
- 56
web/src/pages/demo.tsx View File

@@ -1,56 +0,0 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Form, useForm } from 'react-hook-form';
import { z } from 'zod';

import { Button } from '@/components/ui/button';
import { useCallback, useState } from 'react';
import DynamicCategorize from './agent/form/categorize-form/dynamic-categorize';

const formSchema = z.object({
items: z
.array(
z
.object({
name: z.string().min(1, 'xxx').trim(),
description: z.string().optional(),
// examples: z
// .array(
// z.object({
// value: z.string(),
// }),
// )
// .optional(),
})
.optional(),
)
.optional(),
});

export function Demo() {
const [flag, setFlag] = useState(false);

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
items: [],
},
});

const handleReset = useCallback(() => {
form?.reset();
}, [form]);

const handleSwitch = useCallback(() => {
setFlag(true);
}, []);

return (
<div>
<Form {...form}>
<DynamicCategorize></DynamicCategorize>
</Form>
<Button onClick={handleReset}>reset</Button>
<Button onClick={handleSwitch}>switch</Button>
</div>
);
}

+ 2
- 0
web/tailwind.config.js View File

@@ -54,6 +54,8 @@ module.exports = {
'background-highlight': 'var(--background-highlight)',

'input-border': 'var(--input-border)',
'dot-green': 'var(--dot-green)',
'dot-red': 'var(--dot-red)',

primary: {
DEFAULT: 'hsl(var(--primary))',

+ 5
- 0
web/tailwind.css View File

@@ -89,6 +89,8 @@
--background-highlight: rgba(76, 164, 231, 0.1);

--input-border: rgba(22, 22, 24, 0.2);
--dot-green: rgba(59, 160, 92, 1);
--dot-red: rgba(216, 73, 75, 1);
}

.dark {
@@ -199,6 +201,9 @@
--background-highlight: rgba(76, 164, 231, 0.1);

--input-border: rgba(255, 255, 255, 0.2);

--dot-green: rgba(59, 160, 92, 1);
--dot-red: rgba(216, 73, 75, 1);
}
}


Loading…
Cancel
Save