mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-09 21:08:59 +00:00
Native data AI application framework based on AWEL+AGENT (#1152)
Co-authored-by: Fangyin Cheng <staneyffer@gmail.com> Co-authored-by: lcx01800250 <lcx01800250@alibaba-inc.com> Co-authored-by: licunxing <864255598@qq.com> Co-authored-by: Aralhi <xiaoping0501@gmail.com> Co-authored-by: xuyuan23 <643854343@qq.com> Co-authored-by: aries_ckt <916701291@qq.com> Co-authored-by: hzh97 <2976151305@qq.com>
This commit is contained in:
112
web/components/common/chat-dialog.tsx
Normal file
112
web/components/common/chat-dialog.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import useChat from '@/hooks/use-chat';
|
||||
import CompletionInput from './completion-input';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { IChatDialogueMessageSchema, IChatDialogueSchema } from '@/types/chat';
|
||||
import AgentContent from '../chat/agent-content';
|
||||
import { renderModelIcon } from '../chat/header/model-selector';
|
||||
import MyEmpty from './MyEmpty';
|
||||
import { CaretLeftOutlined } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { apiInterceptors, newDialogue } from '@/client/api';
|
||||
import ChatContent from '../chat/chat-content';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
completionApi?: string;
|
||||
chatMode: IChatDialogueSchema['chat_mode'];
|
||||
chatParams?: {
|
||||
select_param?: string;
|
||||
} & Record<string, string>;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
function ChatDialog({ title, chatMode, completionApi, chatParams, model = '' }: Props) {
|
||||
const chat = useChat({ queryAgentURL: completionApi });
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [list, setList] = useState<IChatDialogueMessageSchema[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data } = useRequest(
|
||||
async () => {
|
||||
const [, res] = await apiInterceptors(newDialogue({ chat_mode: chatMode }));
|
||||
return res;
|
||||
},
|
||||
{
|
||||
ready: !!chatMode,
|
||||
},
|
||||
);
|
||||
|
||||
const handleChat = useCallback(
|
||||
(content: string) => {
|
||||
if (!data) return;
|
||||
return new Promise<void>((resolve) => {
|
||||
const tempList: IChatDialogueMessageSchema[] = [
|
||||
...list,
|
||||
{ role: 'human', context: content, model_name: model, order: 0, time_stamp: 0 },
|
||||
{ role: 'view', context: '', model_name: model, order: 0, time_stamp: 0 },
|
||||
];
|
||||
const index = tempList.length - 1;
|
||||
setList([...tempList]);
|
||||
setLoading(true);
|
||||
chat({
|
||||
chatId: data?.conv_uid,
|
||||
data: { ...chatParams, chat_mode: chatMode, model_name: model, user_input: content },
|
||||
onMessage: (message) => {
|
||||
tempList[index].context = message;
|
||||
setList([...tempList]);
|
||||
},
|
||||
onDone: () => {
|
||||
resolve();
|
||||
},
|
||||
onClose: () => {
|
||||
resolve();
|
||||
},
|
||||
onError: (message) => {
|
||||
tempList[index].context = message;
|
||||
setList([...tempList]);
|
||||
resolve();
|
||||
},
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
});
|
||||
},
|
||||
[chat, list, data?.conv_uid],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'fixed top-0 right-0 w-[30rem] h-screen flex flex-col bg-white dark:bg-theme-dark-container shadow-[-5px_0_40px_-4px_rgba(100,100,100,.1)] transition-transform duration-300',
|
||||
{
|
||||
'translate-x-0': open,
|
||||
'translate-x-full': !open,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{title && <div className="p-4 border-b border-solid border-gray-100">{title}</div>}
|
||||
<div className="flex-1 overflow-y-auto px-2">
|
||||
{list.map((item, index) => (
|
||||
<>{chatParams?.chat_mode === 'chat_agent' ? <AgentContent key={index} content={item} /> : <ChatContent key={index} content={item} />}</>
|
||||
))}
|
||||
{!list.length && <MyEmpty description="" />}
|
||||
</div>
|
||||
<div className="flex w-full p-4 border-t border-solid border-gray-100 items-center">
|
||||
{model && <div className="mr-2 flex">{renderModelIcon(model)}</div>}
|
||||
<CompletionInput loading={loading} onSubmit={handleChat} />
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-center rounded-tl rounded-bl cursor-pointer w-5 h-11 absolute top-[50%] -left-5 -translate-y-[50%] bg-white"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
>
|
||||
<CaretLeftOutlined />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatDialog;
|
135
web/components/common/gpt-card.tsx
Normal file
135
web/components/common/gpt-card.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React, { HtmlHTMLAttributes, PropsWithChildren, ReactNode, memo, useCallback, useMemo } from 'react';
|
||||
import { Tag, TagProps, Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
desc?: string;
|
||||
disabled?: boolean;
|
||||
tags?: (
|
||||
| string
|
||||
| {
|
||||
text: ReactNode;
|
||||
/** @default false */
|
||||
border?: boolean;
|
||||
/** @default default */
|
||||
color?: TagProps['color'];
|
||||
}
|
||||
)[];
|
||||
operations?: {
|
||||
children: ReactNode;
|
||||
label?: string;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
icon?: ReactNode;
|
||||
iconBorder?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function GPTCard({
|
||||
icon,
|
||||
iconBorder = true,
|
||||
title,
|
||||
desc,
|
||||
tags,
|
||||
children,
|
||||
disabled,
|
||||
operations,
|
||||
className,
|
||||
...props
|
||||
}: PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & Props>) {
|
||||
const iconNode = useMemo(() => {
|
||||
if (!icon) return null;
|
||||
|
||||
if (typeof icon === 'string') {
|
||||
return (
|
||||
<Image
|
||||
className={classNames('w-11 h-11 rounded-full mr-4 object-contain bg-white', {
|
||||
'border border-gray-200': iconBorder,
|
||||
})}
|
||||
width={44}
|
||||
height={44}
|
||||
src={icon}
|
||||
alt={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return icon;
|
||||
}, [icon]);
|
||||
|
||||
const tagNode = useMemo(() => {
|
||||
if (!tags || !tags.length) return null;
|
||||
return (
|
||||
<div className="flex items-center mt-1 flex-wrap">
|
||||
{tags.map((tag, index) => {
|
||||
if (typeof tag === 'string') {
|
||||
return (
|
||||
<Tag key={index} className="text-xs" bordered={false} color="default">
|
||||
{tag}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tag key={index} className="text-xs" bordered={tag.border ?? false} color={tag.color}>
|
||||
{tag.text}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}, [tags]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'group/card relative flex flex-col w-72 rounded justify-between text-black bg-white shadow-[0_8px_16px_-10px_rgba(100,100,100,.08)] hover:shadow-[0_14px_20px_-10px_rgba(100,100,100,.15)] dark:bg-[#232734] dark:text-white dark:hover:border-white transition-[transfrom_shadow] duration-300 hover:-translate-y-1 min-h-fit',
|
||||
{
|
||||
'grayscale cursor-no-drop': disabled,
|
||||
'cursor-pointer': !disabled && !!props.onClick,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center">
|
||||
{iconNode}
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-sm font-semibold">{title}</h2>
|
||||
{tagNode}
|
||||
</div>
|
||||
</div>
|
||||
{desc && (
|
||||
<Tooltip title={desc}>
|
||||
<p className="mt-2 text-sm text-gray-500 font-normal line-clamp-2">{desc}</p>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{children}
|
||||
{operations && !!operations.length && (
|
||||
<div className="flex flex-wrap items-center justify-center border-t border-solid border-gray-100 dark:border-theme-dark">
|
||||
{operations.map((item, index) => (
|
||||
<Tooltip key={`operation-${index}`} title={item.label}>
|
||||
<div
|
||||
className="relative flex flex-1 items-center justify-center h-11 text-gray-400 hover:text-blue-500 transition-colors duration-300 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
item.onClick?.();
|
||||
}}
|
||||
>
|
||||
{item.children}
|
||||
{index < operations.length - 1 && <div className="w-[1px] h-6 absolute top-2 right-0 bg-gray-100 rounded dark:bg-theme-dark" />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(GPTCard);
|
23
web/components/common/icon-wrapper.tsx
Normal file
23
web/components/common/icon-wrapper.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
interface IconWrapperProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Icon wrapper, with background color and hover color. same width and height
|
||||
const IconWrapper: React.FC<IconWrapperProps> = ({ children, className }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex justify-center items-center w-8 h-8 rounded-full dark:bg-zinc-700 hover:bg-stone-200 dark:hover:bg-zinc-900',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconWrapper;
|
Reference in New Issue
Block a user