Integrate Chat and Copilot into chat-sdk, and add SQL parse display (#166)

This commit is contained in:
williamhliu
2023-10-02 18:05:12 +08:00
committed by GitHub
parent 741ed4191b
commit 71cb20eb4f
68 changed files with 1353 additions and 882 deletions

View File

@@ -1,4 +1,4 @@
const ENV_CONFIG = {
tmeAvatarUrl: 'http://tpp.tmeoa.com/photo/48/',
tmeAvatarUrl: '',
};
export default ENV_CONFIG;

View File

@@ -11,7 +11,7 @@ const ROUTES = [
{
path: '/chat/mobile',
name: 'chat',
component: './Chat',
component: './ChatPage',
hideInMenu: true,
layout: false,
envEnableList: [ENV_KEY.CHAT],
@@ -19,7 +19,7 @@ const ROUTES = [
{
path: '/chat',
name: 'chat',
component: './Chat',
component: './ChatPage',
envEnableList: [ENV_KEY.CHAT],
},
{

View File

@@ -1,23 +1,18 @@
import { AUTH_TOKEN_KEY, FROM_URL_KEY } from '@/common/constants';
import RightContent from '@/components/RightContent';
import S2Icon, { ICON } from '@/components/S2Icon';
import type { Settings as LayoutSettings } from '@ant-design/pro-layout';
import { Space, Spin } from 'antd';
import qs from 'qs';
import ScaleLoader from 'react-spinners/ScaleLoader';
import type { RunTimeLayoutConfig } from 'umi';
import { history } from 'umi';
import defaultSettings from '../config/defaultSettings';
import settings from '../config/themeSettings';
import { queryToken } from './services/login';
import { queryCurrentUser } from './services/user';
import { traverseRoutes, deleteUrlQuery, isMobile } from './utils/utils';
import { traverseRoutes, isMobile, getToken } from './utils/utils';
import { publicPath } from '../config/defaultSettings';
import Copilot from './pages/Copilot';
import { Copilot } from 'supersonic-chat-sdk';
export { request } from './services/request';
const TOKEN_KEY = AUTH_TOKEN_KEY;
const replaceRoute = '/';
const getRunningEnv = async () => {
@@ -42,25 +37,6 @@ export const initialStateConfig = {
),
};
const getToken = async () => {
let { search } = window.location;
if (search.length > 0) {
search = search.slice(1);
}
const data = qs.parse(search);
if (data.code) {
try {
const fromUrl = localStorage.getItem(FROM_URL_KEY);
const res = await queryToken(data.code as string);
localStorage.setItem(TOKEN_KEY, res.payload);
const newUrl = deleteUrlQuery(window.location.href, 'code');
window.location.href = fromUrl || newUrl;
} catch (err) {
console.log(err);
}
}
};
const getAuthCodes = () => {
return [];
};
@@ -81,12 +57,6 @@ export async function getInitialState(): Promise<{
} catch (error) {}
return undefined;
};
const { query } = history.location as any;
const currentToken = query[TOKEN_KEY] || localStorage.getItem(TOKEN_KEY);
if (window.location.host.includes('tmeoa') && !currentToken) {
await getToken();
}
let currentUser: any;
if (!window.location.pathname.includes('login')) {
@@ -162,7 +132,9 @@ export const layout: RunTimeLayoutConfig = (params) => {
style={{ height: location.pathname.includes('chat') ? 'calc(100vh - 48px)' : undefined }}
>
{dom}
{history.location.pathname !== '/chat' && !isMobile && <Copilot />}
{history.location.pathname !== '/chat' && !isMobile && (
<Copilot token={getToken() || ''} isDeveloper />
)}
</div>
);
},

View File

@@ -1,52 +0,0 @@
import { PlusCircleOutlined } from '@ant-design/icons';
import { AgentType } from '../type';
import styles from './style.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;

View File

@@ -1,80 +0,0 @@
.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;
}
}
}
}
}

View File

