mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-13 13:07:32 +00:00
Integrate Chat and Copilot into chat-sdk, and add SQL parse display (#166)
This commit is contained in:
@@ -8,13 +8,17 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@uiw/react-watermark": "^0.0.5",
|
||||
"ahooks": "^3.7.8",
|
||||
"antd": "^5.5.2",
|
||||
"axios": "^1.4.0",
|
||||
"axios": "^0.21.1",
|
||||
"classnames": "^2.3.2",
|
||||
"echarts": "^5.4.2",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "^2.29.4",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-spinners": "^0.13.8",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"sql-formatter": "^2.3.3",
|
||||
"tslib": "^2.5.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -72,6 +76,7 @@
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.14.198",
|
||||
"@types/node": "^16.18.31",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
@@ -122,6 +127,7 @@
|
||||
"rollup-plugin-exclude-dependencies-from-bundle": "^1.1.23",
|
||||
"rollup-plugin-less": "^1.1.3",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-styles": "^4.0.0",
|
||||
"rollup-plugin-typescript2": "^0.34.1",
|
||||
"sass-loader": "^12.3.0",
|
||||
"semver": "^7.3.5",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="root" style="height: 100vh;"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -6,20 +6,10 @@
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { nodeResolve } from '@rollup/plugin-node-resolve'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json'
|
||||
import less from 'rollup-plugin-less'
|
||||
import styles from "rollup-plugin-styles";
|
||||
import postcss from 'rollup-plugin-postcss'
|
||||
import cssnano from 'cssnano'
|
||||
|
||||
@@ -18,7 +19,12 @@ const config = {
|
||||
commonjs(),
|
||||
json(),
|
||||
typescript({ tsconfigOverride: overrides }),
|
||||
less({ output: 'dist/index.css' }),
|
||||
styles({
|
||||
// mode: ["extract"],
|
||||
// modules: true,
|
||||
autoModules: id => id.includes(".module."),
|
||||
}),
|
||||
// less({ output: 'dist/index.css' }),
|
||||
// postcss({
|
||||
// plugins: [
|
||||
// cssnano()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import basicConfig from './rollup.config.mjs'
|
||||
import excludeDependenciesFromBundle from "rollup-plugin-exclude-dependencies-from-bundle"
|
||||
import terser from '@rollup/plugin-terser'
|
||||
|
||||
const config = {
|
||||
...basicConfig,
|
||||
@@ -12,6 +13,7 @@ const config = {
|
||||
plugins: [
|
||||
...basicConfig.plugins,
|
||||
excludeDependenciesFromBundle(),
|
||||
terser()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
52
webapp/packages/chat-sdk/src/Chat/AgentList/index.tsx
Normal file
52
webapp/packages/chat-sdk/src/Chat/AgentList/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { PlusCircleOutlined } from '@ant-design/icons';
|
||||
import { AgentType } from '../type';
|
||||
import styles from './style.module.less';
|
||||
import classNames from 'classnames';
|
||||
import { message } from 'antd';
|
||||
import IconFont from '../../components/IconFont';
|
||||
import { AGENT_ICONS } from '../constants';
|
||||
|
||||
type Props = {
|
||||
agentList: AgentType[];
|
||||
currentAgent?: AgentType;
|
||||
onSelectAgent: (agent: AgentType) => void;
|
||||
};
|
||||
|
||||
const AgentList: React.FC<Props> = ({ agentList, currentAgent, onSelectAgent }) => {
|
||||
const onAddAgent = () => {
|
||||
message.info('正在开发中,敬请期待');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.agentList}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerTitle}>智能助理</div>
|
||||
<PlusCircleOutlined className={styles.plusIcon} onClick={onAddAgent} />
|
||||
</div>
|
||||
<div className={styles.agentListContent}>
|
||||
{agentList.map((agent, index) => {
|
||||
const agentItemClass = classNames(styles.agentItem, {
|
||||
[styles.active]: currentAgent?.id === agent.id,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className={agentItemClass}
|
||||
onClick={() => {
|
||||
onSelectAgent(agent);
|
||||
}}
|
||||
>
|
||||
<IconFont type={AGENT_ICONS[index % AGENT_ICONS.length]} className={styles.avatar} />
|
||||
<div className={styles.agentInfo}>
|
||||
<div className={styles.agentName}>{agent.name}</div>
|
||||
<div className={styles.agentDesc}>{agent.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentList;
|
||||
@@ -0,0 +1,80 @@
|
||||
.agentList {
|
||||
width: 248px;
|
||||
height: 100%;
|
||||
background: #f9f9f9;
|
||||
border-right: 1px solid #f1f1f1;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 0 16px;
|
||||
|
||||
.headerTitle {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.plusIcon {
|
||||
color: var(--text-color);
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agentListContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 8px;
|
||||
row-gap: 2px;
|
||||
|
||||
.agentItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 4px;
|
||||
column-gap: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
.avatar {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.agentInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 2px;
|
||||
|
||||
.agentName {
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.agentDesc {
|
||||
width: 160px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background: #22a5f7;
|
||||
|
||||
.agentName,
|
||||
.agentDesc {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
382
webapp/packages/chat-sdk/src/Chat/ChatFooter/index.tsx
Normal file
382
webapp/packages/chat-sdk/src/Chat/ChatFooter/index.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
import IconFont from '../../components/IconFont';
|
||||
import { getTextWidth, groupByColumn, isMobile } from '../../utils/utils';
|
||||
import { AutoComplete, Select, Tag } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import type { ForwardRefRenderFunction } from 'react';
|
||||
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP, HOLDER_TAG } from '../constants';
|
||||
import { AgentType, ModelType } from '../type';
|
||||
import { searchRecommend } from '../../service';
|
||||
import styles from './style.module.less';
|
||||
|
||||
type Props = {
|
||||
inputMsg: string;
|
||||
chatId?: number;
|
||||
currentAgent?: AgentType;
|
||||
agentList: AgentType[];
|
||||
onToggleHistoryVisible: () => void;
|
||||
onOpenAgents: () => void;
|
||||
onInputMsgChange: (value: string) => void;
|
||||
onSendMsg: (msg: string, modelId?: number) => void;
|
||||
onAddConversation: (agent?: AgentType) => void;
|
||||
onSelectAgent: (agent: AgentType) => void;
|
||||
};
|
||||
|
||||
const { OptGroup, Option } = Select;
|
||||
let isPinyin = false;
|
||||
let isSelect = false;
|
||||
|
||||
const compositionStartEvent = () => {
|
||||
isPinyin = true;
|
||||
};
|
||||
|
||||
const compositionEndEvent = () => {
|
||||
isPinyin = false;
|
||||
};
|
||||
|
||||
const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
{
|
||||
inputMsg,
|
||||
chatId,
|
||||
currentAgent,
|
||||
agentList,
|
||||
onToggleHistoryVisible,
|
||||
onOpenAgents,
|
||||
onInputMsgChange,
|
||||
onSendMsg,
|
||||
onAddConversation,
|
||||
onSelectAgent,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [modelOptions, setModelOptions] = useState<(ModelType | AgentType)[]>([]);
|
||||
const [stepOptions, setStepOptions] = useState<Record<string, any[]>>({});
|
||||
const [open, setOpen] = useState(false);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const inputRef = useRef<any>();
|
||||
const fetchRef = useRef(0);
|
||||
|
||||
const inputFocus = () => {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const inputBlur = () => {
|
||||
inputRef.current?.blur();
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
inputFocus,
|
||||
inputBlur,
|
||||
}));
|
||||
|
||||
const initEvents = () => {
|
||||
const autoCompleteEl = document.getElementById('chatInput');
|
||||
autoCompleteEl!.addEventListener('compositionstart', compositionStartEvent);
|
||||
autoCompleteEl!.addEventListener('compositionend', compositionEndEvent);
|
||||
};
|
||||
|
||||
const removeEvents = () => {
|
||||
const autoCompleteEl = document.getElementById('chatInput');
|
||||
if (autoCompleteEl) {
|
||||
autoCompleteEl.removeEventListener('compositionstart', compositionStartEvent);
|
||||
autoCompleteEl.removeEventListener('compositionend', compositionEndEvent);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initEvents();
|
||||
return () => {
|
||||
removeEvents();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getStepOptions = (recommends: any[]) => {
|
||||
const data = groupByColumn(recommends, 'modelName');
|
||||
return isMobile && recommends.length > 6
|
||||
? Object.keys(data)
|
||||
.slice(0, 4)
|
||||
.reduce((result, key) => {
|
||||
result[key] = data[key].slice(
|
||||
0,
|
||||
Object.keys(data).length > 2 ? 2 : Object.keys(data).length > 1 ? 3 : 6
|
||||
);
|
||||
return result;
|
||||
}, {})
|
||||
: data;
|
||||
};
|
||||
|
||||
const processMsg = (msg: string) => {
|
||||
let msgValue = msg;
|
||||
let modelId: number | undefined;
|
||||
if (msg?.[0] === '/') {
|
||||
const agent = agentList.find(item => msg.includes(`/${item.name}`));
|
||||
msgValue = agent ? msg.replace(`/${agent.name}`, '') : msg;
|
||||
}
|
||||
return { msgValue, modelId };
|
||||
};
|
||||
|
||||
const debounceGetWordsFunc = useCallback(() => {
|
||||
const getAssociateWords = async (msg: string, chatId?: number, currentAgent?: AgentType) => {
|
||||
if (isPinyin) {
|
||||
return;
|
||||
}
|
||||
if (msg === '' || (msg.length === 1 && msg[0] === '@')) {
|
||||
return;
|
||||
}
|
||||
fetchRef.current += 1;
|
||||
const fetchId = fetchRef.current;
|
||||
const { msgValue, modelId } = processMsg(msg);
|
||||
const res = await searchRecommend(msgValue.trim(), chatId, modelId, currentAgent?.id);
|
||||
if (fetchId !== fetchRef.current) {
|
||||
return;
|
||||
}
|
||||
const recommends = msgValue ? res.data || [] : [];
|
||||
const stepOptionList = recommends.map((item: any) => item.subRecommend);
|
||||
|
||||
if (stepOptionList.length > 0 && stepOptionList.every((item: any) => item !== null)) {
|
||||
setStepOptions(getStepOptions(recommends));
|
||||
} else {
|
||||
setStepOptions({});
|
||||
}
|
||||
setOpen(recommends.length > 0);
|
||||
};
|
||||
return debounce(getAssociateWords, 200);
|
||||
}, []);
|
||||
|
||||
const [debounceGetWords] = useState<any>(debounceGetWordsFunc);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputMsg.length === 1 && inputMsg[0] === '/') {
|
||||
setOpen(true);
|
||||
setModelOptions(agentList);
|
||||
setStepOptions({});
|
||||
return;
|
||||
}
|
||||
if (modelOptions.length > 0) {
|
||||
setTimeout(() => {
|
||||
setModelOptions([]);
|
||||
}, 50);
|
||||
}
|
||||
if (!isSelect) {
|
||||
debounceGetWords(inputMsg, chatId, currentAgent);
|
||||
} else {
|
||||
isSelect = false;
|
||||
}
|
||||
if (!inputMsg) {
|
||||
setStepOptions({});
|
||||
fetchRef.current = 0;
|
||||
}
|
||||
}, [inputMsg]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focused) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [focused]);
|
||||
|
||||
useEffect(() => {
|
||||
const autoCompleteDropdown = document.querySelector(
|
||||
`.${styles.autoCompleteDropdown}`
|
||||
) as HTMLElement;
|
||||
if (!autoCompleteDropdown) {
|
||||
return;
|
||||
}
|
||||
const textWidth = getTextWidth(inputMsg);
|
||||
if (Object.keys(stepOptions).length > 0) {
|
||||
autoCompleteDropdown.style.marginLeft = `${textWidth}px`;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
autoCompleteDropdown.style.marginLeft = `0px`;
|
||||
}, 200);
|
||||
}
|
||||
}, [stepOptions]);
|
||||
|
||||
const sendMsg = (value: string) => {
|
||||
const option = Object.keys(stepOptions)
|
||||
.reduce((result: any[], item) => {
|
||||
result = result.concat(stepOptions[item]);
|
||||
return result;
|
||||
}, [])
|
||||
.find(item =>
|
||||
Object.keys(stepOptions).length === 1
|
||||
? item.recommend === value
|
||||
: `${item.modelName || ''}${item.recommend}` === value
|
||||
);
|
||||
if (option && isSelect) {
|
||||
onSendMsg(option.recommend, Object.keys(stepOptions).length > 1 ? option.modelId : undefined);
|
||||
} else {
|
||||
onSendMsg(value.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, {
|
||||
[styles.mobile]: isMobile,
|
||||
[styles.modelOptions]: modelOptions.length > 0,
|
||||
});
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
isSelect = true;
|
||||
if (modelOptions.length > 0) {
|
||||
const agent = agentList.find(item => value.includes(item.name));
|
||||
if (agent) {
|
||||
if (agent.id !== currentAgent?.id) {
|
||||
onSelectAgent(agent);
|
||||
}
|
||||
onInputMsgChange('');
|
||||
}
|
||||
} else {
|
||||
onInputMsgChange(value.replace(HOLDER_TAG, ''));
|
||||
}
|
||||
setOpen(false);
|
||||
setTimeout(() => {
|
||||
isSelect = false;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const chatFooterClass = classNames(styles.chatFooter, {
|
||||
[styles.mobile]: isMobile,
|
||||
});
|
||||
|
||||
const modelOptionNodes = modelOptions.map(model => {
|
||||
return (
|
||||
<Option key={model.id} value={`/${model.name} `} className={styles.searchOption}>
|
||||
{model.name}
|
||||
</Option>
|
||||
);
|
||||
});
|
||||
|
||||
const associateOptionNodes = Object.keys(stepOptions).map(key => {
|
||||
return (
|
||||
<OptGroup key={key} label={key}>
|
||||
{stepOptions[key].map(option => {
|
||||
let optionValue =
|
||||
Object.keys(stepOptions).length === 1
|
||||
? option.recommend
|
||||
: `${option.modelName || ''}${option.recommend}`;
|
||||
if (inputMsg[0] === '/') {
|
||||
const agent = agentList.find(item => inputMsg.includes(item.name));
|
||||
optionValue = agent ? `/${agent.name} ${option.recommend}` : optionValue;
|
||||
}
|
||||
return (
|
||||
<Option
|
||||
key={`${option.recommend}${option.modelName ? `_${option.modelName}` : ''}`}
|
||||
value={`${optionValue}${HOLDER_TAG}`}
|
||||
className={styles.searchOption}
|
||||
>
|
||||
<div className={styles.optionContent}>
|
||||
{option.schemaElementType && (
|
||||
<Tag
|
||||
className={styles.semanticType}
|
||||
color={
|
||||
option.schemaElementType === SemanticTypeEnum.DIMENSION ||
|
||||
option.schemaElementType === SemanticTypeEnum.MODEL
|
||||
? 'blue'
|
||||
: option.schemaElementType === SemanticTypeEnum.VALUE
|
||||
? 'geekblue'
|
||||
: 'cyan'
|
||||
}
|
||||
>
|
||||
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
|
||||
option.schemaElementType ||
|
||||
'维度'}
|
||||
</Tag>
|
||||
)}
|
||||
{option.subRecommend}
|
||||
</div>
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</OptGroup>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={chatFooterClass}>
|
||||
<div className={styles.tools}>
|
||||
<div
|
||||
className={styles.toolItem}
|
||||
onClick={() => {
|
||||
onAddConversation();
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-c003xiaoxiduihua" className={styles.toolIcon} />
|
||||
<div>新对话</div>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className={styles.toolItem} onClick={onToggleHistoryVisible}>
|
||||
<IconFont type="icon-lishi" className={styles.toolIcon} />
|
||||
<div>历史对话</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.toolItem} onClick={onOpenAgents}>
|
||||
<IconFont type="icon-zhinengzhuli" className={styles.toolIcon} />
|
||||
<div>智能助理</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.composer}>
|
||||
<div className={styles.composerInputWrapper}>
|
||||
<AutoComplete
|
||||
className={styles.composerInput}
|
||||
placeholder={`智能助理${
|
||||
isMobile ? `[${currentAgent?.name}]` : `【${currentAgent?.name}】`
|
||||
}将与您对话,输入“/”可切换助理`}
|
||||
value={inputMsg}
|
||||
onChange={(value: string) => {
|
||||
onInputMsgChange(value);
|
||||
}}
|
||||
onSelect={onSelect}
|
||||
autoFocus={!isMobile}
|
||||
ref={inputRef}
|
||||
id="chatInput"
|
||||
onKeyDown={e => {
|
||||
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
|
||||
const chatInputEl: any = document.getElementById('chatInput');
|
||||
const agent = agentList.find(
|
||||
item => chatInputEl.value[0] === '/' && chatInputEl.value.includes(item.name)
|
||||
);
|
||||
if (agent) {
|
||||
if (agent.id !== currentAgent?.id) {
|
||||
onSelectAgent(agent);
|
||||
}
|
||||
onInputMsgChange('');
|
||||
return;
|
||||
}
|
||||
if (!isSelect) {
|
||||
sendMsg(chatInputEl.value);
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocused(false);
|
||||
}}
|
||||
dropdownClassName={autoCompleteDropdownClass}
|
||||
listHeight={500}
|
||||
allowClear
|
||||
open={open}
|
||||
defaultActiveFirstOption={false}
|
||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
>
|
||||
{modelOptions.length > 0 ? modelOptionNodes : associateOptionNodes}
|
||||
</AutoComplete>
|
||||
<div
|
||||
className={classNames(styles.sendBtn, {
|
||||
[styles.sendBtnActive]: inputMsg?.length > 0,
|
||||
})}
|
||||
onClick={() => {
|
||||
sendMsg(inputMsg);
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-ios-send" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(ChatFooter);
|
||||
224
webapp/packages/chat-sdk/src/Chat/ChatFooter/style.module.less
Normal file
224
webapp/packages/chat-sdk/src/Chat/ChatFooter/style.module.less
Normal file
@@ -0,0 +1,224 @@
|
||||
.chatFooter {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 6px 20px 20px;
|
||||
|
||||
.tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
column-gap: 8px;
|
||||
|
||||
.toolItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 12px;
|
||||
column-gap: 6px;
|
||||
background-color: #f6f6f6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: flex;
|
||||
height: 70px;
|
||||
|
||||
.collapseBtn {
|
||||
height: 46px;
|
||||
margin: 0 10px;
|
||||
color: var(--text-color-third);
|
||||
font-size: 20px;
|
||||
line-height: 46px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.addConversation {
|
||||
height: 46px;
|
||||
margin: 0 20px 0 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 26px;
|
||||
line-height: 54px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.composerInputWrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.composerInput {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
:global {
|
||||
.ant-select-selector {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
background: #f9f9f9;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.15s ease-in-out;
|
||||
resize: none;
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
height: 40px !important;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.ant-select-selection-search {
|
||||
right: 0 !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
padding-left: 2px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-clear {
|
||||
right: auto;
|
||||
left: 500px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-select-focused {
|
||||
.ant-select-selector {
|
||||
box-shadow: rgb(74, 114, 245) 0px 0px 3px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
background-color: rgb(184, 184, 191);
|
||||
border: unset;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s ease 0s;
|
||||
|
||||
&.sendBtnActive {
|
||||
background-color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
margin: 6px 10px 10px;
|
||||
|
||||
.addConversation {
|
||||
height: 40px;
|
||||
margin: 0 12px 0 4px;
|
||||
}
|
||||
|
||||
.composer {
|
||||
height: 40px;
|
||||
:global {
|
||||
.ant-select-selector {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
padding-left: 0 !important;
|
||||
line-height: 38px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
right: 4px;
|
||||
bottom: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchOption {
|
||||
padding: 6px 20px;
|
||||
color: #212121;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.searchOption {
|
||||
min-height: 26px;
|
||||
padding: 2px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.model {
|
||||
margin-top: 2px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 13px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.autoCompleteDropdown {
|
||||
left: 20px !important;
|
||||
width: fit-content !important;
|
||||
min-width: 100px !important;
|
||||
border-radius: 6px;
|
||||
|
||||
&.modelOptions {
|
||||
width: 150px !important;
|
||||
|
||||
.searchOption {
|
||||
padding: 0 10px;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-select-item {
|
||||
height: 30px !important;
|
||||
line-height: 30px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.semanticType {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.quoteText {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
236
webapp/packages/chat-sdk/src/Chat/Conversation/index.tsx
Normal file
236
webapp/packages/chat-sdk/src/Chat/Conversation/index.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import { Dropdown, Input, Menu } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
forwardRef,
|
||||
ForwardRefRenderFunction,
|
||||
useImperativeHandle,
|
||||
memo,
|
||||
} from 'react';
|
||||
import ConversationModal from '../components/ConversationModal';
|
||||
import { deleteConversation, getAllConversations, saveConversation } from '../service';
|
||||
import styles from './style.module.less';
|
||||
import { AgentType, ConversationDetailType } from '../type';
|
||||
import { DEFAULT_CONVERSATION_NAME } from '../constants';
|
||||
import moment from 'moment';
|
||||
import { CloseOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
|
||||
type Props = {
|
||||
currentAgent?: AgentType;
|
||||
currentConversation?: ConversationDetailType;
|
||||
historyVisible?: boolean;
|
||||
onSelectConversation: (
|
||||
conversation: ConversationDetailType,
|
||||
sendMsgParams?: any,
|
||||
isAdd?: boolean
|
||||
) => void;
|
||||
onCloseConversation: () => void;
|
||||
};
|
||||
|
||||
const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
{ currentAgent, currentConversation, historyVisible, onSelectConversation, onCloseConversation },
|
||||
ref
|
||||
) => {
|
||||
const [conversations, setConversations] = useState<ConversationDetailType[]>([]);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [editConversation, setEditConversation] = useState<ConversationDetailType>();
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
updateData,
|
||||
onAddConversation,
|
||||
}));
|
||||
|
||||
const updateData = async (agentId?: number) => {
|
||||
const { data } = await getAllConversations(agentId || currentAgent!.id);
|
||||
const conversationList = data || [];
|
||||
setConversations(conversationList.slice(0, 200));
|
||||
return conversationList;
|
||||
};
|
||||
|
||||
const initData = async () => {
|
||||
const data = await updateData();
|
||||
if (data.length > 0) {
|
||||
onSelectConversation(data[0]);
|
||||
} else {
|
||||
onAddConversation();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentAgent) {
|
||||
if (currentAgent.initialSendMsgParams) {
|
||||
onAddConversation(currentAgent.initialSendMsgParams);
|
||||
} else {
|
||||
initData();
|
||||
}
|
||||
}
|
||||
}, [currentAgent]);
|
||||
|
||||
const addConversation = async (sendMsgParams?: any) => {
|
||||
const agentId = sendMsgParams?.agentId || currentAgent!.id;
|
||||
await saveConversation(DEFAULT_CONVERSATION_NAME, agentId);
|
||||
return updateData(agentId);
|
||||
};
|
||||
|
||||
const onDeleteConversation = async (id: number) => {
|
||||
await deleteConversation(id);
|
||||
initData();
|
||||
};
|
||||
|
||||
const onAddConversation = async (sendMsgParams?: any) => {
|
||||
const data = await addConversation(sendMsgParams);
|
||||
if (data.length > 0) {
|
||||
onSelectConversation(data[0], sendMsgParams, true);
|
||||
}
|
||||
};
|
||||
|
||||
const onOperate = (key: string, conversation: ConversationDetailType) => {
|
||||
if (key === 'editName') {
|
||||
setEditConversation(conversation);
|
||||
setEditModalVisible(true);
|
||||
} else if (key === 'delete') {
|
||||
onDeleteConversation(conversation.chatId);
|
||||
}
|
||||
};
|
||||
|
||||
const conversationClass = classNames(styles.conversation, {
|
||||
[styles.historyVisible]: historyVisible,
|
||||
});
|
||||
|
||||
const convertTime = (date: string) => {
|
||||
moment.locale('zh-cn');
|
||||
const now = moment();
|
||||
const inputDate = moment(date);
|
||||
const diffMinutes = now.diff(inputDate, 'minutes');
|
||||
if (diffMinutes < 1) {
|
||||
return '刚刚';
|
||||
} else if (inputDate.isSame(now, 'day')) {
|
||||
return inputDate.format('HH:mm');
|
||||
} else if (inputDate.isSame(now.subtract(1, 'day'), 'day')) {
|
||||
return '昨天';
|
||||
}
|
||||
return inputDate.format('MM/DD');
|
||||
};
|
||||
|
||||
const onSearchValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={conversationClass}>
|
||||
<div className={styles.rightSection}>
|
||||
<div className={styles.titleBar}>
|
||||
<div className={styles.title}>历史对话</div>
|
||||
<div className={styles.rightOperation}>
|
||||
<div
|
||||
className={styles.newConversation}
|
||||
onClick={() => {
|
||||
addConversation();
|
||||
}}
|
||||
>
|
||||
新对话
|
||||
</div>
|
||||
<CloseOutlined className={styles.closeIcon} onClick={onCloseConversation} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.searchConversation}>
|
||||
<Input
|
||||
placeholder="搜索"
|
||||
prefix={<SearchOutlined className={styles.searchIcon} />}
|
||||
className={styles.searchTask}
|
||||
value={searchValue}
|
||||
onChange={onSearchValueChange}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.conversationList}>
|
||||
{conversations
|
||||
.filter(
|
||||
conversation =>
|
||||
searchValue === '' ||
|
||||
conversation.chatName.toLowerCase().includes(searchValue.toLowerCase())
|
||||
)
|
||||
.map(item => {
|
||||
const conversationItemClass = classNames(styles.conversationItem, {
|
||||
[styles.activeConversationItem]: currentConversation?.chatId === item.chatId,
|
||||
});
|
||||
return (
|
||||
<Dropdown
|
||||
key={item.chatId}
|
||||
overlay={
|
||||
<Menu
|
||||
items={[
|
||||
{ label: '修改对话名称', key: 'editName' },
|
||||
{ label: '删除', key: 'delete' },
|
||||
]}
|
||||
onClick={({ key }) => {
|
||||
onOperate(key, item);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<div
|
||||
className={conversationItemClass}
|
||||
onClick={() => {
|
||||
onSelectConversation(item);
|
||||
}}
|
||||
>
|
||||
<div className={styles.conversationContent}>
|
||||
<div className={styles.topTitleBar}>
|
||||
<div className={styles.conversationTitleBar}>
|
||||
<div className={styles.conversationName}>{item.chatName}</div>
|
||||
{currentConversation?.chatId === item.chatId && (
|
||||
<div className={styles.currentConversation}>当前对话</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.conversationTime}>
|
||||
{convertTime(item.lastTime || '')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.bottomSection}>
|
||||
<div className={styles.subTitle}>{item.lastQuestion}</div>
|
||||
<DeleteOutlined
|
||||
className={styles.deleteIcon}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDeleteConversation(item.chatId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ConversationModal
|
||||
visible={editModalVisible}
|
||||
editConversation={editConversation}
|
||||
onClose={() => {
|
||||
setEditModalVisible(false);
|
||||
}}
|
||||
onFinish={() => {
|
||||
setEditModalVisible(false);
|
||||
updateData();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function areEqual(prevProps: Props, nextProps: Props) {
|
||||
if (
|
||||
prevProps.currentAgent?.id === nextProps.currentAgent?.id &&
|
||||
prevProps.currentConversation?.chatId === nextProps.currentConversation?.chatId &&
|
||||
prevProps.historyVisible === nextProps.historyVisible
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default memo(forwardRef(Conversation), areEqual);
|
||||
171
webapp/packages/chat-sdk/src/Chat/Conversation/style.module.less
Normal file
171
webapp/packages/chat-sdk/src/Chat/Conversation/style.module.less
Normal file
@@ -0,0 +1,171 @@
|
||||
.conversation {
|
||||
position: relative;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
|
||||
.rightSection {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.titleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.rightOperation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
|
||||
.newConversation {
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchConversation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0 10px;
|
||||
|
||||
.searchIcon {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.searchTask {
|
||||
font-size: 13px;
|
||||
background-color: #f5f5f5;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: none !important;
|
||||
|
||||
:global {
|
||||
.ant-input {
|
||||
font-size: 13px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 70px);
|
||||
padding: 2px 0 0;
|
||||
overflow-y: auto;
|
||||
row-gap: 12px;
|
||||
|
||||
.conversationItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #efefef;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
.conversationContent {
|
||||
width: 100%;
|
||||
|
||||
.topTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.conversationTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
|
||||
.conversationName {
|
||||
max-width: 300px;
|
||||
margin-right: 2px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.currentConversation {
|
||||
padding: 0 4px;
|
||||
color: var(--chat-blue);
|
||||
font-size: 12px;
|
||||
background-color: var(--light-blue-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.conversationTime {
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottomSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
|
||||
.subTitle {
|
||||
width: 350px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.deleteIcon {
|
||||
color: var(--text-color-six);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.activeConversationItem {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.historyVisible {
|
||||
width: 400px;
|
||||
padding: 10px 16px;
|
||||
border-left: 1px solid #f1f1f1;
|
||||
z-index: 99;
|
||||
}
|
||||
}
|
||||
150
webapp/packages/chat-sdk/src/Chat/MessageContainer/index.tsx
Normal file
150
webapp/packages/chat-sdk/src/Chat/MessageContainer/index.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import Text from '../components/Text';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { AgentType, MessageItem, MessageTypeEnum } from '../type';
|
||||
import { isMobile, updateMessageContainerScroll } from '../../utils/utils';
|
||||
import styles from './style.module.less';
|
||||
import AgentTip from '../components/AgentTip';
|
||||
import classNames from 'classnames';
|
||||
import { MsgDataType } from '../../common/type';
|
||||
import ChatItem from '../../components/ChatItem';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
chatId: number;
|
||||
messageList: MessageItem[];
|
||||
historyVisible: boolean;
|
||||
currentAgent?: AgentType;
|
||||
chatVisible?: boolean;
|
||||
isDeveloper?: boolean;
|
||||
integrateSystem?: string;
|
||||
onMsgDataLoaded: (
|
||||
data: MsgDataType,
|
||||
questionId: string | number,
|
||||
question: string,
|
||||
valid: boolean
|
||||
) => void;
|
||||
onSendMsg: (value: string) => void;
|
||||
};
|
||||
|
||||
const MessageContainer: React.FC<Props> = ({
|
||||
id,
|
||||
chatId,
|
||||
messageList,
|
||||
historyVisible,
|
||||
currentAgent,
|
||||
chatVisible,
|
||||
isDeveloper,
|
||||
integrateSystem,
|
||||
onMsgDataLoaded,
|
||||
onSendMsg,
|
||||
}) => {
|
||||
const [triggerResize, setTriggerResize] = useState(false);
|
||||
|
||||
const onResize = useCallback(() => {
|
||||
setTriggerResize(true);
|
||||
setTimeout(() => {
|
||||
setTriggerResize(false);
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onResize();
|
||||
}, [historyVisible, chatVisible]);
|
||||
|
||||
const messageContainerClass = classNames(styles.messageContainer, { [styles.mobile]: isMobile });
|
||||
|
||||
return (
|
||||
<div id={id} className={messageContainerClass}>
|
||||
<div className={styles.messageList}>
|
||||
{messageList.map((msgItem: MessageItem, index: number) => {
|
||||
const {
|
||||
id: msgId,
|
||||
modelId,
|
||||
agentId,
|
||||
type,
|
||||
msg,
|
||||
msgValue,
|
||||
identityMsg,
|
||||
msgData,
|
||||
score,
|
||||
parseOptions,
|
||||
filters,
|
||||
} = msgItem;
|
||||
|
||||
return (
|
||||
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
|
||||
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
|
||||
{type === MessageTypeEnum.AGENT_LIST && (
|
||||
<AgentTip currentAgent={currentAgent} onSendMsg={onSendMsg} />
|
||||
)}
|
||||
{type === MessageTypeEnum.QUESTION && (
|
||||
<>
|
||||
<Text position="right" data={msg} />
|
||||
{identityMsg && <Text position="left" data={identityMsg} />}
|
||||
<ChatItem
|
||||
msg={msgValue || msg || ''}
|
||||
msgData={msgData}
|
||||
conversationId={chatId}
|
||||
modelId={modelId}
|
||||
agentId={agentId}
|
||||
filter={filters}
|
||||
isLastMessage={index === messageList.length - 1}
|
||||
triggerResize={triggerResize}
|
||||
isDeveloper={isDeveloper}
|
||||
integrateSystem={integrateSystem}
|
||||
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
|
||||
onMsgDataLoaded(data, msgId, msgValue || msg || '', valid);
|
||||
}}
|
||||
onUpdateMessageScroll={updateMessageContainerScroll}
|
||||
onSendMsg={onSendMsg}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === MessageTypeEnum.PARSE_OPTIONS && (
|
||||
<ChatItem
|
||||
msg={msgValue || msg || ''}
|
||||
conversationId={chatId}
|
||||
modelId={modelId}
|
||||
agentId={agentId}
|
||||
filter={filters}
|
||||
isLastMessage={index === messageList.length - 1}
|
||||
triggerResize={triggerResize}
|
||||
parseOptions={parseOptions}
|
||||
integrateSystem={integrateSystem}
|
||||
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
|
||||
onMsgDataLoaded(data, msgId, msgValue || msg || '', valid);
|
||||
}}
|
||||
onUpdateMessageScroll={updateMessageContainerScroll}
|
||||
onSendMsg={onSendMsg}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function areEqual(prevProps: Props, nextProps: Props) {
|
||||
if (
|
||||
prevProps.id === nextProps.id &&
|
||||
isEqual(prevProps.messageList, nextProps.messageList) &&
|
||||
prevProps.historyVisible === nextProps.historyVisible &&
|
||||
prevProps.currentAgent === nextProps.currentAgent &&
|
||||
prevProps.chatVisible === nextProps.chatVisible
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default memo(MessageContainer, areEqual);
|
||||
@@ -0,0 +1,148 @@
|
||||
.messageContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
.messageList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 70px 20px 60px 14px;
|
||||
row-gap: 16px;
|
||||
|
||||
.messageItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
|
||||
:global {
|
||||
.ant-table-small {
|
||||
.ant-table-tbody {
|
||||
.ant-table-cell {
|
||||
padding: 6px 0 !important;
|
||||
}
|
||||
}
|
||||
.ss-chat-table-formatted-value {
|
||||
font-size: 15px !important;
|
||||
}
|
||||
}
|
||||
.ant-table-row {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.ss-chat-table-even-row {
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: 16px 0 1px;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-prev,
|
||||
.ant-pagination .ant-pagination-next {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
.ant-pagination-item-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-jump-prev,
|
||||
.ant-pagination-jump-next {
|
||||
.ant-pagination-item-link {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-options {
|
||||
display: inline-block;
|
||||
margin-left: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-item {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-item-active {
|
||||
font-weight: 600;
|
||||
background-color: #ffffff;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5715;
|
||||
list-style: none;
|
||||
font-feature-settings: 'tnum', 'tnum';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
.messageList {
|
||||
padding: 20px 10px 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
webapp/packages/chat-sdk/src/Chat/MobileAgents/index.tsx
Normal file
62
webapp/packages/chat-sdk/src/Chat/MobileAgents/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import IconFont from '../../components/IconFont';
|
||||
import { Drawer } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { AGENT_ICONS } from '../constants';
|
||||
import { AgentType } from '../type';
|
||||
import styles from './style.module.less';
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
agentList: AgentType[];
|
||||
currentAgent?: AgentType;
|
||||
onSelectAgent: (agent: AgentType) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const MobileAgents: React.FC<Props> = ({
|
||||
open,
|
||||
agentList,
|
||||
currentAgent,
|
||||
onSelectAgent,
|
||||
onClose,
|
||||
}) => {
|
||||
return (
|
||||
<Drawer
|
||||
title="智能助理"
|
||||
placement="bottom"
|
||||
open={open}
|
||||
height="85%"
|
||||
className={styles.mobileAgents}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.agentListContent}>
|
||||
{agentList.map((agent, index) => {
|
||||
const agentItemClass = classNames(styles.agentItem, {
|
||||
[styles.active]: currentAgent?.id === agent.id,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className={agentItemClass}
|
||||
onClick={() => {
|
||||
onSelectAgent(agent);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<div className={styles.agentTitleBar}>
|
||||
<IconFont
|
||||
type={AGENT_ICONS[index % AGENT_ICONS.length]}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
<div className={styles.agentName}>{agent.name}</div>
|
||||
</div>
|
||||
<div className={styles.agentDesc}>{agent.description}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileAgents;
|
||||
@@ -0,0 +1,55 @@
|
||||
.mobileAgents {
|
||||
:global {
|
||||
.ant-drawer-content {
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
|
||||
.ant-drawer-header {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agentListContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
|
||||
.agentItem {
|
||||
padding: 12px 16px;
|
||||
background-color: #f5f7f9;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
|
||||
&.active {
|
||||
border: 1px solid var(--chat-blue);
|
||||
}
|
||||
|
||||
.agentTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
|
||||
.avatar {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.agentName {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.agentDesc {
|
||||
margin-top: 8px;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import LeftAvatar from '../CopilotAvatar';
|
||||
import Message from '../Message';
|
||||
import styles from './style.module.less';
|
||||
import { AgentType } from '../../type';
|
||||
import { isMobile } from '../../../utils/utils';
|
||||
|
||||
type Props = {
|
||||
currentAgent?: AgentType;
|
||||
onSendMsg: (value: string) => void;
|
||||
};
|
||||
|
||||
const AgentTip: React.FC<Props> = ({ currentAgent, onSendMsg }) => {
|
||||
if (!currentAgent) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.agentTip}>
|
||||
{!isMobile && <LeftAvatar />}
|
||||
<Message position="left" bubbleClassName={styles.agentTipMsg}>
|
||||
<div className={styles.title}>
|
||||
您好,智能助理【{currentAgent.name}
|
||||
】将与您对话,试着问:
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.examples}>
|
||||
{currentAgent.examples?.length > 0 ? (
|
||||
currentAgent.examples.map(example => (
|
||||
<div
|
||||
key={example}
|
||||
className={styles.example}
|
||||
onClick={() => {
|
||||
onSendMsg(example);
|
||||
}}
|
||||
>
|
||||
“{example}”
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.example}>{currentAgent.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Message>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentTip;
|
||||
@@ -0,0 +1,44 @@
|
||||
.agentTip {
|
||||
display: flex;
|
||||
|
||||
.agentTipMsg {
|
||||
padding: 12px 20px 20px !important;
|
||||
|
||||
.title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
column-gap: 14px;
|
||||
|
||||
.topBar {
|
||||
.tip {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.examples {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 13px;
|
||||
row-gap: 8px;
|
||||
|
||||
.example {
|
||||
color: var(--chat-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
flex: none;
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { updateConversationName } from '../../service';
|
||||
import type { ConversationDetailType } from '../../type';
|
||||
import { CHAT_TITLE } from '../../constants';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
editConversation?: ConversationDetailType;
|
||||
onClose: () => void;
|
||||
onFinish: (conversationName: string) => void;
|
||||
};
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 18 },
|
||||
};
|
||||
|
||||
const ConversationModal: React.FC<Props> = ({ visible, editConversation, onClose, onFinish }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const conversationNameInputRef = useRef<any>();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.setFieldsValue({ conversationName: editConversation!.chatName });
|
||||
setTimeout(() => {
|
||||
conversationNameInputRef.current.focus({
|
||||
cursor: 'all',
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const onConfirm = async () => {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
await updateConversationName(values.conversationName, editConversation!.chatId);
|
||||
setLoading(false);
|
||||
onFinish(values.conversationName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`修改${CHAT_TITLE}问答名称`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
onOk={onConfirm}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form {...layout} form={form}>
|
||||
<FormItem name="conversationName" label="名称" rules={[{ required: true }]}>
|
||||
<Input
|
||||
placeholder={`请输入${CHAT_TITLE}问答名称`}
|
||||
ref={conversationNameInputRef}
|
||||
onPressEnter={onConfirm}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationModal;
|
||||
@@ -0,0 +1,8 @@
|
||||
import IconFont from '../../../components/IconFont';
|
||||
import styles from './style.module.less';
|
||||
|
||||
const CopilotAvatar = () => {
|
||||
return <IconFont type="icon-zhinengsuanfa" className={styles.leftAvatar} />;
|
||||
};
|
||||
|
||||
export default CopilotAvatar;
|
||||
@@ -0,0 +1,13 @@
|
||||
.leftAvatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 6px;
|
||||
margin-right: 6px;
|
||||
color: var(--chat-blue);
|
||||
font-size: 40px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
38
webapp/packages/chat-sdk/src/Chat/components/Message.tsx
Normal file
38
webapp/packages/chat-sdk/src/Chat/components/Message.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import classNames from 'classnames';
|
||||
import styles from './style.module.less';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type Props = {
|
||||
position: 'left' | 'right';
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
bubbleClassName?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const Message: React.FC<Props> = ({ position, width, height, children, bubbleClassName }) => {
|
||||
const messageClass = classNames(styles.message, {
|
||||
[styles.left]: position === 'left',
|
||||
[styles.right]: position === 'right',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={messageClass} style={{ width }}>
|
||||
<div className={styles.messageContent}>
|
||||
<div className={styles.messageBody}>
|
||||
<div
|
||||
className={`${styles.bubble}${bubbleClassName ? ` ${bubbleClassName}` : ''}`}
|
||||
style={{ height }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Message;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import LeftAvatar from '../CopilotAvatar';
|
||||
import Message from '../Message';
|
||||
import styles from './style.module.less';
|
||||
import { queryRecommendQuestions } from '../../service';
|
||||
import { isMobile } from '../../../utils/utils';
|
||||
|
||||
type Props = {
|
||||
onSelectQuestion: (value: string) => void;
|
||||
};
|
||||
|
||||
const RecommendQuestions: React.FC<Props> = ({ onSelectQuestion }) => {
|
||||
const [questions, setQuestions] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const initData = async () => {
|
||||
setLoading(true);
|
||||
const res = await queryRecommendQuestions();
|
||||
setLoading(false);
|
||||
setQuestions(
|
||||
res.data?.reduce((result: any[], item: any) => {
|
||||
result = [
|
||||
...result,
|
||||
...item.recommendedQuestions.slice(0, 20).map((item: any) => item.question),
|
||||
];
|
||||
return result;
|
||||
}, []) || []
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.recommendQuestions}>
|
||||
{!isMobile && <LeftAvatar />}
|
||||
{loading ? (
|
||||
<></>
|
||||
) : questions.length > 0 ? (
|
||||
<Message position="left" bubbleClassName={styles.recommendQuestionsMsg}>
|
||||
<div className={styles.title}>推荐问题:</div>
|
||||
<div className={styles.content}>
|
||||
{questions.map((question, index) => (
|
||||
<div
|
||||
key={`${question}_${index}`}
|
||||
className={styles.question}
|
||||
onClick={() => {
|
||||
onSelectQuestion(question);
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Message>
|
||||
) : (
|
||||
<Message position="left">您好,请问有什么我可以帮您吗?</Message>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecommendQuestions;
|
||||
@@ -0,0 +1,36 @@
|
||||
.recommendQuestions {
|
||||
display: flex;
|
||||
|
||||
.recommendQuestionsMsg {
|
||||
padding: 12px 20px 20px !important;
|
||||
|
||||
.title {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
row-gap: 20px;
|
||||
|
||||
.question {
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
color: var(--text-color);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
background-color: #f4f4f4;
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
webapp/packages/chat-sdk/src/Chat/components/Text.tsx
Normal file
29
webapp/packages/chat-sdk/src/Chat/components/Text.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { isMobile } from '../../utils/utils';
|
||||
import { Avatar } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import LeftAvatar from './CopilotAvatar';
|
||||
import Message from './Message';
|
||||
import styles from './style.module.less';
|
||||
|
||||
type Props = {
|
||||
position: 'left' | 'right';
|
||||
data: any;
|
||||
quote?: string;
|
||||
};
|
||||
|
||||
const Text: React.FC<Props> = ({ position, data, quote }) => {
|
||||
const textWrapperClass = classNames(styles.textWrapper, {
|
||||
[styles.rightTextWrapper]: position === 'right',
|
||||
});
|
||||
return (
|
||||
<div className={textWrapperClass}>
|
||||
{!isMobile && position === 'left' && <LeftAvatar />}
|
||||
<Message position={position} bubbleClassName={styles.textBubble}>
|
||||
{position === 'right' && quote && <div className={styles.quote}>{quote}</div>}
|
||||
<div className={styles.text}>{data}</div>
|
||||
</Message>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Text;
|
||||
311
webapp/packages/chat-sdk/src/Chat/components/style.module.less
Normal file
311
webapp/packages/chat-sdk/src/Chat/components/style.module.less
Normal file
@@ -0,0 +1,311 @@
|
||||
.message {
|
||||
.messageTitleBar {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 6px;
|
||||
column-gap: 10px;
|
||||
|
||||
.modelName {
|
||||
margin-left: 4px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.messageTopBar {
|
||||
position: relative;
|
||||
max-width: 80%;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.messageContent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.messageBody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
box-sizing: border-box;
|
||||
min-width: 1px;
|
||||
max-width: 100%;
|
||||
padding: 8px 16px 10px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.text {
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.textMsg {
|
||||
padding: 12px 0 5px;
|
||||
}
|
||||
|
||||
.topBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 4px 0 8px;
|
||||
overflow-x: auto;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
||||
|
||||
.messageTitleWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.messageTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
.messageContent {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.bubble {
|
||||
float: right;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 16px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
background: linear-gradient(81.62deg, #2870ea 8.72%, var(--chat-blue) 85.01%);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.text {
|
||||
&::selection {
|
||||
background: #1ba1f7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textBubble {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.listenerSex {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.listenerArea {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.typing {
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
|
||||
:global {
|
||||
.ant-spin-dot {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageEntityName {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.messageAvatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dataHolder {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
margin-left: 20px;
|
||||
color: var(--text-color-third);
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
|
||||
.subTitleValue {
|
||||
margin-left: 6px;
|
||||
color: var(--text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatarPopover {
|
||||
:global {
|
||||
.ant-popover-inner-content {
|
||||
padding: 3px 4px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moreOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
|
||||
.selectOthers {
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
column-gap: 12px;
|
||||
|
||||
.indicator {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contentName {
|
||||
max-width: 350px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.aggregatorIndicator {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.entityId {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
column-gap: 4px;
|
||||
|
||||
.idTitle {
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
}
|
||||
.idValue {
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.typingBubble {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.quote {
|
||||
margin-bottom: 4px;
|
||||
padding: 0 4px 0 6px;
|
||||
color: var(--border-color-base);
|
||||
font-size: 13px;
|
||||
border-left: 4px solid var(--border-color-base);
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
|
||||
.filterSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
.filterItem {
|
||||
padding: 2px 12px;
|
||||
color: var(--text-color-secondary);
|
||||
background-color: #edf2f2;
|
||||
border-radius: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.noPermissionTip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-left: 6px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
.infoBar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
.mainEntityInfo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
column-gap: 20px;
|
||||
|
||||
.infoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.infoName {
|
||||
color: var(--text-color-fourth);
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.rightTextWrapper {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.rightAvatar {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
58
webapp/packages/chat-sdk/src/Chat/constants.ts
Normal file
58
webapp/packages/chat-sdk/src/Chat/constants.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export const THEME_COLOR_LIST = [
|
||||
'#3369FF',
|
||||
'#36D2B8',
|
||||
'#DB8D76',
|
||||
'#47B359',
|
||||
'#8545E6',
|
||||
'#E0B18B',
|
||||
'#7258F3',
|
||||
'#0095FF',
|
||||
'#52CC8F',
|
||||
'#6675FF',
|
||||
'#CC516E',
|
||||
'#5CA9E6',
|
||||
];
|
||||
|
||||
export enum SemanticTypeEnum {
|
||||
MODEL = 'MODEL',
|
||||
DIMENSION = 'DIMENSION',
|
||||
METRIC = 'METRIC',
|
||||
VALUE = 'VALUE',
|
||||
}
|
||||
|
||||
export const SEMANTIC_TYPE_MAP = {
|
||||
[SemanticTypeEnum.MODEL]: '数据来源',
|
||||
[SemanticTypeEnum.DIMENSION]: '维度',
|
||||
[SemanticTypeEnum.METRIC]: '指标',
|
||||
[SemanticTypeEnum.VALUE]: '维度值',
|
||||
};
|
||||
|
||||
export const AGENT_ICONS = [
|
||||
'icon-fukuanbaobiaochaxun',
|
||||
'icon-hangweifenxi1',
|
||||
'icon-xiaofeifenxi',
|
||||
'icon-renwuchaxun',
|
||||
'icon-baobiao',
|
||||
'icon-liushuichaxun',
|
||||
'icon-cangkuchaxun',
|
||||
'icon-xiaoshoushuju',
|
||||
'icon-tongji',
|
||||
'icon-shujutongji',
|
||||
'icon-mendiankanban',
|
||||
];
|
||||
|
||||
export const HOLDER_TAG = '@_supersonic_@';
|
||||
|
||||
export const CHAT_TITLE = '';
|
||||
|
||||
export const DEFAULT_CONVERSATION_NAME = '新问答对话';
|
||||
|
||||
export const PAGE_TITLE = '问答对话';
|
||||
|
||||
export const WEB_TITLE = '问答对话';
|
||||
|
||||
export const MOBILE_TITLE = '问答对话';
|
||||
|
||||
export const PLACE_HOLDER = '请输入您的问题,或输入“/”切换助理';
|
||||
|
||||
export const SIMPLE_PLACE_HOLDER = '请输入您的问题';
|
||||
446
webapp/packages/chat-sdk/src/Chat/index.tsx
Normal file
446
webapp/packages/chat-sdk/src/Chat/index.tsx
Normal file
@@ -0,0 +1,446 @@
|
||||
import { updateMessageContainerScroll, isMobile, uuid, setToken } from '../utils/utils';
|
||||
import {
|
||||
ForwardRefRenderFunction,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import styles from './style.module.less';
|
||||
import { ConversationDetailType, MessageItem, MessageTypeEnum, AgentType } from './type';
|
||||
import { queryAgentList } from './service';
|
||||
import { useThrottleFn } from 'ahooks';
|
||||
import Conversation from './Conversation';
|
||||
import ChatFooter from './ChatFooter';
|
||||
import classNames from 'classnames';
|
||||
import { CHAT_TITLE } from './constants';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import AgentList from './AgentList';
|
||||
import MobileAgents from './MobileAgents';
|
||||
import { HistoryMsgItemType, MsgDataType, SendMsgParamsType } from '../common/type';
|
||||
import { getHistoryMsg } from '../service';
|
||||
|
||||
type Props = {
|
||||
token?: string;
|
||||
agentIds?: number[];
|
||||
initialAgentId?: number;
|
||||
chatVisible?: boolean;
|
||||
noInput?: boolean;
|
||||
isDeveloper?: boolean;
|
||||
integrateSystem?: string;
|
||||
isCopilot?: boolean;
|
||||
apiUrl?: string;
|
||||
onCurrentAgentChange?: (agent?: AgentType) => void;
|
||||
onReportMsgEvent?: (msg: string, valid: boolean) => void;
|
||||
};
|
||||
|
||||
const Chat: ForwardRefRenderFunction<any, Props> = (
|
||||
{
|
||||
token,
|
||||
agentIds,
|
||||
initialAgentId,
|
||||
chatVisible,
|
||||
noInput,
|
||||
isDeveloper,
|
||||
integrateSystem,
|
||||
isCopilot,
|
||||
apiUrl,
|
||||
onCurrentAgentChange,
|
||||
onReportMsgEvent,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [messageList, setMessageList] = useState<MessageItem[]>([]);
|
||||
const [inputMsg, setInputMsg] = useState('');
|
||||
const [pageNo, setPageNo] = useState(1);
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
const [historyInited, setHistoryInited] = useState(false);
|
||||
const [currentConversation, setCurrentConversation] = useState<
|
||||
ConversationDetailType | undefined
|
||||
>(isMobile ? { chatId: 0, chatName: `${CHAT_TITLE}问答` } : undefined);
|
||||
const [historyVisible, setHistoryVisible] = useState(false);
|
||||
const [agentList, setAgentList] = useState<AgentType[]>([]);
|
||||
const [currentAgent, setCurrentAgent] = useState<AgentType>();
|
||||
const [mobileAgentsVisible, setMobileAgentsVisible] = useState(false);
|
||||
const [agentListVisible, setAgentListVisible] = useState(true);
|
||||
|
||||
const conversationRef = useRef<any>();
|
||||
const chatFooterRef = useRef<any>();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
sendCopilotMsg,
|
||||
}));
|
||||
|
||||
const sendCopilotMsg = (params: SendMsgParamsType) => {
|
||||
setAgentListVisible(false);
|
||||
const { agentId, msg, modelId } = params;
|
||||
if (currentAgent?.id !== agentId) {
|
||||
setMessageList([]);
|
||||
const agent = agentList.find(item => item.id === agentId) || ({} as AgentType);
|
||||
updateCurrentAgent({ ...agent, initialSendMsgParams: params });
|
||||
} else {
|
||||
onSendMsg(msg, messageList, modelId, params);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCurrentAgent = (agent?: AgentType) => {
|
||||
setCurrentAgent(agent);
|
||||
onCurrentAgentChange?.(agent);
|
||||
localStorage.setItem('AGENT_ID', `${agent?.id}`);
|
||||
if (!isCopilot) {
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?agentId=${agent?.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
const initAgentList = async () => {
|
||||
const res = await queryAgentList();
|
||||
const agentListValue = (res.data || []).filter(
|
||||
item => item.status === 1 && (agentIds === undefined || agentIds.includes(item.id))
|
||||
);
|
||||
setAgentList(agentListValue);
|
||||
if (agentListValue.length > 0) {
|
||||
const agentId = initialAgentId || localStorage.getItem('AGENT_ID');
|
||||
if (agentId) {
|
||||
const agent = agentListValue.find(item => item.id === +agentId);
|
||||
updateCurrentAgent(agent || agentListValue[0]);
|
||||
} else {
|
||||
updateCurrentAgent(agentListValue[0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initAgentList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
setToken(token);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (apiUrl) {
|
||||
localStorage.setItem('SUPERSONIC_CHAT_API_URL', apiUrl);
|
||||
}
|
||||
}, [apiUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatVisible) {
|
||||
inputFocus();
|
||||
updateMessageContainerScroll();
|
||||
}
|
||||
}, [chatVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentConversation) {
|
||||
return;
|
||||
}
|
||||
const { initialMsgParams, isAdd } = currentConversation;
|
||||
if (isAdd) {
|
||||
inputFocus();
|
||||
if (initialMsgParams) {
|
||||
onSendMsg(initialMsgParams.msg, [], initialMsgParams.modelId, initialMsgParams);
|
||||
return;
|
||||
}
|
||||
sendHelloRsp();
|
||||
return;
|
||||
}
|
||||
updateHistoryMsg(1);
|
||||
setPageNo(1);
|
||||
}, [currentConversation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (historyInited) {
|
||||
const messageContainerEle = document.getElementById('messageContainer');
|
||||
messageContainerEle?.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
return () => {
|
||||
const messageContainerEle = document.getElementById('messageContainer');
|
||||
messageContainerEle?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [historyInited]);
|
||||
|
||||
const sendHelloRsp = (agent?: AgentType) => {
|
||||
if (noInput) {
|
||||
return;
|
||||
}
|
||||
setMessageList([
|
||||
{
|
||||
id: uuid(),
|
||||
type: MessageTypeEnum.AGENT_LIST,
|
||||
msg: agent?.name || currentAgent?.name || agentList?.[0].name,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const convertHistoryMsg = (list: HistoryMsgItemType[]) => {
|
||||
return list.map((item: HistoryMsgItemType) => ({
|
||||
id: item.questionId,
|
||||
type: MessageTypeEnum.QUESTION,
|
||||
msg: item.queryText,
|
||||
msgData: item.queryResult,
|
||||
score: item.score,
|
||||
}));
|
||||
};
|
||||
|
||||
const updateHistoryMsg = async (page: number) => {
|
||||
const res = await getHistoryMsg(page, currentConversation!.chatId, 3);
|
||||
const { hasNextPage, list } = res?.data || { hasNextPage: false, list: [] };
|
||||
const msgList = [...convertHistoryMsg(list), ...(page === 1 ? [] : messageList)];
|
||||
setMessageList(msgList);
|
||||
setHasNextPage(hasNextPage);
|
||||
if (page === 1) {
|
||||
if (list.length === 0) {
|
||||
sendHelloRsp();
|
||||
}
|
||||
updateMessageContainerScroll();
|
||||
setHistoryInited(true);
|
||||
inputFocus();
|
||||
} else {
|
||||
const msgEle = document.getElementById(`${messageList[0]?.id}`);
|
||||
msgEle?.scrollIntoView();
|
||||
}
|
||||
};
|
||||
|
||||
const { run: handleScroll } = useThrottleFn(
|
||||
e => {
|
||||
if (e.target.scrollTop === 0 && hasNextPage) {
|
||||
updateHistoryMsg(pageNo + 1);
|
||||
setPageNo(pageNo + 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
wait: 200,
|
||||
}
|
||||
);
|
||||
|
||||
const inputFocus = () => {
|
||||
if (!isMobile) {
|
||||
chatFooterRef.current?.inputFocus();
|
||||
}
|
||||
};
|
||||
|
||||
const inputBlur = () => {
|
||||
chatFooterRef.current?.inputBlur();
|
||||
};
|
||||
|
||||
const onSendMsg = async (
|
||||
msg?: string,
|
||||
list?: MessageItem[],
|
||||
modelId?: number,
|
||||
sendMsgParams?: SendMsgParamsType
|
||||
) => {
|
||||
const currentMsg = msg || inputMsg;
|
||||
if (currentMsg.trim() === '') {
|
||||
setInputMsg('');
|
||||
return;
|
||||
}
|
||||
|
||||
const msgAgent = agentList.find(item => currentMsg.indexOf(item.name) === 1);
|
||||
const certainAgent = currentMsg[0] === '/' && msgAgent;
|
||||
const agentIdValue = certainAgent ? msgAgent.id : undefined;
|
||||
const agent = agentList.find(item => item.id === sendMsgParams?.agentId);
|
||||
|
||||
if (agent || certainAgent) {
|
||||
updateCurrentAgent(agent || msgAgent);
|
||||
}
|
||||
const msgs = [
|
||||
...(list || messageList),
|
||||
{
|
||||
id: uuid(),
|
||||
msg: currentMsg,
|
||||
msgValue: certainAgent
|
||||
? currentMsg.replace(`/${certainAgent.name}`, '').trim()
|
||||
: currentMsg,
|
||||
modelId: modelId === -1 ? undefined : modelId,
|
||||
agentId: agent?.id || agentIdValue || currentAgent?.id,
|
||||
type: MessageTypeEnum.QUESTION,
|
||||
filters: sendMsgParams?.filters,
|
||||
},
|
||||
];
|
||||
setMessageList(msgs);
|
||||
updateMessageContainerScroll();
|
||||
setInputMsg('');
|
||||
};
|
||||
|
||||
const onInputMsgChange = (value: string) => {
|
||||
const inputMsgValue = value || '';
|
||||
setInputMsg(inputMsgValue);
|
||||
};
|
||||
|
||||
const saveConversationToLocal = (conversation: ConversationDetailType) => {
|
||||
if (conversation) {
|
||||
if (conversation.chatId !== -1) {
|
||||
localStorage.setItem('CONVERSATION_ID', `${conversation.chatId}`);
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('CONVERSATION_ID');
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectConversation = (
|
||||
conversation: ConversationDetailType,
|
||||
sendMsgParams?: SendMsgParamsType,
|
||||
isAdd?: boolean
|
||||
) => {
|
||||
setCurrentConversation({
|
||||
...conversation,
|
||||
initialMsgParams: sendMsgParams,
|
||||
isAdd,
|
||||
});
|
||||
saveConversationToLocal(conversation);
|
||||
};
|
||||
|
||||
const onMsgDataLoaded = (
|
||||
data: MsgDataType,
|
||||
questionId: string | number,
|
||||
question: string,
|
||||
valid: boolean
|
||||
) => {
|
||||
onReportMsgEvent?.(question, valid);
|
||||
if (!isMobile) {
|
||||
conversationRef?.current?.updateData(currentAgent?.id);
|
||||
}
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
let parseOptionsItem: any;
|
||||
if (data.parseOptions && data.parseOptions.length > 0) {
|
||||
parseOptionsItem = {
|
||||
id: uuid(),
|
||||
msg: messageList[messageList.length - 1]?.msg,
|
||||
type: MessageTypeEnum.PARSE_OPTIONS,
|
||||
parseOptions: data.parseOptions,
|
||||
};
|
||||
}
|
||||
const msgs = cloneDeep(messageList);
|
||||
const msg = msgs.find(item => item.id === questionId);
|
||||
if (msg) {
|
||||
msg.msgData = data;
|
||||
const msgList = [...msgs, ...(parseOptionsItem ? [parseOptionsItem] : [])];
|
||||
setMessageList(msgList);
|
||||
}
|
||||
updateMessageContainerScroll(`${questionId}`);
|
||||
};
|
||||
|
||||
const onToggleHistoryVisible = () => {
|
||||
setHistoryVisible(!historyVisible);
|
||||
};
|
||||
|
||||
const onAddConversation = () => {
|
||||
conversationRef.current?.onAddConversation();
|
||||
inputFocus();
|
||||
};
|
||||
|
||||
const onSelectAgent = (agent: AgentType) => {
|
||||
if (agent.id === currentAgent?.id) {
|
||||
return;
|
||||
}
|
||||
if (messageList.length === 1 && messageList[0].type === MessageTypeEnum.AGENT_LIST) {
|
||||
setMessageList([]);
|
||||
}
|
||||
updateCurrentAgent(agent);
|
||||
updateMessageContainerScroll();
|
||||
};
|
||||
|
||||
const sendMsg = (msg: string, modelId?: number) => {
|
||||
onSendMsg(msg, messageList, modelId);
|
||||
if (isMobile) {
|
||||
inputBlur();
|
||||
}
|
||||
};
|
||||
|
||||
const onCloseConversation = () => {
|
||||
setHistoryVisible(false);
|
||||
};
|
||||
|
||||
const chatClass = classNames(styles.chat, {
|
||||
[styles.mobile]: isMobile,
|
||||
[styles.historyVisible]: historyVisible,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={chatClass}>
|
||||
<div className={styles.chatSection}>
|
||||
{!isMobile && agentList.length > 1 && agentListVisible && (
|
||||
<AgentList
|
||||
agentList={agentList}
|
||||
currentAgent={currentAgent}
|
||||
onSelectAgent={onSelectAgent}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.chatApp}>
|
||||
{currentConversation && (
|
||||
<div className={styles.chatBody}>
|
||||
<div className={styles.chatContent}>
|
||||
{currentAgent && !isMobile && !noInput && (
|
||||
<div className={styles.chatHeader}>
|
||||
<div className={styles.chatHeaderTitle}>{currentAgent.name}</div>
|
||||
<div className={styles.chatHeaderTip}>{currentAgent.description}</div>
|
||||
</div>
|
||||
)}
|
||||
<MessageContainer
|
||||
id="messageContainer"
|
||||
messageList={messageList}
|
||||
chatId={currentConversation?.chatId}
|
||||
historyVisible={historyVisible}
|
||||
currentAgent={currentAgent}
|
||||
chatVisible={chatVisible}
|
||||
isDeveloper={isDeveloper}
|
||||
integrateSystem={integrateSystem}
|
||||
onMsgDataLoaded={onMsgDataLoaded}
|
||||
onSendMsg={onSendMsg}
|
||||
/>
|
||||
{!noInput && (
|
||||
<ChatFooter
|
||||
inputMsg={inputMsg}
|
||||
chatId={currentConversation?.chatId}
|
||||
agentList={agentList}
|
||||
currentAgent={currentAgent}
|
||||
onToggleHistoryVisible={onToggleHistoryVisible}
|
||||
onInputMsgChange={onInputMsgChange}
|
||||
onSendMsg={sendMsg}
|
||||
onAddConversation={onAddConversation}
|
||||
onSelectAgent={onSelectAgent}
|
||||
onOpenAgents={() => {
|
||||
if (isMobile) {
|
||||
setMobileAgentsVisible(true);
|
||||
} else {
|
||||
setAgentListVisible(!agentListVisible);
|
||||
}
|
||||
}}
|
||||
ref={chatFooterRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Conversation
|
||||
currentAgent={currentAgent}
|
||||
currentConversation={currentConversation}
|
||||
historyVisible={historyVisible}
|
||||
onSelectConversation={onSelectConversation}
|
||||
onCloseConversation={onCloseConversation}
|
||||
ref={conversationRef}
|
||||
/>
|
||||
</div>
|
||||
<MobileAgents
|
||||
open={mobileAgentsVisible}
|
||||
agentList={agentList}
|
||||
currentAgent={currentAgent}
|
||||
onSelectAgent={onSelectAgent}
|
||||
onClose={() => {
|
||||
setMobileAgentsVisible(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Chat);
|
||||
51
webapp/packages/chat-sdk/src/Chat/service.ts
Normal file
51
webapp/packages/chat-sdk/src/Chat/service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import axios from '../service/axiosInstance';
|
||||
import { isMobile } from '../utils/utils';
|
||||
import { AgentType, ModelType } from './type';
|
||||
|
||||
const prefix = isMobile ? '/openapi' : '/api';
|
||||
|
||||
export function saveConversation(chatName: string, agentId: number) {
|
||||
return axios.post<any>(
|
||||
`${prefix}/chat/manage/save?chatName=${chatName}&agentId=${agentId}`
|
||||
);
|
||||
}
|
||||
|
||||
export function updateConversationName(chatName: string, chatId: number = 0) {
|
||||
return axios.post<any>(
|
||||
`${prefix}/chat/manage/updateChatName?chatName=${chatName}&chatId=${chatId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteConversation(chatId: number) {
|
||||
return axios.post<any>(`${prefix}/chat/manage/delete?chatId=${chatId}`);
|
||||
}
|
||||
|
||||
export function getAllConversations(agentId?: number) {
|
||||
return axios.get<any>(`${prefix}/chat/manage/getAll`, { params: { agentId } });
|
||||
}
|
||||
|
||||
export function getModelList() {
|
||||
return axios.get<ModelType[]>(`${prefix}/chat/conf/modelList/view`);
|
||||
}
|
||||
|
||||
export function updateQAFeedback(questionId: number, score: number) {
|
||||
return axios.post<any>(
|
||||
`${prefix}/chat/manage/updateQAFeedback?id=${questionId}&score=${score}&feedback=`,
|
||||
);
|
||||
}
|
||||
|
||||
export function queryMetricSuggestion(modelId: number) {
|
||||
return axios.get<any>(`${prefix}/chat/recommend/metric/${modelId}`);
|
||||
}
|
||||
|
||||
export function querySuggestion(modelId: number) {
|
||||
return axios.get<any>(`${prefix}/chat/recommend/${modelId}`);
|
||||
}
|
||||
|
||||
export function queryRecommendQuestions() {
|
||||
return axios.get<any>(`${prefix}/chat/recommend/question`);
|
||||
}
|
||||
|
||||
export function queryAgentList() {
|
||||
return axios.get<AgentType[]>(`${prefix}/chat/agent/getAgentList`);
|
||||
}
|
||||
91
webapp/packages/chat-sdk/src/Chat/style.module.less
Normal file
91
webapp/packages/chat-sdk/src/Chat/style.module.less
Normal file
@@ -0,0 +1,91 @@
|
||||
.chat {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, rgba(23, 74, 228, 0) 29.44%, rgba(23, 74, 228, 0.06) 100%),
|
||||
linear-gradient(90deg, #f3f3f7 0%, #f3f3f7 20%, #ebf0f9 60%, #f3f3f7 80%, #f3f3f7 100%);
|
||||
|
||||
.chatSection {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.chatApp {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
|
||||
.chatBody {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
.chatContent {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.chatHeader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
background: rgba(243, 243, 247, 0.85);
|
||||
backdrop-filter: blur(2px);
|
||||
|
||||
.chatHeaderTitle {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.chatHeaderTip {
|
||||
width: 600px;
|
||||
margin-left: 16px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.historyVisible {
|
||||
.chatSection {
|
||||
.chatApp {
|
||||
width: calc(100% - 707px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
.chatSection {
|
||||
.chatApp {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.ss-chat-recommend-options {
|
||||
.ant-table-thead .ant-table-cell {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
.ant-table-tbody .ant-table-cell {
|
||||
padding: 8px !important;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
webapp/packages/chat-sdk/src/Chat/type.ts
Normal file
87
webapp/packages/chat-sdk/src/Chat/type.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { ChatContextType, MsgDataType, SendMsgParamsType } from "../common/type";
|
||||
|
||||
export enum MessageTypeEnum {
|
||||
TEXT = 'text', // 指标文本
|
||||
QUESTION = 'question',
|
||||
TAG = 'tag', // 标签
|
||||
SUGGESTION = 'suggestion', // 建议
|
||||
NO_PERMISSION = 'no_permission', // 无权限
|
||||
SEMANTIC_DETAIL = 'semantic_detail', // 语义指标/维度等信息详情
|
||||
PLUGIN = 'PLUGIN', // 插件
|
||||
WEB_PAGE = 'WEB_PAGE', // 插件
|
||||
RECOMMEND_QUESTIONS = 'recommend_questions', // 推荐问题
|
||||
PARSE_OPTIONS = 'parse_options', // 解析选项
|
||||
AGENT_LIST = 'agent_list', // 专家列表
|
||||
}
|
||||
|
||||
export type MessageItem = {
|
||||
id: string | number;
|
||||
type?: MessageTypeEnum;
|
||||
msg?: string;
|
||||
msgValue?: string;
|
||||
identityMsg?: string;
|
||||
modelId?: number;
|
||||
agentId?: number;
|
||||
entityId?: string;
|
||||
msgData?: MsgDataType;
|
||||
quote?: string;
|
||||
score?: number;
|
||||
feedback?: string;
|
||||
parseOptions?: ChatContextType[];
|
||||
filters?: any;
|
||||
};
|
||||
|
||||
export type ConversationDetailType = {
|
||||
chatId: number;
|
||||
chatName: string;
|
||||
createTime?: string;
|
||||
creator?: string;
|
||||
lastQuestion?: string;
|
||||
lastTime?: string;
|
||||
initialMsgParams?: SendMsgParamsType;
|
||||
isAdd?: boolean;
|
||||
};
|
||||
|
||||
export enum MessageModeEnum {
|
||||
INTERPRET = 'interpret',
|
||||
}
|
||||
|
||||
export type ModelType = {
|
||||
id: number;
|
||||
name: string;
|
||||
bizName: string;
|
||||
};
|
||||
|
||||
export enum PluginShowTypeEnum {
|
||||
DASHBOARD = 'DASHBOARD',
|
||||
WIDGET = 'WIDGET',
|
||||
URL = 'URL',
|
||||
TAG = 'TAG',
|
||||
}
|
||||
|
||||
export type PluginType = {
|
||||
id: number;
|
||||
name: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type SuggestionItemType = {
|
||||
id: number;
|
||||
model: number;
|
||||
name: string;
|
||||
bizName: string;
|
||||
};
|
||||
|
||||
export type SuggestionType = {
|
||||
dimensions: SuggestionItemType[];
|
||||
metrics: SuggestionItemType[];
|
||||
};
|
||||
|
||||
export type AgentType = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
examples: string[];
|
||||
status: 0 | 1;
|
||||
initialSendMsgParams?: SendMsgParamsType;
|
||||
};
|
||||
11
webapp/packages/chat-sdk/src/Copilot/constants.ts
Normal file
11
webapp/packages/chat-sdk/src/Copilot/constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const MODEL_PATH_MAP = {
|
||||
song: '歌曲库',
|
||||
'song-detail': '歌曲库',
|
||||
singer: '艺人库',
|
||||
'singer-detail': '艺人库',
|
||||
album: '专辑库',
|
||||
'album-detail': '专辑库',
|
||||
'digital-album': '专辑库',
|
||||
brand: '厂牌库',
|
||||
'brand-detail': '厂牌库',
|
||||
};
|
||||
148
webapp/packages/chat-sdk/src/Copilot/index.tsx
Normal file
148
webapp/packages/chat-sdk/src/Copilot/index.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import IconFont from '../components/IconFont';
|
||||
import { CaretRightOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
ForwardRefRenderFunction,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Chat from '../Chat';
|
||||
import { AgentType } from '../Chat/type';
|
||||
import { setToken } from '../utils/utils';
|
||||
import { SendMsgParamsType } from '../common/type';
|
||||
import styles from './style.module.less';
|
||||
|
||||
type Props = {
|
||||
token?: string;
|
||||
agentIds?: number[];
|
||||
noInput?: boolean;
|
||||
isDeveloper?: boolean;
|
||||
integrateSystem?: string;
|
||||
apiUrl?: string;
|
||||
onReportMsgEvent?: (msg: string, valid: boolean) => void;
|
||||
onOpenChatPage?: (agentId?: number) => void;
|
||||
};
|
||||
|
||||
const Copilot: ForwardRefRenderFunction<any, Props> = (
|
||||
{
|
||||
token,
|
||||
agentIds,
|
||||
noInput,
|
||||
isDeveloper,
|
||||
integrateSystem,
|
||||
apiUrl,
|
||||
onReportMsgEvent,
|
||||
onOpenChatPage,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [chatVisible, setChatVisible] = useState(false);
|
||||
const [copilotMinimized, setCopilotMinimized] = useState(false);
|
||||
const [currentAgent, setCurrentAgent] = useState<AgentType>();
|
||||
|
||||
const chatRef = useRef<any>();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
sendCopilotMsg,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
setToken(token);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (apiUrl) {
|
||||
localStorage.setItem('SUPERSONIC_CHAT_API_URL', apiUrl);
|
||||
}
|
||||
}, [apiUrl]);
|
||||
|
||||
const sendCopilotMsg = (params: SendMsgParamsType) => {
|
||||
chatRef?.current?.sendCopilotMsg(params);
|
||||
updateChatVisible(true);
|
||||
};
|
||||
|
||||
const updateChatVisible = (visible: boolean) => {
|
||||
setChatVisible(visible);
|
||||
};
|
||||
|
||||
const onToggleChatVisible = () => {
|
||||
updateChatVisible(!chatVisible);
|
||||
};
|
||||
|
||||
const onCloseChat = () => {
|
||||
updateChatVisible(false);
|
||||
};
|
||||
|
||||
const onTransferChat = () => {
|
||||
onOpenChatPage?.(currentAgent?.id);
|
||||
};
|
||||
|
||||
const onMinimizeCopilot = (e: any) => {
|
||||
e.stopPropagation();
|
||||
updateChatVisible(false);
|
||||
setCopilotMinimized(true);
|
||||
};
|
||||
|
||||
const copilotClass = classNames(styles.copilot, {
|
||||
[styles.copilotMinimized]: copilotMinimized,
|
||||
});
|
||||
|
||||
const chatPopoverClass = classNames(styles.chatPopover, {
|
||||
[styles.c2System]: integrateSystem === 'c2',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={copilotClass}
|
||||
onMouseEnter={() => {
|
||||
setCopilotMinimized(false);
|
||||
}}
|
||||
onClick={onToggleChatVisible}
|
||||
>
|
||||
<IconFont type="icon-copilot-fill" />
|
||||
<div className={styles.minimizeWrapper} onClick={onMinimizeCopilot}>
|
||||
<div className={styles.minimize}>-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.copilotContent} style={{ display: chatVisible ? 'block' : 'none' }}>
|
||||
<div className={chatPopoverClass}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.leftSection}>
|
||||
<CloseOutlined className={styles.close} onClick={onCloseChat} />
|
||||
{onOpenChatPage && (
|
||||
<IconFont
|
||||
type="icon-weibiaoti-"
|
||||
className={styles.transfer}
|
||||
onClick={onTransferChat}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.title}>内容库问答</div>
|
||||
</div>
|
||||
<div className={styles.chat}>
|
||||
<Chat
|
||||
chatVisible={chatVisible}
|
||||
agentIds={agentIds}
|
||||
noInput={noInput}
|
||||
isDeveloper={isDeveloper}
|
||||
integrateSystem={integrateSystem}
|
||||
isCopilot
|
||||
onCurrentAgentChange={setCurrentAgent}
|
||||
onReportMsgEvent={onReportMsgEvent}
|
||||
ref={chatRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CaretRightOutlined className={styles.rightArrow} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Copilot);
|
||||
151
webapp/packages/chat-sdk/src/Copilot/style.module.less
Normal file
151
webapp/packages/chat-sdk/src/Copilot/style.module.less
Normal file
@@ -0,0 +1,151 @@
|
||||
.copilot {
|
||||
position: fixed;
|
||||
right: 8px;
|
||||
bottom: 220px;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
color: #fff;
|
||||
font-size: 26px;
|
||||
background-color: var(--chat-blue);
|
||||
background-clip: padding-box;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 8px 8px 20px 0 rgba(55, 99, 170, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&.copilotMinimized {
|
||||
right: -40px;
|
||||
}
|
||||
|
||||
.minimizeWrapper {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
right: -6px;
|
||||
display: none;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.minimize {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 5px;
|
||||
background-color: var(--text-color-fifth-4);
|
||||
border-radius: 50%;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.minimize {
|
||||
background-color: var(--text-color-fifth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
box-shadow: 8px 8px 20px rgba(55, 99, 170, 0.3);
|
||||
|
||||
.minimizeWrapper {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chatPopover {
|
||||
position: fixed;
|
||||
right: 90px;
|
||||
bottom: 5vh;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 70vw;
|
||||
min-width: 1100px;
|
||||
height: 90vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 4px 4px 10px rgba(55, 99, 170, 0.3), -2px -2px 16px rgba(55, 99, 170, 0.3);
|
||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out,
|
||||
-webkit-transform 0.3s ease-in-out;
|
||||
|
||||
&.c2System {
|
||||
width: calc(100vw - 180px);
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
background: linear-gradient(81.62deg, #2870ea 8.72%, var(--chat-blue) 85.01%);
|
||||
box-shadow: 1px 1px 8px #1b4aef5c;
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.leftSection {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
column-gap: 20px;
|
||||
|
||||
.close {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.transfer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat {
|
||||
height: calc(90vh - 40px);
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
bottom: 0;
|
||||
left: 60px;
|
||||
width: calc(100vw - 150px);
|
||||
height: 100vh;
|
||||
|
||||
.chat {
|
||||
height: calc(100vh - 50px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rightArrow {
|
||||
position: fixed;
|
||||
right: 69px;
|
||||
bottom: 232px;
|
||||
z-index: 999;
|
||||
color: var(--chat-blue);
|
||||
font-size: 30px;
|
||||
}
|
||||
@@ -47,8 +47,8 @@ export type FilterItemType = {
|
||||
elementID: number;
|
||||
name: string;
|
||||
bizName: string;
|
||||
operator: string;
|
||||
type: string;
|
||||
operator?: string;
|
||||
type?: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
@@ -69,6 +69,12 @@ export type EntityDimensionType = {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type SqlInfoType = {
|
||||
llmParseSql: string;
|
||||
logicSql: string;
|
||||
querySql: string;
|
||||
}
|
||||
|
||||
export type ChatContextType = {
|
||||
id: number;
|
||||
queryId: number;
|
||||
@@ -86,6 +92,7 @@ export type ChatContextType = {
|
||||
queryMode: string;
|
||||
dimensionFilters: FilterItemType[];
|
||||
properties: any;
|
||||
sqlInfo: SqlInfoType;
|
||||
};
|
||||
|
||||
export enum MsgValidTypeEnum {
|
||||
@@ -143,6 +150,7 @@ export type ParseDataType = {
|
||||
state: ParseStateEnum;
|
||||
selectedParses: ChatContextType[];
|
||||
candidateParses: ChatContextType[];
|
||||
similarSolvedQuery: SimilarQuestionType[];
|
||||
}
|
||||
|
||||
export type QueryDataType = {
|
||||
@@ -173,7 +181,7 @@ export enum SemanticTypeEnum {
|
||||
};
|
||||
|
||||
export const SEMANTIC_TYPE_MAP = {
|
||||
[SemanticTypeEnum.DOMAIN]: '数据模型',
|
||||
[SemanticTypeEnum.DOMAIN]: '数据来源',
|
||||
[SemanticTypeEnum.DIMENSION]: '维度',
|
||||
[SemanticTypeEnum.METRIC]: '指标',
|
||||
[SemanticTypeEnum.VALUE]: '维度值',
|
||||
@@ -217,4 +225,17 @@ export type DrillDownDimensionType = {
|
||||
model: number;
|
||||
name: string;
|
||||
bizName: string;
|
||||
}
|
||||
}
|
||||
|
||||
export type SendMsgParamsType = {
|
||||
msg: string;
|
||||
agentId: number;
|
||||
modelId: number;
|
||||
filters?: FilterItemType[];
|
||||
}
|
||||
|
||||
export type SimilarQuestionType = {
|
||||
// queryId: number;
|
||||
// parseId: number;
|
||||
queryText: string;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ const ExecuteItem: React.FC<Props> = ({
|
||||
<>
|
||||
<div className={`${prefixCls}-title-bar`}>
|
||||
<CheckCircleFilled className={`${prefixCls}-step-icon`} />
|
||||
<div className={`${prefixCls}-step-title`}>数据查询结果</div>
|
||||
<div className={`${prefixCls}-step-title`}>数据查询</div>
|
||||
</div>
|
||||
<div className={`${prefixCls}-content-container ${prefixCls}-last-node`}>
|
||||
<Spin spinning={entitySwitchLoading}>
|
||||
@@ -68,7 +68,12 @@ const ExecuteItem: React.FC<Props> = ({
|
||||
{data?.queryMode === 'WEB_PAGE' ? (
|
||||
<WebPage id={queryId!} data={data} />
|
||||
) : (
|
||||
<ChatMsg data={data} chartIndex={chartIndex} triggerResize={triggerResize} />
|
||||
<ChatMsg
|
||||
queryId={queryId}
|
||||
data={data}
|
||||
chartIndex={chartIndex}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ const FilterItem: React.FC<Props> = ({ modelId, filters, filter, onFiltersChange
|
||||
const initData = async () => {
|
||||
const { data } = await queryDimensionValues(modelId, filter.bizName, '');
|
||||
setOptions(
|
||||
data?.data?.resultList.map((item: any) => ({
|
||||
data?.resultList.map((item: any) => ({
|
||||
label: item[filter.bizName],
|
||||
value: item[filter.bizName],
|
||||
})) || []
|
||||
@@ -47,9 +47,8 @@ const FilterItem: React.FC<Props> = ({ modelId, filters, filter, onFiltersChange
|
||||
if (fetchId !== fetchRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOptions(
|
||||
newOptions.data?.data?.resultList.map((item: any) => ({
|
||||
newOptions.data?.resultList.map((item: any) => ({
|
||||
label: item[filter.bizName],
|
||||
value: item[filter.bizName],
|
||||
})) || []
|
||||
@@ -76,7 +75,8 @@ const FilterItem: React.FC<Props> = ({ modelId, filters, filter, onFiltersChange
|
||||
|
||||
return (
|
||||
<span className={prefixCls}>
|
||||
{typeof filter.value === 'string' || isArray(filter.value) ? (
|
||||
{(typeof filter.value === 'string' || isArray(filter.value)) &&
|
||||
(filter.operator === '=' || filter.operator === 'IN') ? (
|
||||
<Select
|
||||
bordered={false}
|
||||
value={filter.value}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { AGG_TYPE_MAP, PREFIX_CLS } from '../../common/constants';
|
||||
import { ChatContextType, FilterItemType } from '../../common/type';
|
||||
import { CheckCircleFilled, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { CheckCircleFilled } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import SwicthEntity from './SwitchEntity';
|
||||
import Loading from './Loading';
|
||||
@@ -30,7 +30,7 @@ const ParseTip: React.FC<Props> = ({
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
const getNode = (tipTitle: string, tipNode?: ReactNode, parseSucceed?: boolean) => {
|
||||
const getNode = (tipTitle: ReactNode, tipNode?: ReactNode, parseSucceed?: boolean) => {
|
||||
const contentContainerClass = classNames(`${prefixCls}-content-container`, {
|
||||
[`${prefixCls}-content-container-succeed`]: parseSucceed,
|
||||
});
|
||||
@@ -62,7 +62,6 @@ const ParseTip: React.FC<Props> = ({
|
||||
|
||||
const getTipNode = (parseInfo: ChatContextType, isOptions?: boolean, index?: number) => {
|
||||
const {
|
||||
modelId,
|
||||
modelName,
|
||||
dateInfo,
|
||||
dimensionFilters,
|
||||
@@ -76,8 +75,6 @@ const ParseTip: React.FC<Props> = ({
|
||||
nativeQuery,
|
||||
} = parseInfo || {};
|
||||
|
||||
const maxOptionCount = queryMode === 'DSL' ? 10 : MAX_OPTION_VALUES_COUNT;
|
||||
|
||||
const { startDate, endDate } = dateInfo || {};
|
||||
const dimensionItems = dimensions?.filter(item => item.type === 'DIMENSION');
|
||||
const metric = metrics?.[0];
|
||||
@@ -92,44 +89,6 @@ const ParseTip: React.FC<Props> = ({
|
||||
const fields =
|
||||
queryMode === 'ENTITY_DETAIL' ? dimensionItems?.concat(metrics || []) : dimensionItems;
|
||||
|
||||
const getFilterContent = (filters: any) => {
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item-filter-content`}>
|
||||
{filters.map((filter: any) => (
|
||||
<div className={`${prefixCls}-tip-item-option`}>
|
||||
<span>
|
||||
<span className={`${prefixCls}-tip-item-filter-name`}>{filter.name}</span>
|
||||
{filter.operator !== '=' && filter.operator !== 'IN'
|
||||
? ` ${filter.operator} `
|
||||
: ':'}
|
||||
</span>
|
||||
{queryMode !== 'DSL' && !filter.bizName?.includes('_id') ? (
|
||||
<FilterItem
|
||||
modelId={modelId}
|
||||
filters={dimensionFilters}
|
||||
filter={filter}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
) : (
|
||||
<span className={itemValueClass}>{filter.value}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFiltersNode = () => {
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>筛选条件:</div>
|
||||
<div className={`${prefixCls}-tip-item-content`}>
|
||||
{getFilterContent(dimensionFilters)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${prefixCls}-tip-content`}
|
||||
@@ -147,7 +106,7 @@ const ParseTip: React.FC<Props> = ({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(queryMode.includes('ENTITY') || queryMode === 'DSL') &&
|
||||
{(queryMode?.includes('ENTITY') || queryMode === 'DSL') &&
|
||||
typeof entityId === 'string' &&
|
||||
!!entityAlias &&
|
||||
!!entityName ? (
|
||||
@@ -165,11 +124,11 @@ const ParseTip: React.FC<Props> = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>数据模型:</div>
|
||||
<div className={`${prefixCls}-tip-item-name`}>数据来源:</div>
|
||||
<div className={itemValueClass}>{modelName}</div>
|
||||
</div>
|
||||
)}
|
||||
{!queryMode.includes('ENTITY') && metric && (
|
||||
{!queryMode?.includes('ENTITY') && metric && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>指标:</div>
|
||||
<div className={itemValueClass}>{metric.name}</div>
|
||||
@@ -199,24 +158,21 @@ const ParseTip: React.FC<Props> = ({
|
||||
</div>
|
||||
<div className={itemValueClass}>
|
||||
{fields
|
||||
.slice(0, maxOptionCount)
|
||||
.slice(0, MAX_OPTION_VALUES_COUNT)
|
||||
.map(field => field.name)
|
||||
.join('、')}
|
||||
{fields.length > maxOptionCount && '...'}
|
||||
{fields.length > MAX_OPTION_VALUES_COUNT && '...'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{[
|
||||
'METRIC_FILTER',
|
||||
'METRIC_ENTITY',
|
||||
'ENTITY_DETAIL',
|
||||
'ENTITY_LIST_FILTER',
|
||||
'ENTITY_ID',
|
||||
'DSL',
|
||||
].includes(queryMode) &&
|
||||
dimensionFilters &&
|
||||
dimensionFilters?.length > 0 &&
|
||||
getFiltersNode()}
|
||||
{queryMode !== 'ENTITY_ID' &&
|
||||
entityDimensions?.length > 0 &&
|
||||
entityDimensions.map(dimension => (
|
||||
<div className={`${prefixCls}-tip-item`} key={dimension.itemId}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>{dimension.name}:</div>
|
||||
<div className={itemValueClass}>{dimension.value}</div>
|
||||
</div>
|
||||
))}
|
||||
{queryMode === 'METRIC_ORDERBY' && aggType && aggType !== 'NONE' && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>聚合方式:</div>
|
||||
@@ -230,7 +186,8 @@ const ParseTip: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const parseInfo = parseInfoOptions[0] || {};
|
||||
const { properties, entity, entityInfo, elementMatches, queryMode } = parseInfo || {};
|
||||
const { modelId, properties, entity, entityInfo, elementMatches, queryMode, dimensionFilters } =
|
||||
parseInfo || {};
|
||||
|
||||
const { type } = properties || {};
|
||||
const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
|
||||
@@ -249,31 +206,71 @@ const ParseTip: React.FC<Props> = ({
|
||||
)
|
||||
);
|
||||
|
||||
const getFilterContent = (filters: any) => {
|
||||
const itemValueClass = `${prefixCls}-tip-item-value`;
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item-filter-content`}>
|
||||
{filters.map((filter: any) => (
|
||||
<div className={`${prefixCls}-tip-item-option`} key={filter.name}>
|
||||
<span>
|
||||
<span className={`${prefixCls}-tip-item-filter-name`}>{filter.name}</span>
|
||||
{filter.operator !== '=' && filter.operator !== 'IN' ? ` ${filter.operator} ` : ':'}
|
||||
</span>
|
||||
{/* {queryMode !== 'DSL' && !filter.bizName?.includes('_id') ? ( */}
|
||||
{!filter.bizName?.includes('_id') ? (
|
||||
<FilterItem
|
||||
modelId={modelId}
|
||||
filters={dimensionFilters}
|
||||
filter={filter}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
) : (
|
||||
<span className={itemValueClass}>{filter.value}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFiltersNode = () => {
|
||||
return (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>筛选条件:</div>
|
||||
<div className={`${prefixCls}-tip-item-content`}>{getFilterContent(dimensionFilters)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tipNode = (
|
||||
<div className={`${prefixCls}-tip`}>
|
||||
{getTipNode(parseInfo)}
|
||||
{queryMode !== 'ENTITY_ID' && entityDimensions?.length > 0 && (
|
||||
<div className={`${prefixCls}-entity-info`}>
|
||||
{entityDimensions.map(dimension => (
|
||||
<div className={`${prefixCls}-dimension-item`} key={dimension.itemId}>
|
||||
<div className={`${prefixCls}-dimension-name`}>{dimension.name}:</div>
|
||||
<div className={`${prefixCls}-dimension-value`}>{dimension.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(!type || queryMode === 'DSL') && entityAlias && entityName && (
|
||||
<div className={`${prefixCls}-switch-entity-tip`}>
|
||||
<InfoCircleOutlined />
|
||||
<div>
|
||||
如果未匹配到您查询的{entityAlias},可点击上面的{entityAlias}名切换
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{[
|
||||
'METRIC_FILTER',
|
||||
'METRIC_ENTITY',
|
||||
'ENTITY_DETAIL',
|
||||
'ENTITY_LIST_FILTER',
|
||||
'ENTITY_ID',
|
||||
'DSL',
|
||||
].includes(queryMode) &&
|
||||
dimensionFilters &&
|
||||
dimensionFilters?.length > 0 &&
|
||||
getFiltersNode()}
|
||||
</div>
|
||||
);
|
||||
|
||||
return getNode('意图解析结果', tipNode, true);
|
||||
return getNode(
|
||||
<div className={`${prefixCls}-title-bar`}>
|
||||
意图解析
|
||||
{(!type || queryMode === 'DSL') && entityAlias && entityAlias !== '厂牌' && entityName && (
|
||||
<div className={`${prefixCls}-switch-entity-tip`}>
|
||||
(如果未匹配到您查询的{entityAlias},可点击{entityAlias}名切换)
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
tipNode,
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
export default ParseTip;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { CheckCircleFilled, DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { PREFIX_CLS } from '../../common/constants';
|
||||
import { SimilarQuestionType } from '../../common/type';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
similarQuestions: SimilarQuestionType[];
|
||||
defaultExpanded?: boolean;
|
||||
onSelectQuestion: (question: SimilarQuestionType) => void;
|
||||
};
|
||||
|
||||
const SimilarQuestions: React.FC<Props> = ({
|
||||
similarQuestions,
|
||||
defaultExpanded,
|
||||
onSelectQuestion,
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(defaultExpanded || false);
|
||||
|
||||
const tipPrefixCls = `${PREFIX_CLS}-item`;
|
||||
const prefixCls = `${PREFIX_CLS}-similar-questions`;
|
||||
|
||||
const onToggleExpanded = () => {
|
||||
setExpanded(!expanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${tipPrefixCls}-parse-tip`}>
|
||||
<div className={`${tipPrefixCls}-title-bar`}>
|
||||
<CheckCircleFilled className={`${tipPrefixCls}-step-icon`} />
|
||||
<div className={`${tipPrefixCls}-step-title`}>
|
||||
推荐相似问题
|
||||
<span className={`${prefixCls}-toggle-expand-btn`} onClick={onToggleExpanded}>
|
||||
{expanded ? <UpOutlined /> : <DownOutlined />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={prefixCls}>
|
||||
{expanded && (
|
||||
<div className={`${prefixCls}-content`}>
|
||||
{similarQuestions.slice(0, 5).map((question, index) => {
|
||||
return (
|
||||
<div
|
||||
className={`${prefixCls}-question`}
|
||||
key={question.queryText}
|
||||
onClick={() => {
|
||||
onSelectQuestion(question);
|
||||
}}
|
||||
>
|
||||
{index + 1}. {question.queryText}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimilarQuestions;
|
||||
116
webapp/packages/chat-sdk/src/components/ChatItem/SqlItem.tsx
Normal file
116
webapp/packages/chat-sdk/src/components/ChatItem/SqlItem.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, { useState } from 'react';
|
||||
import { format } from 'sql-formatter';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { solarizedlight } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { message } from 'antd';
|
||||
import { PREFIX_CLS } from '../../common/constants';
|
||||
import { CheckCircleFilled, UpOutlined } from '@ant-design/icons';
|
||||
import { SqlInfoType } from '../../common/type';
|
||||
|
||||
type Props = {
|
||||
integrateSystem?: string;
|
||||
sqlInfo: SqlInfoType;
|
||||
};
|
||||
|
||||
const SqlItem: React.FC<Props> = ({ integrateSystem, sqlInfo }) => {
|
||||
const [sqlType, setSqlType] = useState('');
|
||||
|
||||
const tipPrefixCls = `${PREFIX_CLS}-item`;
|
||||
const prefixCls = `${PREFIX_CLS}-sql-item`;
|
||||
|
||||
const handleCopy = (text, result) => {
|
||||
result ? message.success('复制SQL成功', 1) : message.error('复制SQL失败', 1);
|
||||
};
|
||||
|
||||
const onCollapse = () => {
|
||||
setSqlType('');
|
||||
};
|
||||
|
||||
if (!sqlInfo.llmParseSql && !sqlInfo.logicSql && !sqlInfo.querySql) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${tipPrefixCls}-parse-tip`}>
|
||||
<div className={`${tipPrefixCls}-title-bar`}>
|
||||
<CheckCircleFilled className={`${tipPrefixCls}-step-icon`} />
|
||||
<div className={`${tipPrefixCls}-step-title`}>
|
||||
SQL生成
|
||||
{sqlType && (
|
||||
<span className={`${prefixCls}-toggle-expand-btn`} onClick={onCollapse}>
|
||||
<UpOutlined />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${prefixCls}-sql-options`}>
|
||||
{sqlInfo.llmParseSql && (
|
||||
<div
|
||||
className={`${prefixCls}-sql-option ${
|
||||
sqlType === 'llmParseSql' ? `${prefixCls}-sql-option-active` : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSqlType(sqlType === 'llmParseSql' ? '' : 'llmParseSql');
|
||||
}}
|
||||
>
|
||||
LLM解析SQL
|
||||
</div>
|
||||
)}
|
||||
{sqlInfo.logicSql && (
|
||||
<div
|
||||
className={`${prefixCls}-sql-option ${
|
||||
sqlType === 'logicSql' ? `${prefixCls}-sql-option-active` : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSqlType(sqlType === 'logicSql' ? '' : 'logicSql');
|
||||
}}
|
||||
>
|
||||
逻辑SQL
|
||||
</div>
|
||||
)}
|
||||
{sqlInfo.querySql && (
|
||||
<div
|
||||
className={`${prefixCls}-sql-option ${
|
||||
sqlType === 'querySql' ? `${prefixCls}-sql-option-active` : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSqlType(sqlType === 'querySql' ? '' : 'querySql');
|
||||
}}
|
||||
>
|
||||
物理SQL
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${prefixCls} ${
|
||||
!window.location.pathname.includes('/chat') &&
|
||||
integrateSystem &&
|
||||
integrateSystem !== 'wiki'
|
||||
? `${prefixCls}-copilot`
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{sqlType && (
|
||||
<>
|
||||
<SyntaxHighlighter
|
||||
className={`${prefixCls}-code`}
|
||||
language="sql"
|
||||
style={solarizedlight}
|
||||
>
|
||||
{format(sqlInfo[sqlType])}
|
||||
</SyntaxHighlighter>
|
||||
<CopyToClipboard
|
||||
text={format(sqlInfo[sqlType])}
|
||||
onCopy={(text, result) => handleCopy(text, result)}
|
||||
>
|
||||
<button className={`${prefixCls}-copy-btn`}>复制代码</button>
|
||||
</CopyToClipboard>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SqlItem;
|
||||
@@ -1,4 +1,10 @@
|
||||
import { ChatContextType, FilterItemType, MsgDataType, ParseStateEnum } from '../../common/type';
|
||||
import {
|
||||
ChatContextType,
|
||||
FilterItemType,
|
||||
MsgDataType,
|
||||
ParseStateEnum,
|
||||
SimilarQuestionType,
|
||||
} from '../../common/type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { chatExecute, chatParse, queryData, switchEntity } from '../../service';
|
||||
import { PARSE_ERROR_TIP, PREFIX_CLS, SEARCH_EXCEPTION_TIP } from '../../common/constants';
|
||||
@@ -8,6 +14,8 @@ import ExecuteItem from './ExecuteItem';
|
||||
import { isMobile } from '../../utils/utils';
|
||||
import classNames from 'classnames';
|
||||
import Tools from '../Tools';
|
||||
import SqlItem from './SqlItem';
|
||||
import SimilarQuestionItem from './SimilarQuestionItem';
|
||||
|
||||
type Props = {
|
||||
msg: string;
|
||||
@@ -17,11 +25,13 @@ type Props = {
|
||||
filter?: any[];
|
||||
isLastMessage?: boolean;
|
||||
msgData?: MsgDataType;
|
||||
isHistory?: boolean;
|
||||
triggerResize?: boolean;
|
||||
parseOptions?: ChatContextType[];
|
||||
isDeveloper?: boolean;
|
||||
integrateSystem?: string;
|
||||
onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void;
|
||||
onUpdateMessageScroll?: () => void;
|
||||
onSendMsg?: (msg: string) => void;
|
||||
};
|
||||
|
||||
const ChatItem: React.FC<Props> = ({
|
||||
@@ -31,12 +41,14 @@ const ChatItem: React.FC<Props> = ({
|
||||
agentId,
|
||||
filter,
|
||||
isLastMessage,
|
||||
isHistory,
|
||||
triggerResize,
|
||||
msgData,
|
||||
parseOptions,
|
||||
isDeveloper,
|
||||
integrateSystem,
|
||||
onMsgDataLoaded,
|
||||
onUpdateMessageScroll,
|
||||
onSendMsg,
|
||||
}) => {
|
||||
const [data, setData] = useState<MsgDataType>();
|
||||
const [parseLoading, setParseLoading] = useState(false);
|
||||
@@ -47,6 +59,7 @@ const ChatItem: React.FC<Props> = ({
|
||||
const [executeTip, setExecuteTip] = useState('');
|
||||
const [executeMode, setExecuteMode] = useState(false);
|
||||
const [entitySwitchLoading, setEntitySwitchLoading] = useState(false);
|
||||
const [similarQuestions, setSimilarQuestions] = useState<SimilarQuestionType[]>([]);
|
||||
|
||||
const [chartIndex, setChartIndex] = useState(0);
|
||||
|
||||
@@ -79,13 +92,13 @@ const ChatItem: React.FC<Props> = ({
|
||||
setExecuteMode(true);
|
||||
setExecuteLoading(true);
|
||||
try {
|
||||
const { data } = await chatExecute(msg, conversationId!, parseInfoValue);
|
||||
const res: any = await chatExecute(msg, conversationId!, parseInfoValue);
|
||||
setExecuteLoading(false);
|
||||
const valid = updateData(data);
|
||||
const valid = updateData(res);
|
||||
if (onMsgDataLoaded) {
|
||||
onMsgDataLoaded(
|
||||
{
|
||||
...data.data,
|
||||
...res.data,
|
||||
chatContext: parseInfoValue,
|
||||
},
|
||||
valid
|
||||
@@ -97,12 +110,13 @@ const ChatItem: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onSendMsg = async () => {
|
||||
const sendMsg = async () => {
|
||||
setParseLoading(true);
|
||||
const { data: parseData } = await chatParse(msg, conversationId, modelId, agentId, filter);
|
||||
const parseData: any = await chatParse(msg, conversationId, modelId, agentId, filter);
|
||||
setParseLoading(false);
|
||||
const { code, data } = parseData || {};
|
||||
const { state, selectedParses, queryId } = data || {};
|
||||
const { state, selectedParses, queryId, similarSolvedQuery } = data || {};
|
||||
setSimilarQuestions(similarSolvedQuery || []);
|
||||
if (
|
||||
code !== 200 ||
|
||||
state === ParseStateEnum.FAILED ||
|
||||
@@ -115,7 +129,7 @@ const ChatItem: React.FC<Props> = ({
|
||||
if (onUpdateMessageScroll) {
|
||||
onUpdateMessageScroll();
|
||||
}
|
||||
const parseInfos = selectedParses.map(item => ({
|
||||
const parseInfos = selectedParses.map((item: any) => ({
|
||||
...item,
|
||||
queryId,
|
||||
}));
|
||||
@@ -126,15 +140,17 @@ const ChatItem: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data !== undefined || parseOptions !== undefined || executeTip !== '') {
|
||||
if (data !== undefined || parseOptions !== undefined || executeTip !== '' || parseLoading) {
|
||||
return;
|
||||
}
|
||||
if (msgData) {
|
||||
setParseInfoOptions([msgData.chatContext]);
|
||||
const parseInfoValue = { ...msgData.chatContext, queryId: msgData.queryId };
|
||||
setParseInfoOptions([parseInfoValue]);
|
||||
setParseInfo(parseInfoValue);
|
||||
setExecuteMode(true);
|
||||
updateData({ code: 200, data: msgData, msg: 'success' });
|
||||
} else if (msg) {
|
||||
onSendMsg();
|
||||
sendMsg();
|
||||
}
|
||||
}, [msg, msgData]);
|
||||
|
||||
@@ -142,19 +158,27 @@ const ChatItem: React.FC<Props> = ({
|
||||
setEntitySwitchLoading(true);
|
||||
const res = await switchEntity(entityId, data?.chatContext?.modelId, conversationId || 0);
|
||||
setEntitySwitchLoading(false);
|
||||
setData(res.data.data);
|
||||
const { chatContext } = res.data.data;
|
||||
setData(res.data);
|
||||
const { chatContext } = res.data;
|
||||
setParseInfo(chatContext);
|
||||
setParseInfoOptions([chatContext]);
|
||||
};
|
||||
|
||||
const onFiltersChange = async (dimensionFilters: FilterItemType[]) => {
|
||||
setEntitySwitchLoading(true);
|
||||
const chatContextValue = { ...(parseInfoOptions[0] || {}), dimensionFilters };
|
||||
const { dimensions, metrics, dateInfo, id, queryId } = parseInfoOptions[0] || {};
|
||||
const chatContextValue = {
|
||||
dimensions,
|
||||
metrics,
|
||||
dateInfo,
|
||||
dimensionFilters,
|
||||
parseId: id,
|
||||
queryId,
|
||||
};
|
||||
const res: any = await queryData(chatContextValue);
|
||||
setEntitySwitchLoading(false);
|
||||
const resChatContext = res.data?.data?.chatContext;
|
||||
setData({ ...(res.data?.data || {}), chatContext: resChatContext || chatContextValue });
|
||||
const resChatContext = res.data?.chatContext;
|
||||
setData({ ...(res.data || {}), chatContext: resChatContext || chatContextValue });
|
||||
setParseInfo(resChatContext || chatContextValue);
|
||||
setParseInfoOptions([resChatContext || chatContextValue]);
|
||||
};
|
||||
@@ -167,6 +191,10 @@ const ChatItem: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectQuestion = (question: SimilarQuestionType) => {
|
||||
onSendMsg?.(question.queryText);
|
||||
};
|
||||
|
||||
const contentClass = classNames(`${prefixCls}-content`, {
|
||||
[`${prefixCls}-content-mobile`]: isMobile,
|
||||
});
|
||||
@@ -189,17 +217,36 @@ const ChatItem: React.FC<Props> = ({
|
||||
onSwitchEntity={onSwitchEntity}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
{executeMode && (
|
||||
<ExecuteItem
|
||||
queryId={parseInfo?.queryId}
|
||||
executeLoading={executeLoading}
|
||||
entitySwitchLoading={entitySwitchLoading}
|
||||
executeTip={executeTip}
|
||||
chartIndex={chartIndex}
|
||||
data={data}
|
||||
triggerResize={triggerResize}
|
||||
{parseTip && similarQuestions.length > 0 && (
|
||||
<SimilarQuestionItem
|
||||
similarQuestions={similarQuestions}
|
||||
defaultExpanded
|
||||
onSelectQuestion={onSelectQuestion}
|
||||
/>
|
||||
)}
|
||||
{executeMode && (
|
||||
<>
|
||||
{parseInfoOptions?.[0]?.sqlInfo && isDeveloper && integrateSystem !== 'c2' && (
|
||||
<SqlItem integrateSystem={integrateSystem} sqlInfo={parseInfoOptions[0].sqlInfo} />
|
||||
)}
|
||||
{similarQuestions.length > 0 && (
|
||||
<SimilarQuestionItem
|
||||
similarQuestions={similarQuestions}
|
||||
defaultExpanded={executeTip !== ''}
|
||||
onSelectQuestion={onSelectQuestion}
|
||||
/>
|
||||
)}
|
||||
<ExecuteItem
|
||||
queryId={parseInfo?.queryId}
|
||||
executeLoading={executeLoading}
|
||||
entitySwitchLoading={entitySwitchLoading}
|
||||
executeTip={executeTip}
|
||||
chartIndex={chartIndex}
|
||||
data={data}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isMetricCard && data && (
|
||||
<Tools data={data} scoreValue={undefined} isLastMessage={isLastMessage} />
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
@chat-item-prefix-cls: ~'@{supersonic-chat-prefix}-item';
|
||||
@filter-item-prefix-cls: ~'@{supersonic-chat-prefix}-filter-item';
|
||||
@sql-item-prefix-cls: ~'@{supersonic-chat-prefix}-sql-item';
|
||||
@similar-questions-prefix-cls: ~'@{supersonic-chat-prefix}-similar-questions';
|
||||
|
||||
.@{chat-item-prefix-cls} {
|
||||
display: flex;
|
||||
@@ -17,7 +19,6 @@
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
// border-radius: 50%;
|
||||
background-color: var(--text-color);
|
||||
margin: 0 2px;
|
||||
opacity: 0;
|
||||
@@ -124,8 +125,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-color-third);
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
&-tip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 6px;
|
||||
row-gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
@@ -178,7 +178,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
row-gap: 10px;
|
||||
column-gap: 12px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
@@ -251,7 +251,6 @@
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
column-gap: 12px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-color-third);
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -325,4 +324,93 @@
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{sql-item-prefix-cls} {
|
||||
position: relative;
|
||||
margin: 2px 0 2px 7px;
|
||||
padding: 2px 0 8px 18px;
|
||||
border-left: 1px solid var(--green);
|
||||
overflow: auto;
|
||||
|
||||
&-toggle-expand-btn {
|
||||
margin-left: 4px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-sql-options {
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 13px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-sql-option {
|
||||
border-radius: 4px;
|
||||
padding: 1px 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-sql-option-active {
|
||||
color: #fff !important;
|
||||
background-color: var(--chat-blue);
|
||||
}
|
||||
|
||||
&-code {
|
||||
margin-top: 10px !important;
|
||||
padding: 6px 14px 8px !important;
|
||||
border: 1px solid var(--border-color-base) !important;
|
||||
border-radius: 4px !important;
|
||||
background: #f5f8fb !important;
|
||||
}
|
||||
|
||||
&-copy-btn {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 20px;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
color: var(--chat-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.@{sql-item-prefix-cls}-copilot {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.@{similar-questions-prefix-cls} {
|
||||
position: relative;
|
||||
margin: 2px 0 2px 7px;
|
||||
padding: 2px 0 8px 18px;
|
||||
border-left: 1px solid var(--green);
|
||||
overflow: auto;
|
||||
|
||||
&-toggle-expand-btn {
|
||||
margin-left: 4px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&-question {
|
||||
width: fit-content;
|
||||
color: var(--chat-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ const Message: React.FC<Props> = ({
|
||||
<div className={`${prefixCls}-info-bar`}>
|
||||
<div className={`${prefixCls}-main-entity-info`}>
|
||||
<div className={`${prefixCls}-info-item`}>
|
||||
<div className={`${prefixCls}-info-name`}>数据模型:</div>
|
||||
<div className={`${prefixCls}-info-name`}>数据来源:</div>
|
||||
<div className={`${prefixCls}-info-value`}>{modelName}</div>
|
||||
</div>
|
||||
<div className={`${prefixCls}-info-item`}>
|
||||
|
||||
@@ -19,33 +19,7 @@ const Text: React.FC<Props> = ({ columns, referenceColumn, dataSource }) => {
|
||||
|
||||
const initData = () => {
|
||||
let textValue = dataSource[0][columns[0].nameEn];
|
||||
let htmlCodeValue: string;
|
||||
const match = textValue.match(/```html([\s\S]*?)```/);
|
||||
htmlCodeValue = match && match[1].trim();
|
||||
if (htmlCodeValue) {
|
||||
textValue = textValue.replace(/```html([\s\S]*?)```/, '');
|
||||
}
|
||||
let scriptCode: string;
|
||||
let scriptSrc: string;
|
||||
if (htmlCodeValue) {
|
||||
scriptSrc = htmlCodeValue.match(/<script src="([\s\S]*?)"><\/script>/)?.[1] || '';
|
||||
scriptCode =
|
||||
htmlCodeValue.match(/<script type="text\/javascript">([\s\S]*?)<\/script>/)?.[1] || '';
|
||||
if (scriptSrc) {
|
||||
const script = document.createElement('script');
|
||||
script.src = scriptSrc;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
if (scriptCode) {
|
||||
const script = document.createElement('script');
|
||||
script.innerHTML = scriptCode;
|
||||
setTimeout(() => {
|
||||
document.body.appendChild(script);
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
setText(textValue);
|
||||
setHtmlCode(htmlCodeValue);
|
||||
setText(textValue === undefined ? '暂无数据,如有疑问请联系管理员' : textValue);
|
||||
if (referenceColumn) {
|
||||
const referenceDataValue = dataSource[0][referenceColumn.nameEn];
|
||||
setReferenceData(referenceDataValue || []);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { CLS_PREFIX } from '../../../common/constants';
|
||||
import { MsgDataType } from '../../../common/type';
|
||||
import { isProd } from '../../../utils/utils';
|
||||
import { getToken, isProd } from '../../../utils/utils';
|
||||
|
||||
type Props = {
|
||||
id: string | number;
|
||||
@@ -89,10 +89,8 @@ const WebPage: React.FC<Props> = ({ id, data }) => {
|
||||
);
|
||||
urlValue = urlValue.replace(
|
||||
'?',
|
||||
`?miniProgram=true&reportName=${name}&filterData=${filterData}&`
|
||||
`?token=${getToken()}&miniProgram=true&reportName=${name}&filterData=${filterData}&`
|
||||
);
|
||||
urlValue =
|
||||
!isProd() && !urlValue.includes('http') ? `http://s2.tmeoa.com${urlValue}` : urlValue;
|
||||
} else {
|
||||
const params = Object.keys(valueParams || {}).map(key => `${key}=${valueParams[key]}`);
|
||||
if (params.length > 0) {
|
||||
@@ -103,7 +101,6 @@ const WebPage: React.FC<Props> = ({ id, data }) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// onReportLoaded(heightValue + 190);
|
||||
setPluginUrl(urlValue);
|
||||
};
|
||||
|
||||
@@ -112,7 +109,6 @@ const WebPage: React.FC<Props> = ({ id, data }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// <div className={prefixCls} style={{ height }}>
|
||||
<iframe
|
||||
id={`reportIframe_${id}`}
|
||||
name={`reportIframe_${id}`}
|
||||
@@ -121,7 +117,6 @@ const WebPage: React.FC<Props> = ({ id, data }) => {
|
||||
title="reportIframe"
|
||||
allowFullScreen
|
||||
/>
|
||||
// </div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ import DrillDownDimensions from '../DrillDownDimensions';
|
||||
import MetricOptions from '../MetricOptions';
|
||||
|
||||
type Props = {
|
||||
queryId?: number;
|
||||
data: MsgDataType;
|
||||
chartIndex: number;
|
||||
triggerResize?: boolean;
|
||||
};
|
||||
|
||||
const ChatMsg: React.FC<Props> = ({ data, chartIndex, triggerResize }) => {
|
||||
const ChatMsg: React.FC<Props> = ({ queryId, data, chartIndex, triggerResize }) => {
|
||||
const { queryColumns, queryResults, chatContext, queryMode } = data || {};
|
||||
const { dimensionFilters, elementMatches } = chatContext || {};
|
||||
|
||||
@@ -127,14 +128,16 @@ const ChatMsg: React.FC<Props> = ({ data, chartIndex, triggerResize }) => {
|
||||
|
||||
const onLoadData = async (value: any) => {
|
||||
setLoading(true);
|
||||
const { data } = await queryData({
|
||||
...chatContext,
|
||||
const res: any = await queryData({
|
||||
// ...chatContext,
|
||||
queryId,
|
||||
parseId: chatContext.id,
|
||||
...value,
|
||||
});
|
||||
setLoading(false);
|
||||
if (data.code === 200) {
|
||||
updateColummns(data.data?.queryColumns || []);
|
||||
setDataSource(data.data?.queryResults || []);
|
||||
if (res.code === 200) {
|
||||
updateColummns(res.data?.queryColumns || []);
|
||||
setDataSource(res.data?.queryResults || []);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const DrillDownDimensions: React.FC<Props> = ({
|
||||
const initData = async () => {
|
||||
const res = await queryDrillDownDimensions(modelId);
|
||||
setDimensions(
|
||||
res.data.data.dimensions
|
||||
res.data.dimensions
|
||||
.filter(
|
||||
dimension =>
|
||||
!dimensionFilters?.some(filter => filter.name === dimension.name) &&
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_x5c4www9bqm.js',
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_46xw04fpzii.js',
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -25,7 +25,7 @@ const RecommendOptions: React.FC<Props> = ({ entityId, modelId, modelName, onSel
|
||||
setLoading(true);
|
||||
const res = await queryEntities(entityId, modelId);
|
||||
setLoading(false);
|
||||
setData(res.data.data);
|
||||
setData(res.data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
14
webapp/packages/chat-sdk/src/demo/ChatDemo.tsx
Normal file
14
webapp/packages/chat-sdk/src/demo/ChatDemo.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Chat from '../Chat';
|
||||
import styles from './style.module.less';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const ChatDemo: React.FC<Props> = ({}) => {
|
||||
return (
|
||||
<div className={styles.chatDemo}>
|
||||
<Chat isDeveloper />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatDemo;
|
||||
50
webapp/packages/chat-sdk/src/demo/CopilotDemo.tsx
Normal file
50
webapp/packages/chat-sdk/src/demo/CopilotDemo.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Button, Space } from 'antd';
|
||||
import styles from './style.module.less';
|
||||
import Copilot from '../Copilot';
|
||||
import { useRef } from 'react';
|
||||
|
||||
const buttonParams = [
|
||||
{
|
||||
msg: '周杰伦 艺人趋势解读',
|
||||
agentId: 8,
|
||||
modelId: 23,
|
||||
filters: [{ bizName: 'singer_id', elementID: 283, value: 4558 }],
|
||||
},
|
||||
{
|
||||
msg: '林俊杰 艺人趋势解读',
|
||||
agentId: 8,
|
||||
modelId: 23,
|
||||
filters: [{ bizName: 'singer_id', elementID: 283, value: 4286 }],
|
||||
},
|
||||
];
|
||||
|
||||
const CopilotDemo = () => {
|
||||
const copilotRef = useRef<any>();
|
||||
|
||||
return (
|
||||
<div className={styles.copilotDemo}>
|
||||
<Space>
|
||||
{buttonParams.map(params => (
|
||||
<Button
|
||||
key={params.msg}
|
||||
onClick={() => {
|
||||
copilotRef?.current?.sendCopilotMsg(params);
|
||||
}}
|
||||
>
|
||||
{params.msg}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
<Copilot
|
||||
// token={localStorage.getItem('SUPERSONIC_TOKEN') || ''}
|
||||
// agentIds={[8]}
|
||||
isDeveloper
|
||||
// integrateSystem="c2"
|
||||
ref={copilotRef}
|
||||
// noInput
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopilotDemo;
|
||||
@@ -8,4 +8,8 @@
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.chatDemo {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import './styles/index.less';
|
||||
|
||||
// import React from 'react';
|
||||
// import ReactDOM from 'react-dom/client';
|
||||
// import Chat from './demo/Chat';
|
||||
|
||||
// import ChatDemo from './demo/ChatDemo';
|
||||
// import CopilotDemo from './demo/CopilotDemo';
|
||||
// const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
// root.render(<ChatDemo />);
|
||||
|
||||
// root.render(
|
||||
// <React.StrictMode>
|
||||
// <Chat />
|
||||
// </React.StrictMode>
|
||||
// );
|
||||
export { default as Chat } from './Chat';
|
||||
|
||||
export { default as Copilot } from './Copilot';
|
||||
|
||||
export { default as ChatMsg } from './components/ChatMsg';
|
||||
|
||||
@@ -33,6 +32,7 @@ export type {
|
||||
FilterItemType,
|
||||
HistoryType,
|
||||
HistoryMsgItemType,
|
||||
SendMsgParamsType,
|
||||
} from './common/type';
|
||||
|
||||
export { getHistoryMsg, searchRecommend, queryContext } from './service';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getToken } from '../utils/utils';
|
||||
// 创建axios实例
|
||||
const axiosInstance: AxiosInstance = axios.create({
|
||||
// 设置基本URL,所有请求都会使用这个URL作为前缀
|
||||
baseURL: '',
|
||||
baseURL: localStorage.getItem('SUPERSONIC_CHAT_API_URL') || '',
|
||||
// 设置请求超时时间(毫秒)
|
||||
timeout: 120000,
|
||||
// 设置请求头
|
||||
@@ -19,7 +19,7 @@ axiosInstance.interceptors.request.use(
|
||||
(config: any) => {
|
||||
const token = getToken();
|
||||
if (token && config?.headers) {
|
||||
config.headers.Auth = `Bearer ${token}`;
|
||||
// config.headers.Auth = `Bearer ${token}`;
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
@@ -33,18 +33,18 @@ axiosInstance.interceptors.request.use(
|
||||
// 响应拦截器
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response: any) => {
|
||||
const redirect = response.headers.get('redirect');
|
||||
const redirect = response.headers['redirect'];
|
||||
if (redirect === 'REDIRECT') {
|
||||
let win: any = window;
|
||||
while (win !== win.top) {
|
||||
win = win.top;
|
||||
}
|
||||
const contextpath = response.headers.get('contextpath');
|
||||
const contextpath = response.headers['contextpath'];
|
||||
win.location.href =
|
||||
contextpath?.substring(0, contextpath?.indexOf('&')) +
|
||||
`&redirect_uri=${encodeURIComponent(`http://${win.location.host}`)}`;
|
||||
}
|
||||
return response;
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
// 对响应错误进行处理
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import axios from './axiosInstance';
|
||||
import { ChatContextType, DrillDownDimensionType, HistoryType, MsgDataType, ParseDataType, SearchRecommendItem } from '../common/type';
|
||||
|
||||
const DEFAULT_CHAT_ID = 12009993;
|
||||
const DEFAULT_CHAT_ID = 0;
|
||||
|
||||
const prefix = '/api';
|
||||
|
||||
export function searchRecommend(queryText: string, chatId?: number, modelId?: number, agentId?: number) {
|
||||
return axios.post<Result<SearchRecommendItem[]>>(`${prefix}/chat/query/search`, {
|
||||
return axios.post<SearchRecommendItem[]>(`${prefix}/chat/query/search`, {
|
||||
queryText,
|
||||
chatId: chatId || DEFAULT_CHAT_ID,
|
||||
modelId,
|
||||
@@ -15,7 +15,7 @@ export function searchRecommend(queryText: string, chatId?: number, modelId?: nu
|
||||
}
|
||||
|
||||
export function chatQuery(queryText: string, chatId?: number, modelId?: number, filters?: any[]) {
|
||||
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/query`, {
|
||||
return axios.post<MsgDataType>(`${prefix}/chat/query/query`, {
|
||||
queryText,
|
||||
chatId: chatId || DEFAULT_CHAT_ID,
|
||||
modelId,
|
||||
@@ -26,7 +26,7 @@ export function chatQuery(queryText: string, chatId?: number, modelId?: number,
|
||||
}
|
||||
|
||||
export function chatParse(queryText: string, chatId?: number, modelId?: number, agentId?: number, filters?: any[]) {
|
||||
return axios.post<Result<ParseDataType>>(`${prefix}/chat/query/parse`, {
|
||||
return axios.post<ParseDataType>(`${prefix}/chat/query/parse`, {
|
||||
queryText,
|
||||
chatId: chatId || DEFAULT_CHAT_ID,
|
||||
modelId,
|
||||
@@ -38,7 +38,7 @@ export function chatParse(queryText: string, chatId?: number, modelId?: number,
|
||||
}
|
||||
|
||||
export function chatExecute(queryText: string, chatId: number, parseInfo: ChatContextType ) {
|
||||
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/execute`, {
|
||||
return axios.post<MsgDataType>(`${prefix}/chat/query/execute`, {
|
||||
queryText,
|
||||
chatId: chatId || DEFAULT_CHAT_ID,
|
||||
queryId: parseInfo.queryId,
|
||||
@@ -47,7 +47,7 @@ export function chatExecute(queryText: string, chatId: number, parseInfo: ChatC
|
||||
}
|
||||
|
||||
export function switchEntity(entityId: string, modelId?: number, chatId?: number) {
|
||||
return axios.post<Result<any>>(`${prefix}/chat/query/switchQuery`, {
|
||||
return axios.post<any>(`${prefix}/chat/query/switchQuery`, {
|
||||
queryText: entityId,
|
||||
modelId,
|
||||
chatId: chatId || DEFAULT_CHAT_ID,
|
||||
@@ -55,46 +55,46 @@ export function switchEntity(entityId: string, modelId?: number, chatId?: number
|
||||
}
|
||||
|
||||
export function queryData(chatContext: Partial<ChatContextType>) {
|
||||
return axios.post<Result<MsgDataType>>(`${prefix}/chat/query/queryData`, chatContext);
|
||||
return axios.post<MsgDataType>(`${prefix}/chat/query/queryData`, chatContext);
|
||||
}
|
||||
|
||||
export function queryContext(queryText: string, chatId?: number) {
|
||||
return axios.post<Result<ChatContextType>>(`${prefix}/chat/query/queryContext`, {
|
||||
return axios.post<ChatContextType>(`${prefix}/chat/query/queryContext`, {
|
||||
queryText,
|
||||
chatId: chatId || DEFAULT_CHAT_ID,
|
||||
});
|
||||
}
|
||||
|
||||
export function getHistoryMsg(current: number, chatId: number = DEFAULT_CHAT_ID, pageSize: number = 10) {
|
||||
return axios.post<Result<HistoryType>>(`${prefix}/chat/manage/pageQueryInfo?chatId=${chatId}`, {
|
||||
return axios.post<HistoryType>(`${prefix}/chat/manage/pageQueryInfo?chatId=${chatId}`, {
|
||||
current,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
export function saveConversation(chatName: string) {
|
||||
return axios.post<Result<any>>(`${prefix}/chat/manage/save?chatName=${chatName}`);
|
||||
return axios.post<any>(`${prefix}/chat/manage/save?chatName=${chatName}`);
|
||||
}
|
||||
|
||||
export function getAllConversations() {
|
||||
return axios.get<Result<any>>(`${prefix}/chat/manage/getAll`);
|
||||
return axios.get<any>(`${prefix}/chat/manage/getAll`);
|
||||
}
|
||||
|
||||
export function queryEntities(entityId: string | number, modelId: number) {
|
||||
return axios.post<Result<any>>(`${prefix}/chat/query/choice`, {
|
||||
return axios.post<any>(`${prefix}/chat/query/choice`, {
|
||||
entityId,
|
||||
modelId,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateQAFeedback(questionId: number, score: number) {
|
||||
return axios.post<Result<any>>(`${prefix}/chat/manage/updateQAFeedback?id=${questionId}&score=${score}&feedback=`);
|
||||
return axios.post<any>(`${prefix}/chat/manage/updateQAFeedback?id=${questionId}&score=${score}&feedback=`);
|
||||
}
|
||||
|
||||
export function queryDrillDownDimensions(modelId: number) {
|
||||
return axios.get<Result<{ dimensions: DrillDownDimensionType[] }>>(`${prefix}/chat/recommend/metric/${modelId}`);
|
||||
return axios.get<{ dimensions: DrillDownDimensionType[] }>(`${prefix}/chat/recommend/metric/${modelId}`);
|
||||
}
|
||||
|
||||
export function queryDimensionValues(modelId: number, bizName: string, value: string) {
|
||||
return axios.post<Result<any>>(`${prefix}/chat/query/queryDimensionValue`, { modelId, bizName, value});
|
||||
return axios.post<any>(`${prefix}/chat/query/queryDimensionValue`, { modelId, bizName, value});
|
||||
}
|
||||
|
||||
19
webapp/packages/chat-sdk/src/typings.d.ts
vendored
19
webapp/packages/chat-sdk/src/typings.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
declare module 'slash2';
|
||||
declare module '*.css';
|
||||
declare module '*.less';
|
||||
declare module '*.module.less';
|
||||
declare module '*.scss';
|
||||
declare module '*.sass';
|
||||
declare module '*.svg';
|
||||
@@ -18,12 +19,22 @@ declare module 'react-fittext';
|
||||
declare module 'bizcharts-plugin-slider';
|
||||
declare module 'react-split-pane/lib/Pane';
|
||||
|
||||
// preview.pro.ant.design only do not use in your production ;
|
||||
// preview.pro.ant.design Dedicated environment variable, please do not use it in your project.
|
||||
declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined;
|
||||
|
||||
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;
|
||||
|
||||
declare module '*.module.less' {
|
||||
const classes: {
|
||||
readonly [key: string]: string
|
||||
}
|
||||
export default classes
|
||||
declare module '*.less'
|
||||
}
|
||||
|
||||
interface AxiosResponse<T = any> extends Promise<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
type Result<T> = {
|
||||
code: number;
|
||||
data: T;
|
||||
|
||||
@@ -178,9 +178,76 @@ export function isProd() {
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
localStorage.setItem('SUPERSONIC_CHAT_TOKEN', token);
|
||||
localStorage.setItem('SUPERSONIC_TOKEN', token);
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem('SUPERSONIC_CHAT_TOKEN');
|
||||
return localStorage.getItem('SUPERSONIC_TOKEN');
|
||||
}
|
||||
|
||||
export const updateMessageContainerScroll = (nodeId?: string) => {
|
||||
setTimeout(() => {
|
||||
const ele: any = document.getElementById('messageContainer');
|
||||
if (ele && ele.scrollHeight > ele.clientHeight) {
|
||||
if (nodeId) {
|
||||
const node = document.getElementById(nodeId);
|
||||
if (node) {
|
||||
ele.scrollTop = ele.scrollHeight - node.clientHeight - 130;
|
||||
}
|
||||
} else {
|
||||
ele.scrollTop = ele.scrollHeight;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* UUID生成器
|
||||
* @param len 长度 number
|
||||
* @param radix 随机数基数 number
|
||||
* @returns {string}
|
||||
*/
|
||||
export const uuid = (len: number = 8, radix: number = 62) => {
|
||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
|
||||
const uuid: string[] = [];
|
||||
let i;
|
||||
|
||||
if (len) {
|
||||
// Compact form
|
||||
for (i = 0; i < len; i++) {
|
||||
uuid[i] = chars[Math.floor(Math.random() * radix)];
|
||||
}
|
||||
} else {
|
||||
// rfc4122, version 4 form
|
||||
let r;
|
||||
|
||||
// rfc4122 requires these characters
|
||||
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
|
||||
uuid[14] = '4';
|
||||
|
||||
// Fill in random data. At i==19 set the high bits of clock sequence as
|
||||
// per rfc4122, sec. 4.1.5
|
||||
for (i = 0; i < 36; i++) {
|
||||
if (!uuid[i]) {
|
||||
r = Math.floor(Math.random() * 16);
|
||||
uuid[i] = chars[i === 19 ? ((r % 4) % 8) + 8 : r];
|
||||
}
|
||||
}
|
||||
}
|
||||
return uuid.join('');
|
||||
};
|
||||
|
||||
let utilCanvas: any = null;
|
||||
|
||||
export const getTextWidth = (
|
||||
text: string,
|
||||
fontSize: string = '16px',
|
||||
fontWeight: string = 'normal',
|
||||
fontFamily: string = 'DINPro Medium',
|
||||
): number => {
|
||||
const canvas = utilCanvas || (utilCanvas = document.createElement('canvas'));
|
||||
const context = canvas.getContext('2d');
|
||||
context.font = `${fontWeight} ${fontSize} ${fontFamily}`;
|
||||
const metrics = context.measureText(text);
|
||||
return Math.ceil(metrics.width);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution":"Node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"importHelpers": true,
|
||||
"importHelpers": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
Reference in New Issue
Block a user