|
|
|
@ -1,9 +1,9 @@
|
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
|
|
|
import { App, Popover, Flex, Button, List, Input } from 'antd';
|
|
|
|
|
import { useState, useRef, useEffect, memo, useMemo, useCallback } from 'react';
|
|
|
|
|
import { App, Popover, Flex, Button, List, Input, Tabs, Tag, Alert } from 'antd';
|
|
|
|
|
import { MessageOutlined, SendOutlined } from '@ant-design/icons';
|
|
|
|
|
import useAuthStore from '@/stores/AuthStore';
|
|
|
|
|
import useConversationStore from '@/stores/ConversationStore';
|
|
|
|
|
import { cloneDeep, getNestedValue, objectMapper, removeFormattingChars, sortArrayByOrder } from '@/utils/commons';
|
|
|
|
|
import { cloneDeep, getNestedValue, groupBy, objectMapper, removeFormattingChars, sortArrayByOrder, TagColorStyle } from '@/utils/commons';
|
|
|
|
|
import { replaceTemplateString } from '@/channel/bubbleMsgUtils';
|
|
|
|
|
import { isEmpty } from '@/utils/commons';
|
|
|
|
|
import useStyleStore from '@/stores/StyleStore';
|
|
|
|
@ -22,6 +22,121 @@ const splitTemplate = (template) => {
|
|
|
|
|
}, []);
|
|
|
|
|
return obj;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// UTILITY
|
|
|
|
|
// MARKETING
|
|
|
|
|
const templateCaterogyText = { 'UTILITY': '跟进', 'MARKETING': '营销' }
|
|
|
|
|
|
|
|
|
|
const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, activeInput }) => {
|
|
|
|
|
const currentConversation = useConversationStore((state) => state.currentConversation);
|
|
|
|
|
const renderForm = ({ tempItem }, key = 'body') => {
|
|
|
|
|
const templateText = tempItem.components?.[key]?.[0]?.text || '';
|
|
|
|
|
const tempArr = splitTemplate(templateText);
|
|
|
|
|
const keys = (templateText.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''));
|
|
|
|
|
const paramsVal = keys.reduce((r, k) => ({ ...r, [k]: getNestedValue(valueMapped, [k]) }), {});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{tempArr.map((ele) =>
|
|
|
|
|
typeof ele === 'string' ? (
|
|
|
|
|
<span key={ele.trim()} className=' text-wrap'>
|
|
|
|
|
{ele.replace(/\n+/g, '\n')}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<Input
|
|
|
|
|
key={ele.key}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
onInput(tempItem, ele.key, e.target.value, paramsVal);
|
|
|
|
|
}}
|
|
|
|
|
className={ele.key.includes('free') || ele.key.includes('detail') ? `w-full block ` : `w-auto ${paramsVal[ele.key] ? 'max-w-24' : 'max-w-60'}`}
|
|
|
|
|
size={'small'}
|
|
|
|
|
title={ele.key}
|
|
|
|
|
placeholder={`${paramsVal[ele.key] || ele.key} 按Tab键跳到下一个空格`}
|
|
|
|
|
value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''}
|
|
|
|
|
// onPressEnter={() => handleSendTemplate(tempItem)}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
const renderHeader = ({ tempItem }) => {
|
|
|
|
|
if (isEmpty(tempItem.components.header)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const headerObj = tempItem.components.header[0];
|
|
|
|
|
return (
|
|
|
|
|
<div className='pb-1'>
|
|
|
|
|
{'text' === headerObj.format.toLowerCase() && <div>{renderForm({ tempItem }, 'header')}</div>}
|
|
|
|
|
{'image' === headerObj.format.toLowerCase() && <img src={headerObj.example.header_url} height={100}></img>}
|
|
|
|
|
{['document', 'video'].includes(headerObj.format.toLowerCase()) && (
|
|
|
|
|
<a href={headerObj.example.header_url} target='_blank' key={headerObj.format} rel='noreferrer' className='text-sm'>
|
|
|
|
|
[ {headerObj.format} ]({headerObj.example.header_url})
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const renderButtons = ({ tempItem }) => {
|
|
|
|
|
if (isEmpty(tempItem.components.buttons)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const buttons = tempItem.components.buttons.reduce((r, c) => r.concat(c.buttons), []);
|
|
|
|
|
return (
|
|
|
|
|
<div className='flex gap-1 pt-1'>
|
|
|
|
|
{buttons.map((btn, index) =>
|
|
|
|
|
btn.type.toLowerCase() === 'url' ? (
|
|
|
|
|
<Button className='text-blue-500' size={'small'} href={btn.url} target={'_blank'} key={btn.url} rel='noreferrer'>
|
|
|
|
|
{btn.text}
|
|
|
|
|
</Button>
|
|
|
|
|
) : btn.type.toLowerCase() === 'phone_number' ? (
|
|
|
|
|
<Button className='text-blue-500' size={'small'} key={btn.phone_number} rel='noreferrer'>
|
|
|
|
|
{btn.text} ({btn.phone_number})
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button className='text-blue-500' size={'small'} key={btn.type} rel='noreferrer'>
|
|
|
|
|
{btn.text}
|
|
|
|
|
</Button>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<List
|
|
|
|
|
className=' h-[90%] overflow-y-auto text-slate-900'
|
|
|
|
|
itemLayout='horizontal'
|
|
|
|
|
dataSource={dataSource}
|
|
|
|
|
rowKey={'name'}
|
|
|
|
|
pagination={dataSource.length < 4 ? false : { position: 'bottom', pageSize: 3, align: 'start', size: 'small' }}
|
|
|
|
|
renderItem={(item, index) => (
|
|
|
|
|
<List.Item key={`${currentConversation.sn}_${item.name}`}>
|
|
|
|
|
<List.Item.Meta
|
|
|
|
|
className=' '
|
|
|
|
|
title={
|
|
|
|
|
<Flex justify={'space-between'}>
|
|
|
|
|
<span>{item.components.header?.[0]?.text || item.name}<Tag style={{...TagColorStyle(item.language.toUpperCase(), true)}} className='ml-1'>{item.language.toUpperCase()}</Tag><Tag style={{...TagColorStyle(item.category.toUpperCase(), true)}}>{templateCaterogyText[item.category]}</Tag></span>
|
|
|
|
|
<Button onClick={() => handleSendTemplate(item)} size={'small'} type='link' key={'send'} icon={<SendOutlined />}>
|
|
|
|
|
Send
|
|
|
|
|
</Button>
|
|
|
|
|
</Flex>
|
|
|
|
|
}
|
|
|
|
|
description={
|
|
|
|
|
<>
|
|
|
|
|
<div className=' max-h-32 overflow-y-auto divide-dashed divide-x-0 divide-y divide-gray-300'>
|
|
|
|
|
{renderHeader({ tempItem: item })}
|
|
|
|
|
<div className='text-slate-500 py-1 whitespace-pre-wrap'>{renderForm({ tempItem: item })}</div>
|
|
|
|
|
{item.components?.footer?.[0] ? <div className=''>{item.components.footer[0].text || ''}</div> : null}
|
|
|
|
|
{renderButtons({ tempItem: item })}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</List.Item>
|
|
|
|
|
)}
|
|
|
|
|
/>)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
|
|
|
|
|
const [mobile] = useStyleStore((state) => [state.mobile]);
|
|
|
|
|
|
|
|
|
@ -29,14 +144,26 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
|
|
|
|
|
const { notification } = App.useApp();
|
|
|
|
|
const loginUser = useAuthStore((state) => state.loginUser);
|
|
|
|
|
const { whatsAppBusiness } = loginUser;
|
|
|
|
|
|
|
|
|
|
loginUser.usernameEN = loginUser.accountList[0].OPI_NameEN.split(' ')?.[0] || loginUser.username;
|
|
|
|
|
const currentConversation = useConversationStore((state) => state.currentConversation);
|
|
|
|
|
const templates = useConversationStore((state) => state.templates);
|
|
|
|
|
|
|
|
|
|
const [openTemplates, setOpenTemplates] = useState(false);
|
|
|
|
|
const [dataSource, setDataSource] = useState(templates);
|
|
|
|
|
const [templateCMapped, setTemplateCMapped] = useState({});
|
|
|
|
|
const [templateLangMapped, setTemplateLangMapped] = useState({});
|
|
|
|
|
|
|
|
|
|
const [searchContent, setSearchContent] = useState('');
|
|
|
|
|
|
|
|
|
|
// 用于替换变量: customer, agent
|
|
|
|
|
const valueMapped = { ...cloneDeep(currentConversation), ...objectMapper(loginUser, { usernameEN: [{ key: 'agent_name' }, { key: 'your_name' }, { key: 'your_name1' }, { key: 'your_name2' }] }) };
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setDataSource(templates);
|
|
|
|
|
setDataSource([]);
|
|
|
|
|
// setDataSource(templates);
|
|
|
|
|
const mappedByCategory = groupBy(templates, 'category');
|
|
|
|
|
const mappedByLang = groupBy(templates, 'language');
|
|
|
|
|
setTemplateCMapped(mappedByCategory);
|
|
|
|
|
setTemplateLangMapped(mappedByLang);
|
|
|
|
|
return () => {};
|
|
|
|
|
}, [templates]);
|
|
|
|
|
|
|
|
|
@ -47,9 +174,6 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
|
|
|
|
|
}, [currentConversation.sn])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [openTemplates, setOpenTemplates] = useState(false);
|
|
|
|
|
const [dataSource, setDataSource] = useState(templates);
|
|
|
|
|
const [searchContent, setSearchContent] = useState('');
|
|
|
|
|
const handleSearchTemplates = (val) => {
|
|
|
|
|
if (val.toLowerCase().trim() !== '') {
|
|
|
|
|
const res = templates.filter(
|
|
|
|
@ -58,7 +182,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
|
|
|
|
|
setDataSource(res);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
setDataSource(templates);
|
|
|
|
|
setDataSource([]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSendTemplate = (fromTemplate) => {
|
|
|
|
@ -113,80 +237,10 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderHeader = ({ tempItem }) => {
|
|
|
|
|
if (isEmpty(tempItem.components.header)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const headerObj = tempItem.components.header[0];
|
|
|
|
|
return (
|
|
|
|
|
<div className='pb-1'>
|
|
|
|
|
{'text' === headerObj.format.toLowerCase() && <div>{renderForm({ tempItem }, 'header')}</div>}
|
|
|
|
|
{'image' === headerObj.format.toLowerCase() && <img src={headerObj.example.header_url} height={100}></img>}
|
|
|
|
|
{['document', 'video'].includes(headerObj.format.toLowerCase()) && (
|
|
|
|
|
<a href={headerObj.example.header_url} target='_blank' key={headerObj.format} rel='noreferrer' className='text-sm'>
|
|
|
|
|
[ {headerObj.format} ]({headerObj.example.header_url})
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const renderButtons = ({ tempItem }) => {
|
|
|
|
|
if (isEmpty(tempItem.components.buttons)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const buttons = tempItem.components.buttons.reduce((r, c) => r.concat(c.buttons), []);
|
|
|
|
|
return (
|
|
|
|
|
<div className='flex gap-1 pt-1'>
|
|
|
|
|
{buttons.map((btn, index) =>
|
|
|
|
|
btn.type.toLowerCase() === 'url' ? (
|
|
|
|
|
<Button className='text-blue-500' size={'small'} href={btn.url} target={'_blank'} key={btn.url} rel='noreferrer'>
|
|
|
|
|
{btn.text}
|
|
|
|
|
</Button>
|
|
|
|
|
) : btn.type.toLowerCase() === 'phone_number' ? (
|
|
|
|
|
<Button className='text-blue-500' size={'small'} key={btn.phone_number} rel='noreferrer'>
|
|
|
|
|
{btn.text} ({btn.phone_number})
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button className='text-blue-500' size={'small'} key={btn.type} rel='noreferrer'>
|
|
|
|
|
{btn.text}
|
|
|
|
|
</Button>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderForm = ({ tempItem }, key = 'body') => {
|
|
|
|
|
const templateText = tempItem.components?.[key]?.[0]?.text || '';
|
|
|
|
|
const tempArr = splitTemplate(templateText);
|
|
|
|
|
const keys = (templateText.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''));
|
|
|
|
|
const paramsVal = keys.reduce((r, k) => ({ ...r, [k]: getNestedValue(valueMapped, [k]) }), {});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{tempArr.map((ele) =>
|
|
|
|
|
typeof ele === 'string' ? (
|
|
|
|
|
<span key={ele.trim()} className=' text-wrap'>
|
|
|
|
|
{ele}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<Input
|
|
|
|
|
key={ele.key}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
onInput(tempItem, ele.key, e.target.value, paramsVal);
|
|
|
|
|
}}
|
|
|
|
|
className={ele.key.includes('free') || ele.key.includes('detail') ? `w-full block ` : `w-auto ${paramsVal[ele.key] ? 'max-w-24' : 'max-w-60'}`}
|
|
|
|
|
size={'small'}
|
|
|
|
|
title={ele.key}
|
|
|
|
|
placeholder={paramsVal[ele.key] || ele.key}
|
|
|
|
|
value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''}
|
|
|
|
|
// onPressEnter={() => handleSendTemplate(tempItem)}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Popover
|
|
|
|
@ -210,7 +264,21 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
|
|
|
|
|
/>
|
|
|
|
|
<Button size='small' onClick={() => setOpenTemplates(false)}>×</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<List
|
|
|
|
|
{isEmpty(dataSource) && isEmpty(searchContent) ? (
|
|
|
|
|
<Tabs items={[
|
|
|
|
|
{ key: 'marketing', label: '营销问候', children: <CategoryList key={'utility-templates'} dataSource={templateCMapped?.MARKETING || []} {...{ handleSendTemplate, activeInput, onInput, valueMapped}} />},
|
|
|
|
|
{ key: 'utility', label: '持续跟进', children: <CategoryList key={'utility-templates'} dataSource={templateCMapped?.UTILITY || []} {...{ handleSendTemplate, activeInput, onInput, valueMapped}} />},
|
|
|
|
|
|
|
|
|
|
...(Object.keys(templateLangMapped).map(lang => ({
|
|
|
|
|
key: lang, label: lang.toUpperCase(), children: <CategoryList key={'lang-templates-'+lang} dataSource={templateLangMapped[lang]} {...{ handleSendTemplate, activeInput, onInput, valueMapped}} />
|
|
|
|
|
})))
|
|
|
|
|
]} defaultActiveKey='utility' tabBarExtraContent={{right: <Alert type='info' message='为提升账号质量, 请尽量使用跟进模板' showIcon className='py-0' />}} size='small' />
|
|
|
|
|
) :
|
|
|
|
|
(
|
|
|
|
|
// Search result
|
|
|
|
|
<CategoryList {...{ handleSendTemplate, activeInput, onInput, valueMapped }} dataSource={dataSource} key='search-templates' />
|
|
|
|
|
)}
|
|
|
|
|
{/* <List
|
|
|
|
|
className='h-4/6 overflow-y-auto text-slate-900'
|
|
|
|
|
itemLayout='horizontal'
|
|
|
|
|
dataSource={dataSource}
|
|
|
|
@ -222,7 +290,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
|
|
|
|
|
className=' '
|
|
|
|
|
title={
|
|
|
|
|
<Flex justify={'space-between'}>
|
|
|
|
|
<>{item.components.header?.[0]?.text || item.name}</>
|
|
|
|
|
<span>{item.components.header?.[0]?.text || item.name}<Tag color='blue' className='ml-1'>{item.language.toUpperCase()}</Tag></span>
|
|
|
|
|
<Button onClick={() => handleSendTemplate(item)} size={'small'} type='link' key={'send'} icon={<SendOutlined />}>
|
|
|
|
|
Send
|
|
|
|
|
</Button>
|
|
|
|
@ -241,7 +309,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
|
|
|
|
|
/>
|
|
|
|
|
</List.Item>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
/> */}
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
// title={
|
|
|
|
@ -249,7 +317,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
|
|
|
|
|
// <div>🙋打招呼</div>
|
|
|
|
|
// <Button size='small' onClick={() => setOpenTemplates(false)}>×</Button>
|
|
|
|
|
// </div>}
|
|
|
|
|
trigger='click'
|
|
|
|
|
trigger='click' arrow={false}
|
|
|
|
|
open={openTemplates}
|
|
|
|
|
onOpenChange={(v) => {
|
|
|
|
|
setOpenTemplates(v);
|
|
|
|
|