@@ -1,387 +0,0 @@
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 { searchRecommend } from 'supersonic-chat-sdk';
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
import styles from './style.less';
import { AgentType, ModelType } from '../type';
type Props = {
inputMsg: string;
chatId?: number;
currentAgent?: AgentType;
agentList: AgentType[];
onToggleHistoryVisible: () => void;
onOpenMobileAgents: () => 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,
onOpenMobileAgents,
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.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;
} else {
setOpen(false);
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) {
sendMsg(value);
} else {
const agent = agentList.find((item) => value.includes(item.name));
if (agent) {
if (agent.id !== currentAgent?.id) {
onSelectAgent(agent);
}
onInputMsgChange('');
}
}
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}
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>
)}
{isMobile && (
<div className={styles.toolItem} onClick={onOpenMobileAgents}>
<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}
backfill
ref={inputRef}
id="chatInput"
onKeyDown={(e) => {
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
{
const chatInputEl: any = document.getElementById('chatInput');
if (!isSelect) {
sendMsg(chatInputEl.value);
setOpen(false);
} else {
const agent = agentList.find((item) => chatInputEl.value.includes(item.name));
if (agent) {
if (agent.id !== currentAgent?.id) {
onSelectAgent(agent);
}
onInputMsgChange('');
}
}
}
}
}}
onFocus={() => {
setFocused(true);
}}
onBlur={() => {
setFocused(false);
}}
dropdownClassName={autoCompleteDropdownClass}
listHeight={500}
allowClear
open={open}
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);

View File

@@ -1,224 +0,0 @@
.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 16px;
}
.ant-select-selection-search {
right: 0 !important;
left: 0 !important;
}
.ant-select-selection-placeholder {
padding-left: 6px;
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);
}

View File

@@ -1,280 +0,0 @@
import { Dropdown, Input, Menu } from 'antd';
import classNames from 'classnames';
import {
useEffect,
useState,
forwardRef,
ForwardRefRenderFunction,
useImperativeHandle,
} from 'react';
import { useLocation } from 'umi';
import ConversationModal from '../components/ConversationModal';
import { deleteConversation, getAllConversations, saveConversation } from '../service';
import styles from './style.less';
import { AgentType, ConversationDetailType, DefaultEntityType } from '../type';
import { DEFAULT_CONVERSATION_NAME } from '../constants';
import moment from 'moment';
import { CloseOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
type Props = {
agentList?: AgentType[];
currentAgent?: AgentType;
currentConversation?: ConversationDetailType;
historyVisible?: boolean;
isCopilotMode?: boolean;
defaultEntityFilter?: DefaultEntityType;
triggerNewConversation?: boolean;
onNewConversationTriggered?: () => void;
onSelectConversation: (
conversation: ConversationDetailType,
name?: string,
modelId?: number,
entityId?: string,
agent?: AgentType,
) => void;
onCloseConversation: () => void;
};
const Conversation: ForwardRefRenderFunction<any, Props> = (
{
agentList,
currentAgent,
currentConversation,
historyVisible,
isCopilotMode,
defaultEntityFilter,
triggerNewConversation,
onNewConversationTriggered,
onSelectConversation,
onCloseConversation,
},
ref,
) => {
const location = useLocation();
const { q, cid, modelId, entityId } = (location as any).query;
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 (agent?: AgentType) => {
const { data } = await getAllConversations(agent?.id || currentAgent?.id);
const conversationList = data || [];
setConversations(conversationList.slice(0, 200));
return conversationList;
};
const initData = async () => {
const data = await updateData();
if (data.length > 0) {
const chatId = cid;
if (chatId) {
const conversation = data.find((item: any) => item.chatId === +chatId);
onSelectConversation(conversation || data[0]);
} else {
onSelectConversation(data[0]);
}
} else {
onAddConversation();
}
};
// useEffect(() => {
// if (triggerNewConversation) {
// return;
// }
// if (q && cid === undefined && window.location.href.includes('/chat')) {
// onAddConversation({
// name: q,
// modelId: modelId ? +modelId : undefined,
// entityId,
// });
// } else {
// initData();
// }
// }, [q]);
useEffect(() => {
if (currentAgent && !triggerNewConversation) {
initData();
}
}, [currentAgent]);
const addConversation = async (name?: string, agent?: AgentType) => {
await saveConversation(name || DEFAULT_CONVERSATION_NAME, agent?.id || currentAgent!.id);
return updateData(agent);
};
const onDeleteConversation = async (id: number) => {
await deleteConversation(id);
initData();
};
const onAddConversation = async ({
name,
modelId,
entityId,
type,
agent,
}: {
name?: string;
modelId?: number;
entityId?: string;
type?: string;
agent?: AgentType;
} = {}) => {
const data = await addConversation(name, agent);
if (data.length > 0) {
onSelectConversation(
data[0],
type || name || DEFAULT_CONVERSATION_NAME,
modelId,
entityId,
agent,
);
}
};
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,
[styles.copilotMode]: isCopilotMode,
});
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>
);
};
export default forwardRef(Conversation);

View File

@@ -1,176 +0,0 @@
.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;
}
&.copilotMode {
&.collapsed {
width: 0 !important;
}
}
}

View File

@@ -1,156 +0,0 @@
import Text from '../components/Text';
import { memo, useCallback, useEffect, useState } from 'react';
import { isEqual } from 'lodash';
import { ChatItem } from 'supersonic-chat-sdk';
import type { MsgDataType } from 'supersonic-chat-sdk';
import { AgentType, MessageItem, MessageTypeEnum } from '../type';
import { isMobile, updateMessageContainerScroll } from '@/utils/utils';
import styles from './style.less';
import AgentTip from '../components/AgentTip';
import classNames from 'classnames';
type Props = {
id: string;
chatId: number;
messageList: MessageItem[];
historyVisible: boolean;
currentAgent?: AgentType;
chatVisible?: boolean;
onMsgDataLoaded: (
data: MsgDataType,
questionId: string | number,
question: string,
valid: boolean,
) => void;
onApplyAuth: (model: string) => void;
onSendMsg: (value: string) => void;
};
const MessageContainer: React.FC<Props> = ({
id,
chatId,
messageList,
historyVisible,
currentAgent,
chatVisible,
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 getFilters = (modelId?: number, entityId?: string) => {
if (!modelId || !entityId) {
return undefined;
}
return [
{
value: entityId,
},
];
};
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,
entityId,
type,
msg,
msgValue,
identityMsg,
msgData,
score,
isHistory,
parseOptions,
} = 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={getFilters(modelId, entityId)}
isLastMessage={index === messageList.length - 1}
isHistory={isHistory}
triggerResize={triggerResize}
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
onMsgDataLoaded(data, msgId, msgValue || msg || '', valid);
}}
onUpdateMessageScroll={updateMessageContainerScroll}
/>
</>
)}
{type === MessageTypeEnum.PARSE_OPTIONS && (
<ChatItem
msg={msgValue || msg || ''}
conversationId={chatId}
modelId={modelId}
agentId={agentId}
filter={getFilters(modelId, entityId)}
isLastMessage={index === messageList.length - 1}
isHistory={isHistory}
triggerResize={triggerResize}
parseOptions={parseOptions}
onMsgDataLoaded={(data: MsgDataType, valid: boolean) => {
onMsgDataLoaded(data, msgId, msgValue || msg || '', valid);
}}
onUpdateMessageScroll={updateMessageContainerScroll}
/>
)}
</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);

View File

@@ -1,148 +0,0 @@
.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;
}
}
}

View File

@@ -1,62 +0,0 @@
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.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;

View File

@@ -1,55 +0,0 @@
.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;
}
}
}
}

View File

@@ -1,48 +0,0 @@
import LeftAvatar from '../CopilotAvatar';
import Message from '../Message';
import styles from './style.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;

View File

@@ -1,44 +0,0 @@
.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;
}
}
}
}

View File

@@ -1,66 +0,0 @@
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}问答名称`}
visible={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;

View File

@@ -1,8 +0,0 @@
import IconFont from '@/components/IconFont';
import styles from './style.less';
const CopilotAvatar = () => {
return <IconFont type="icon-zhinengsuanfa" className={styles.leftAvatar} />;
};
export default CopilotAvatar;

View File

@@ -1,13 +0,0 @@
.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%;
}

View File

@@ -1,36 +0,0 @@
import classNames from 'classnames';
import styles from './style.less';
type Props = {
position: 'left' | 'right';
width?: number | string;
height?: number | string;
bubbleClassName?: string;
};
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;

View File

@@ -1,65 +0,0 @@
import { useEffect, useState } from 'react';
import LeftAvatar from '../CopilotAvatar';
import Message from '../Message';
import styles from './style.less';
import { queryRecommendQuestions } from '../../service';
import Typing from '../Typing';
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 ? (
<Typing />
) : 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;

View File

@@ -1,36 +0,0 @@
.recommendQuestions {
display: flex;
.recommendQuestionsMsg {
padding: 12px 20px 20px !important;
.title {
font-size: 14px;
font-weight: 500;
margin-bottom: 12px;
}
.content {
display: flex;
align-items: center;
flex-wrap: wrap;
column-gap: 16px;
row-gap: 20px;
.question {
padding: 0 6px;
height: 22px;
line-height: 22px;
font-size: 12px;
color: var(--text-color);
border-radius:11px;
background-color: #f4f4f4;
cursor: pointer;
&:hover {
color: var(--chat-blue);
}
}
}
}
}

View File

@@ -1,27 +0,0 @@
import classNames from 'classnames';
import LeftAvatar from './CopilotAvatar';
import Message from './Message';
import styles from './style.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}>
{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;

View File

@@ -1,19 +0,0 @@
import { CHAT_BLUE } from '@/common/constants';
import { Spin } from 'antd';
import BeatLoader from 'react-spinners/BeatLoader';
import Message from './Message';
import styles from './style.less';
const Typing = () => {
return (
<Message position="left" bubbleClassName={styles.typingBubble}>
<Spin
spinning={true}
indicator={<BeatLoader color={CHAT_BLUE} size={10} />}
className={styles.typing}
/>
</Message>
);
};
export default Typing;

View File

@@ -1,311 +0,0 @@
.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;
}
}

View File

@@ -1,52 +0,0 @@
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 CHAT_TITLE = '';
export const DEFAULT_CONVERSATION_NAME = '新问答对话';
export const PAGE_TITLE = '问答对话';
export const WEB_TITLE = '问答对话';
export const PLACE_HOLDER = '请输入您的问题';

View File

@@ -1,444 +0,0 @@
import { updateMessageContainerScroll, isMobile, uuid } from '@/utils/utils';
import { useEffect, useRef, useState } from 'react';
import { Helmet, useDispatch } from 'umi';
import MessageContainer from './MessageContainer';
import styles from './style.less';
import {
ConversationDetailType,
DefaultEntityType,
ModelType,
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, DEFAULT_CONVERSATION_NAME, WEB_TITLE } from './constants';
import { HistoryMsgItemType, MsgDataType, getHistoryMsg } from 'supersonic-chat-sdk';
import { cloneDeep } from 'lodash';
import 'supersonic-chat-sdk/dist/index.css';
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
import AgentList from './AgentList';
import { AUTH_TOKEN_KEY } from '@/common/constants';
import MobileAgents from './MobileAgents';
type Props = {
isCopilotMode?: boolean;
defaultModelName?: string;
defaultEntityFilter?: DefaultEntityType;
copilotSendMsg?: string;
triggerNewConversation?: boolean;
chatVisible?: boolean;
onNewConversationTriggered?: () => void;
onCurrentModelChange?: (model?: ModelType) => void;
};
const Chat: React.FC<Props> = ({
isCopilotMode,
defaultModelName,
defaultEntityFilter,
copilotSendMsg,
triggerNewConversation,
chatVisible,
onNewConversationTriggered,
}) => {
const isMobileMode = isMobile || isCopilotMode;
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 [defaultEntity, setDefaultEntity] = useState<DefaultEntityType>();
const [agentList, setAgentList] = useState<AgentType[]>([]);
const [currentAgent, setCurrentAgent] = useState<AgentType>();
const [mobileAgentsVisible, setMobileAgentsVisible] = useState(false);
const dispatch = useDispatch();
const conversationRef = useRef<any>();
const chatFooterRef = useRef<any>();
const initAgentList = async () => {
const res = await queryAgentList();
const agentListValue = (res.data || []).filter((item) => item.status === 1);
setAgentList(agentListValue);
if (agentListValue.length > 0) {
setCurrentAgent(agentListValue[0]);
}
};
useEffect(() => {
setChatSdkToken(localStorage.getItem(AUTH_TOKEN_KEY) || '');
initAgentList();
}, []);
useEffect(() => {
if (chatVisible) {
updateMessageContainerScroll();
}
}, [chatVisible]);
useEffect(() => {
if (triggerNewConversation) {
setCurrentAgent(agentList?.find((item) => item.name === '做分析'));
conversationRef.current?.onAddConversation({
type: 'CUSTOMIZE',
modelId: defaultEntityFilter?.modelId,
entityId: defaultEntityFilter?.entityId,
agent: agentList?.find((item) => item.name === '做分析'),
});
setTimeout(() => {
onNewConversationTriggered?.();
}, 200);
}
}, [triggerNewConversation]);
useEffect(() => {
if (!currentConversation) {
return;
}
const { initMsg, modelId, entityId, agent } = currentConversation;
if (initMsg) {
inputFocus();
if (initMsg === 'CUSTOMIZE' && copilotSendMsg) {
onSendMsg(
copilotSendMsg,
[],
modelId,
entityId,
agentList.find((item) => item.name === '做分析'),
);
dispatch({
type: 'globalState/setCopilotSendMsg',
payload: '',
});
return;
}
if (initMsg === DEFAULT_CONVERSATION_NAME || initMsg.includes('CUSTOMIZE')) {
if (agent) {
setCurrentAgent(agent);
}
sendHelloRsp(agent);
return;
}
onSendMsg(
initMsg,
[],
modelId,
entityId,
initMsg.includes('商业线索') ? agentList.find((item) => item.name === '做分析') : undefined,
);
return;
}
updateHistoryMsg(1);
setPageNo(1);
}, [currentConversation]);
useEffect(() => {
setDefaultEntity(defaultEntityFilter);
}, [defaultEntityFilter]);
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) => {
setMessageList([
{
id: uuid(),
type: MessageTypeEnum.AGENT_LIST,
msg: agent?.name || currentAgent?.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,
isHistory: true,
}));
};
const updateHistoryMsg = async (page: number) => {
const res = await getHistoryMsg(page, currentConversation!.chatId, 3);
const { hasNextPage, list } = res.data?.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,
entityId?: string,
agent?: AgentType,
) => {
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;
if (agent || certainAgent) {
setCurrentAgent(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,
entityId: entityId || defaultEntity?.entityId,
type: MessageTypeEnum.QUESTION,
},
];
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,
name?: string,
modelId?: number,
entityId?: string,
agent?: AgentType,
) => {
if (!isMobileMode) {
window.history.replaceState('', '', `?q=${conversation.chatName}&cid=${conversation.chatId}`);
}
setCurrentConversation({
...conversation,
initMsg: name,
modelId,
entityId,
agent,
});
saveConversationToLocal(conversation);
};
const reportMsgEvent = (msg: string, valid: boolean) => {};
const onMsgDataLoaded = (
data: MsgDataType,
questionId: string | number,
question: string,
valid: boolean,
) => {
reportMsgEvent(question, valid);
if (!isMobile) {
conversationRef?.current?.updateData();
}
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 onApplyAuth = (model: string) => {};
const onAddConversation = (agent?: AgentType) => {
conversationRef.current?.onAddConversation({ agent });
inputFocus();
};
const onSelectAgent = (agent: AgentType) => {
if (agent.id === currentAgent?.id) {
return;
}
setCurrentAgent(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}>
{!isMobileMode && <Helmet title={WEB_TITLE} />}
<div className={styles.chatSection}>
{!isMobile && (
<AgentList
agentList={agentList}
currentAgent={currentAgent}
onSelectAgent={onSelectAgent}
/>
)}
<div className={styles.chatApp}>
{currentConversation && (
<div className={styles.chatBody}>
<div className={styles.chatContent}>
{currentAgent && !isMobile && (
<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}
onMsgDataLoaded={onMsgDataLoaded}
onApplyAuth={onApplyAuth}
onSendMsg={onSendMsg}
/>
<ChatFooter
inputMsg={inputMsg}
chatId={currentConversation?.chatId}
agentList={agentList}
currentAgent={currentAgent}
onToggleHistoryVisible={onToggleHistoryVisible}
onInputMsgChange={onInputMsgChange}
onSendMsg={sendMsg}
onAddConversation={onAddConversation}
onSelectAgent={onSelectAgent}
onOpenMobileAgents={() => {
setMobileAgentsVisible(true);
}}
ref={chatFooterRef}
/>
</div>
</div>
)}
</div>
<Conversation
agentList={agentList}
currentAgent={currentAgent}
currentConversation={currentConversation}
historyVisible={historyVisible}
isCopilotMode={isCopilotMode}
defaultEntityFilter={defaultEntityFilter}
triggerNewConversation={triggerNewConversation}
onNewConversationTriggered={onNewConversationTriggered}
onSelectConversation={onSelectConversation}
onCloseConversation={onCloseConversation}
ref={conversationRef}
/>
</div>
<MobileAgents
open={mobileAgentsVisible}
agentList={agentList}
currentAgent={currentAgent}
onSelectAgent={onSelectAgent}
onClose={() => {
setMobileAgentsVisible(false);
}}
/>
</div>
);
};
export default Chat;

View File

@@ -1,78 +0,0 @@
import { isMobile } from '@/utils/utils';
import { request } from 'umi';
import { AgentType, ModelType } from './type';
const prefix = isMobile ? '/openapi' : '/api';
export function saveConversation(chatName: string, agentId: number) {
return request<Result<any>>(
`${prefix}/chat/manage/save?chatName=${chatName}&agentId=${agentId}`,
{
method: 'POST',
},
);
}
export function updateConversationName(chatName: string, chatId: number = 0) {
return request<Result<any>>(
`${prefix}/chat/manage/updateChatName?chatName=${chatName}&chatId=${chatId}`,
{ method: 'POST' },
);
}
export function deleteConversation(chatId: number) {
return request<Result<any>>(`${prefix}/chat/manage/delete?chatId=${chatId}`, { method: 'POST' });
}
export function getAllConversations(agentId?: number) {
return request<Result<any>>(`${prefix}/chat/manage/getAll`, { params: { agentId } });
}
export function getMiniProgramList(entityId: string, modelId: number) {
return request<Result<any>>(
`${prefix}/chat/plugin/extend/getAvailablePlugin/${entityId}/${modelId}`,
{
method: 'GET',
skipErrorHandler: true,
},
);
}
export function getModelList() {
return request<Result<ModelType[]>>(`${prefix}/chat/conf/modelList/view`, {
method: 'GET',
});
}
export function updateQAFeedback(questionId: number, score: number) {
return request<Result<any>>(
`${prefix}/chat/manage/updateQAFeedback?id=${questionId}&score=${score}&feedback=`,
{
method: 'POST',
},
);
}
export function queryMetricSuggestion(modelId: number) {
return request<Result<any>>(`${prefix}/chat/recommend/metric/${modelId}`, {
method: 'GET',
});
}
export function querySuggestion(modelId: number) {
return request<Result<any>>(`${prefix}/chat/recommend/${modelId}`, {
method: 'GET',
});
}
export function queryRecommendQuestions() {
return request<Result<any>>(`${prefix}/chat/recommend/question`, {
method: 'GET',
});
}
export function queryAgentList() {
return request<Result<AgentType[]>>(`${prefix}/chat/agent/getAgentList`, {
method: 'GET',
});
}

View File

@@ -1,93 +0,0 @@
@import '~antd/es/style/themes/default.less';
.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: 999;
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;
}
}
}

View File

@@ -1,95 +0,0 @@
import { ChatContextType, MsgDataType } from 'supersonic-chat-sdk';
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;
isHistory?: boolean;
parseOptions?: ChatContextType[];
};
export type ConversationDetailType = {
chatId: number;
chatName: string;
createTime?: string;
creator?: string;
lastQuestion?: string;
lastTime?: string;
initMsg?: string;
modelId?: number;
entityId?: string;
agent?: AgentType;
};
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 DefaultEntityType = {
entityId: string;
entityName: string;
modelName?: string;
modelId?: number;
};
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;
};

View File

@@ -0,0 +1,14 @@
import { useLocation } from 'umi';
import { getToken } from '@/utils/utils';
import { Chat } from 'supersonic-chat-sdk';
const ChatPage = () => {
const location = useLocation();
const { agentId } = (location as any).query;
return (
<Chat initialAgentId={agentId ? +agentId : undefined} token={getToken() || ''} isDeveloper />
);
};
export default ChatPage;

View File

@@ -1,11 +0,0 @@
export const MODEL_PATH_MAP = {
song: '歌曲库',
'song-detail': '歌曲库',
singer: '艺人库',
'singer-detail': '艺人库',
album: '专辑库',
'album-detail': '专辑库',
'digital-album': '专辑库',
brand: '厂牌库',
'brand-detail': '厂牌库',
};

View File

@@ -1,119 +0,0 @@
import IconFont from '@/components/IconFont';
import { CaretRightOutlined, CloseOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import Chat from '../Chat';
import { DefaultEntityType, ModelType } from '../Chat/type';
import styles from './style.less';
type Props = {
globalCopilotFilter: DefaultEntityType;
copilotSendMsg: string;
};
const Copilot: React.FC<Props> = ({ globalCopilotFilter, copilotSendMsg }) => {
const [chatVisible, setChatVisible] = useState(false);
const [defaultModelName, setDefaultModelName] = useState('');
const [defaultEntityFilter, setDefaultEntityFilter] = useState<DefaultEntityType>();
const [triggerNewConversation, setTriggerNewConversation] = useState(false);
const [copilotMinimized, setCopilotMinimized] = useState(false);
useEffect(() => {
if (globalCopilotFilter && globalCopilotFilter.entityId !== defaultEntityFilter?.entityId) {
setTriggerNewConversation(true);
}
setDefaultEntityFilter(globalCopilotFilter);
if (globalCopilotFilter?.modelName) {
setDefaultModelName(globalCopilotFilter.modelName);
}
}, [globalCopilotFilter]);
useEffect(() => {
if (copilotSendMsg) {
updateChatVisible(true);
setTriggerNewConversation(true);
}
}, [copilotSendMsg]);
const updateChatVisible = (visible: boolean) => {
setChatVisible(visible);
};
const onToggleChatVisible = () => {
const chatVisibleValue = !chatVisible;
updateChatVisible(chatVisibleValue);
};
const onCloseChat = () => {
updateChatVisible(false);
};
const onTransferChat = () => {
window.open('/chat');
};
const onCurrentModelChange = (model?: ModelType) => {
setDefaultModelName(model?.name || '');
};
const onNewConversationTriggered = () => {
setTriggerNewConversation(false);
};
const onMinimizeCopilot = (e: any) => {
e.stopPropagation();
updateChatVisible(false);
setCopilotMinimized(true);
};
const copilotClass = classNames(styles.copilot, {
[styles.copilotMinimized]: copilotMinimized,
});
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={styles.chatPopover}>
<div className={styles.header}>
<div className={styles.leftSection}>
<CloseOutlined className={styles.close} onClick={onCloseChat} />
<IconFont
type="icon-weibiaoti-"
className={styles.transfer}
onClick={onTransferChat}
/>
</div>
<div className={styles.title}>Copilot</div>
</div>
<div className={styles.chat}>
<Chat
copilotSendMsg={copilotSendMsg}
defaultModelName={defaultModelName}
defaultEntityFilter={defaultEntityFilter}
triggerNewConversation={triggerNewConversation}
chatVisible={chatVisible}
isCopilotMode
onNewConversationTriggered={onNewConversationTriggered}
onCurrentModelChange={onCurrentModelChange}
/>
</div>
</div>
<CaretRightOutlined className={styles.rightArrow} />
</div>
</>
);
};
export default Copilot;

View File

@@ -1,147 +0,0 @@
.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 {
display: none;
cursor: pointer;
width: 22px;
height: 22px;
position: absolute;
right: -6px;
top: -18px;
padding: 4px;
.minimize {
width: 100%;
height: 100%;
padding-bottom: 5px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: var(--text-color-fifth-4);
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;
.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;
}

View File

@@ -1,5 +1,3 @@
import { request } from 'umi';
export type LoginParamsType = {
username: string;
password: string;
@@ -7,9 +5,3 @@ export type LoginParamsType = {
captcha: string;
type: string;
};
export async function queryToken(code: string) {
return request(`/davinciapi/login/tmeloginCallback`, {
params: { code },
});
}