feat: update

This commit is contained in:
Eleanor Mao 2024-04-12 16:51:46 +08:00
parent 89eea2d482
commit 20481ca406
18 changed files with 578 additions and 191 deletions

View File

@ -47,6 +47,7 @@
"match-sorter": "^6.3.4", "match-sorter": "^6.3.4",
"mini-css-extract-plugin": "^2.4.5", "mini-css-extract-plugin": "^2.4.5",
"monaco-editor": "^0.47.0", "monaco-editor": "^0.47.0",
"mui-message": "^1.0.3",
"postcss": "^8.4.4", "postcss": "^8.4.4",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^6.2.1", "postcss-loader": "^6.2.1",

31
src/AIBot.tsx Normal file
View File

@ -0,0 +1,31 @@
import React, { ChangeEvent, FC } from 'react';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
export const AIBot: FC<{
name?: string
value: string
onChange: (value: string) => void
}>
= ({ value, name, onChange }) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return (
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="subtitle1">{name ? `${name}` : 'AI Robot'}</Typography>
<Typography variant="caption" component="div" sx={{ mb: 1 }}>
You can maintain your knowledge in <a style={{ color: '#f50057' }} target="_blank" rel="noreferrer"
href="https://arf-xmn.int.rclabenv.com/admin/source">Artificial
Intelligence Admin Portal</a>
</Typography>
<Box>
<TextField label="AI Robot ID" onChange={handleChange} value={value}
/>
</Box>
</Paper>
);
};

View File

@ -15,8 +15,11 @@ const router = createBrowserRouter([
path: '/', path: '/',
element: <List/> element: <List/>
}, { }, {
path: '/create', path: '/create/:id',
element: <Create /> element: <Create />
}, {
path: '/detail/:id',
element: <Create edit />
}] }]
}, },
]); ]);

View File

@ -1,32 +0,0 @@
import React, { ChangeEvent, FC } from 'react'
import Paper from '@mui/material/Paper'
import Typography from '@mui/material/Typography'
import Box from '@mui/material/Box'
import TextField from '@mui/material/TextField'
interface Value {
id: string
secret: string
}
export const BotAddin: FC<{
value: Value
onChange: (value: Value) => void
}>
= ({ value, onChange }) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
[e.target.name]: e.target.value
})
}
return (
<Paper>
<Typography variant="h6">RC Bot Add-in</Typography>
<Box>
<TextField label="RingCentral Client ID" name="id" onChange={handleChange} value={value.id} />
<TextField label="RingCentral Client Screct" name="screct" onChange={handleChange} value={value.secret} />
</Box>
</Paper>
)
}

View File

@ -44,7 +44,7 @@ const ChatMessageWrap = styled(Box)<{ isMe: boolean }>(({ isMe }) => ({
})); }));
const ChatMessage = styled(Box)(() => ({ const ChatMessage = styled(Box)(() => ({
maxWidth: '80%', maxWidth: 'calc(100% - 50px)',
whiteSpace: 'pre-line' whiteSpace: 'pre-line'
})); }));
export const ChatItem: FC<Props> = ({ isMe, message, loading }) => { export const ChatItem: FC<Props> = ({ isMe, message, loading }) => {

View File

@ -5,26 +5,59 @@
* *
* *
*/ */
import React, { ChangeEvent, KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"; import React, { ChangeEvent, FC, KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import OutlinedInput from '@mui/material/OutlinedInput';
import SendIcon from '@mui/icons-material/Send'; import SendIcon from '@mui/icons-material/Send';
import List from '@mui/material/List'; import List from '@mui/material/List';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import { ChatItem } from "./ChatItem"; import { ChatItem } from "./ChatItem";
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import { uniqueId } from 'lodash'; import { cloneDeep, uniqueId } from 'lodash';
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { useLocation } from "react-router-dom"; import { useLocation, useNavigate, useParams } from "react-router-dom";
import axios from 'axios'; import axios from 'axios';
import { Definition, DSL2Json, Json2Preview } from "./loadYml"; import { Action, Definition, DSL2Json, Json2Preview, Json2Yml } from "./loadYml";
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import { EmailSender, EmailValue } from "./EmailSender";
import { AIBot } from "./AIBot";
import { GlipSender } from "./GlipSender";
import { ScriptBlock } from "./Script";
import { message } from 'mui-message';
import { isChatBot } from "./transform";
const DefaultMessage = [
'Hello! Please tell me what kind of workflow you want to generate!\n',
'Currently the node types I support are: Chat, BotMessage, GlipSender, Script'].join('\n');
const DefaultMessageZh = [
'你好(*´▽`)\n 快告诉我你想生成什么样的工作流吧 ❗',
].join('\n');
const DefaultEditMessageZh = [
'又见面啦(*╹▽╹*)\n 你已经生成过工作流啦,如果有需要修改的请告诉我吧 ❗',
].join('\n');
function getDefaultMessage(edit?: boolean): string {
if (edit) {
return DefaultEditMessageZh;
}
return DefaultMessageZh;
}
type PreValue = string | undefined | null
const preValueObj: { bot: PreValue; script: PreValue; email: PreValue; glip: PreValue } = {
bot: '',
script: '',
email: '',
glip: ''
};
interface Message { interface Message {
id: string; id: string;
@ -35,56 +68,82 @@ interface Message {
const SupportedBlock = ['BotMessage', 'Chat', 'GlipSender', 'Script']; const SupportedBlock = ['BotMessage', 'Chat', 'GlipSender', 'Script'];
export const Create = () => { export const Create: FC<{ edit?: boolean }> = ({ edit }) => {
const location = useLocation(); const { id } = useParams();
const id = useMemo(() => new URLSearchParams(location.search).get('id'), [location]); const [formJson, setFormJson] = useState({
const [formJson, setFormJson] = useState(() => { id: 'abc',
let value = { description: '',
id: 'abc', name: 'My workflow',
description: '', versionId: '',
name: 'My workflow', versionVisibleId: ''
versionId: '',
};
try {
value = JSON.parse(`create-${id}`);
} catch (e) {
}
return value;
}); });
const [chatList, setChatList] = useState<Message[]>([{ const [chatList, setChatList] = useState<Message[]>([{
id: uniqueId('r'), id: uniqueId('r'),
message: [ message: getDefaultMessage(edit),
'Hello! Please tell me what kind of workflow you want to generate!\n',
'Currently the node types I support are: Chat, BotMessage, GlipSender, Script'].join('\n'),
isMe: false isMe: false
}]); }]);
const [chatInput, setChatInput] = useState(''); const [chatInput, setChatInput] = useState('');
const [workflowContent, setWorkflowContent] = useState(''); const [workflowContent, setWorkflowContent] = useState('');
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
const flowDefinition = useMemo(() => DSL2Json(workflowContent), [workflowContent]); const flowDefinition = useMemo<Definition | undefined>(() => DSL2Json(workflowContent), [workflowContent]);
const previewImg = useMemo(() => Json2Preview(flowDefinition), [flowDefinition]); const previewImg = useMemo<string>(() => Json2Preview(flowDefinition), [flowDefinition]);
const chatContentRef = useRef<HTMLUListElement>(null); const chatContentRef = useRef<HTMLUListElement>(null);
const hasEmailSender = useMemo(() => workflowContent.includes('"EmailSender"'), [workflowContent]); const actions = useMemo<Array<Action & {
const hasBotAddin = useMemo(() => workflowContent.includes('"Chat"'), [workflowContent]); stateName: string
const hasScript = useMemo(() => workflowContent.includes('"Script"'), [workflowContent]); }>>(() => ((flowDefinition || {}).states || []).reduce<Array<Action & { stateName: string }>>((l, s) => {
l.push(...s.actions.map(a => ({ ...a, stateName: s.name })));
const [emailData, setEmailData] = useState({ return l;
}, []), [flowDefinition]);
const hasEmailSender = useMemo(() => actions.some(a => a.actionType === "EmailSender"), [actions]);
const botAddinAction = useMemo<Action & {
stateName: string
} | undefined>(() => actions.find(a => a.actionType === "Chat"), [actions]);
const scriptAction = useMemo<Action & {
stateName: string
} | undefined>(() => actions.find(a => a.actionType === "Script"), [actions]);
const glipSenderAction = useMemo<Action & {
stateName: string
} | undefined>(() => actions.find(a => a.actionType === "GlipSender"), [actions]);
const [aibot, setAIBot] = useState('');
const [script, setScript] = useState('');
const [emailData, setEmailData] = useState<EmailValue>({
to: '', to: '',
subject: '', subject: '',
content: `${content}` // eslint-disable-next-line no-template-curly-in-string
content: '${localData.Desc}',
}); });
const [glipSenderData, setGlipSenderData] = useState({
clientId: '',
clientSecret: '',
// eslint-disable-next-line no-template-curly-in-string
message: ''
});
const [channelId, setChannelId] = useState('78394499078-4034902020');
const location = useLocation();
const debug = useMemo(() => {
return (new URLSearchParams(location.search)).get('debug') === 'true';
}, [location]);
useEffect(() => { useEffect(() => {
console.log(workflowContent); console.log(workflowContent);
console.log(flowDefinition); console.log(flowDefinition);
}, [flowDefinition]); }, [flowDefinition, workflowContent]);
// useEffect(() => { useEffect(() => {
// axios.post('/chat/start', { if (id) {
// accountId: 10086, axios.get<{
// channelId: "10000", id: string
// dialogId: "1", visibleId: string
// segmentId: "1" versionId: string
// }); versionVisibleId: string
// }, []); content: string
name: string
description: string
}>(`/bot/workflow/${id}`).then(({ data }) => {
setFormJson(data);
setWorkflowContent(data.content || '');
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setChatInput(e.target.value); setChatInput(e.target.value);
}; };
@ -123,6 +182,57 @@ export const Create = () => {
} }
}; };
useEffect(() => {
if (glipSenderAction) {
let actionValueString = glipSenderAction.message;
let crtValueString = glipSenderData.message;
if (actionValueString !== preValueObj.glip && crtValueString === preValueObj.glip) {
setGlipSenderData({
clientSecret: '',
clientId: '',
message: actionValueString || '',
});
preValueObj.glip = actionValueString || '';
}
} else {
if (preValueObj.glip) {
preValueObj.glip = '';
setGlipSenderData({
clientSecret: '',
clientId: '',
// eslint-disable-next-line no-template-curly-in-string
message: '${localData.Desc}'
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [glipSenderAction]);
useEffect(() => {
if (scriptAction) {
if (scriptAction.scriptSource !== preValueObj.script && script === preValueObj.script) {
setScript(scriptAction.scriptSource || '');
preValueObj.script = scriptAction.scriptSource || '';
}
} else if (preValueObj.script) {
preValueObj.script = '';
setScript('');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scriptAction]);
useEffect(() => {
if (botAddinAction) {
if (botAddinAction.id !== preValueObj.bot && aibot === preValueObj.bot) {
setAIBot(botAddinAction.id || '');
preValueObj.bot = botAddinAction.id || '';
}
} else if (preValueObj.bot) {
preValueObj.bot = '';
setAIBot('');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [botAddinAction]);
useEffect(() => { useEffect(() => {
if (chatContentRef.current) { if (chatContentRef.current) {
chatContentRef.current.scrollTop = chatContentRef.current.scrollHeight - chatContentRef.current.offsetHeight; chatContentRef.current.scrollTop = chatContentRef.current.scrollHeight - chatContentRef.current.offsetHeight;
@ -133,27 +243,74 @@ export const Create = () => {
await axios.delete(`/bot/workflow/${id}/messages`); await axios.delete(`/bot/workflow/${id}/messages`);
setChatList([{ setChatList([{
id: uniqueId('r'), id: uniqueId('r'),
message: [ message: getDefaultMessage(edit),
'Hello! Please tell me what kind of workflow you want to generate!\n',
'Currently the node types I support are: Chat, BotMessage, GlipSender, Script'].join('\n'),
isMe: false isMe: false
}]); }]);
setChatInput(''); setChatInput('');
setWorkflowContent(''); setWorkflowContent('');
}; };
useEffect(() => {
if (!hasEmailSender)
setEmailData({
to: '',
subject: '',
// eslint-disable-next-line no-template-curly-in-string
content: '${localData.Desc}',
});
}, [hasEmailSender]);
const navigate = useNavigate();
const handleSubmit = async () => {
if (!flowDefinition) return;
const newJson = cloneDeep(flowDefinition);
newJson.states.forEach(state => {
state.actions.forEach(action => {
if (action.actionType === 'Chat') {
action.id = aibot;
}
if (action.actionType === 'Script') {
action.scriptSource = script;
}
if (action.actionType === 'EmailSender') {
action.to = emailData.to;
action.subject = emailData.subject;
action.content = emailData.content;
}
if (action.actionType === 'GlipSender') {
// action.url = glipSenderData.url;
action.message = glipSenderData.message || '${localData.Desc}';
}
});
});
console.log(Json2Yml(newJson));
await axios.put(`/bot/workflow/${id}`, {
name: formJson.name,
content: Json2Yml(newJson)
});
if (!debug) {
await axios.delete(`/bot/workflow/${id}/bind/${channelId}`);
await axios.post(`/bot/workflow/${id}/bind`, { channelId });
}
message.success(`Saved! ${debug ? 'Back to List and go bind your channel!' : ''}`);
setTimeout(() => {
navigate(`/${debug ? '?debug=true' : ''}`);
}, 3000);
};
return ( return (
<> <>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}> <Grid item xs={12}>
<TextField value={formJson.name} fullWidth variant="outlined" label="Workflow Name" <TextField value={formJson.name} fullWidth variant="outlined" label="Workflow Name" InputProps={{
readOnly: true,
}}
placeholder="Please name your workflow" onChange={e => { placeholder="Please name your workflow" onChange={e => {
setFormJson(json => ({ setFormJson(json => ({
...json, ...json,
name: e.target.value name: e.target.value
})); }));
}}/> }}/>
{/*<Divider sx={{ mt: 4, mb: -2 }}/>*/}
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Grid container spacing={2}> <Grid container spacing={2}>
@ -166,7 +323,7 @@ export const Create = () => {
borderColor: 'rgba(255,255,255,0.23)', borderColor: 'rgba(255,255,255,0.23)',
position: 'relative' position: 'relative'
}}> }}>
<Tooltip title="Refresh AI"> <Tooltip title="Clear AI 🧠">
<IconButton onClick={handleRefresh} style={{ position: 'absolute', right: 10, top: 10, zIndex: 5 }}> <IconButton onClick={handleRefresh} style={{ position: 'absolute', right: 10, top: 10, zIndex: 5 }}>
<RefreshIcon/> <RefreshIcon/>
</IconButton> </IconButton>
@ -180,27 +337,29 @@ export const Create = () => {
</List> </List>
<Box sx={{ display: 'flex', flex: '0 0 auto', p: 2, borderTop: 1, borderColor: 'divider' }}> <Box sx={{ display: 'flex', flex: '0 0 auto', p: 2, borderTop: 1, borderColor: 'divider' }}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} style={{ gap: 8 }}> {/*<Grid item xs={12} style={{ gap: 8 }}>*/}
<Stack direction="row" spacing={1}> {/* <Stack direction="row" spacing={1}>*/}
<Typography variant="body2"> {/* <Typography variant="body2">*/}
Supported Action Types: {/* Supported Action Types:*/}
</Typography> {/* </Typography>*/}
{SupportedBlock.map(label => ( {/* {SupportedBlock.map(label => (*/}
<Chip label={label} key={label} size="small" clickable color="primary" {/* <Chip label={label} key={label} size="small" clickable color="primary"*/}
onClick={handleClickChip(label)}/>))} {/* onClick={handleClickChip(label)}/>))}*/}
</Stack> {/* </Stack>*/}
</Grid> {/*</Grid>*/}
<Grid item xs> <Grid item xs>
<OutlinedInput fullWidth size="small" onChange={handleChange} <TextField fullWidth size="small" onChange={handleChange}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => { onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && chatInput.length) { if (e.key === 'Enter' && chatInput.length) {
e.preventDefault(); e.preventDefault();
handleSend(); handleSend();
} }
}} }}
autoFocus autoFocus
multiline maxRows={3} variant="outlined"
value={chatInput}/> focused placeholder="Enter your goal ..."
multiline maxRows={3}
value={chatInput}/>
</Grid> </Grid>
<Grid item xs="auto" justifyContent="center"> <Grid item xs="auto" justifyContent="center">
<Button variant="contained" size="large" disabled={!chatInput.length} onClick={handleSend}> <Button variant="contained" size="large" disabled={!chatInput.length} onClick={handleSend}>
@ -214,15 +373,23 @@ export const Create = () => {
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
<Box sx={{ border: 1, height: 600, borderRadius: 1, p: 2, mt: 4, borderColor: 'rgba(255,255,255,0.23)' }}> <Box sx={{ border: 1, height: 600, borderRadius: 1, p: 2, mt: 4, borderColor: 'rgba(255,255,255,0.23)' }}>
{!previewImg ? 'Workflow Preview...' : {!previewImg ? <>👋 The flow chart will be previewed here! <br/>😻 Tell the AI robot what you want to
do! </> :
<img src={previewImg} style={{ width: '100%', height: '100%' }} alt="preview"/>} <img src={previewImg} style={{ width: '100%', height: '100%' }} alt="preview"/>}
</Box> </Box>
</Grid> </Grid>
</Grid> </Grid>
<Divider sx={{ mt: 4, mb: 4 }}/> <Divider sx={{ mt: 4, mb: 4 }}/>
{!!scriptAction || hasEmailSender || !!botAddinAction ?
<Typography variant="subtitle2" sx={{ mb: 2 }}>
Please fill in the following information related to your workflow nodes. 👇🏻👇🏻</Typography> : null}
{hasEmailSender && <EmailSender value={emailData} onChange={setEmailData}/>}
{!!botAddinAction && <AIBot name={botAddinAction.stateName} value={aibot} onChange={setAIBot}/>}
{!!scriptAction && <ScriptBlock name={scriptAction.stateName} value={script} onChange={setScript}/>}
{!!glipSenderAction && <GlipSender debug={debug} value={glipSenderData} onChange={setGlipSenderData}/>}
</Grid> </Grid>
</Grid> </Grid>
<div style={{ height: 80 }}></div>
<Box <Box
sx={{ sx={{
position: 'fixed', position: 'fixed',
@ -232,11 +399,12 @@ export const Create = () => {
p: 2, p: 2,
boxShadow: 2, boxShadow: 2,
borderTop: 1, borderTop: 1,
zIndex: 10,
bgcolor: 'background.default', bgcolor: 'background.default',
borderColor: 'divider', borderColor: 'divider',
display: { xs: 'flex' }, justifyContent: "flex-end" display: { xs: 'flex' }, justifyContent: "flex-end"
}}> }}>
<Button variant="contained" style={{ width: 100 }}>Save</Button> <Button variant="contained" style={{ width: 100 }} onClick={handleSubmit}>Save</Button>
</Box> </Box>
</> </>
); );

View File

@ -1,22 +1,41 @@
import React, { ChangeEvent, FC } from 'react' import React, { ChangeEvent, FC } from 'react';
import Paper from '@mui/material/Paper' import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box' import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField' import TextField from '@mui/material/TextField';
export const EmailSender: FC<{ value: string; onChange: (value: string) => void }> = ({ value, onChange }) => { export interface EmailValue {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { to: string;
onChange(e.target.value) content: string;
} subject: string;
return (
<Paper>
<Typography variant="h6">Email Sender</Typography>
<Box>
<TextField label="Email" placeholder="Please enter your email address" type="email"
value={value}
onChange={handleChange}
/>
</Box>
</Paper>
)
} }
export const EmailSender: FC<{
value: EmailValue; onChange: (value: EmailValue) => void
}> = ({ value, onChange }) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
[e.target.name]: e.target.value
});
};
return (
<Paper sx={{p: 2, mb:2}}>
<Typography variant="subtitle1" sx={{mb: 1}}>Email Sender</Typography>
<Box>
<TextField label="Send To" placeholder="Please enter your email address" type="email"
value={value} name="to" fullWidth sx={{mb: 4}}
onChange={handleChange}
/>
<TextField label="Subject" placeholder="Please enter your email subject"
value={value} name="subject" fullWidth sx={{mb: 4}}
onChange={handleChange}
/>
<TextField label="Content" placeholder="Please enter your email content"
value={value} name="content" fullWidth sx={{mb: 4}}
onChange={handleChange} maxRows={8} minRows={3} multiline
/>
</Box>
</Paper>
);
};

44
src/GlipSender.tsx Normal file
View File

@ -0,0 +1,44 @@
import React, { ChangeEvent, FC, useEffect, useState } from 'react';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
export const GlipSender: FC<{
debug?: boolean
value: { clientId: string; clientSecret: string; message: string };
onChange: (value: {
clientId: string; clientSecret: string; message: string
}) => void
}> = ({ value, debug, onChange }) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
[e.target.name]: e.target.value
});
};
return (
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="subtitle1">GlipSender</Typography>
<Typography variant="caption" component="div" sx={{ mb: 1 }}>
WFL Hub will send messages via <a style={{ color: '#f50057' }}
href="https://developers.ringcentral.com/guide/team-messaging/bots/walkthrough"
target="_blank" rel="noreferrer">RingCentral Bot</a>, please fill in:
</Typography>
<Box>
{!debug &&
<>
<TextField label="RingCentral Bot Client ID" fullWidth name="clientId" onChange={handleChange}
value={value.clientId} sx={{ mb: 4 }}/>
<TextField label="RingCentral Bot Client Secret" sx={{ mb: 4 }}
value={value.clientSecret} name="clientSecret" fullWidth onChange={handleChange}
/>
</>}
<TextField label="Message format" placeholder="Please enter your message format"
value={value.message} name="message" fullWidth
onChange={handleChange} maxRows={5} minRows={2} multiline
/>
</Box>
</Paper>
);
};

View File

@ -6,7 +6,7 @@
* *
*/ */
import React from "react"; import React from "react";
import { Outlet } from "react-router-dom"; import { Link, Outlet } from "react-router-dom";
import AppBar from '@mui/material/AppBar'; import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
@ -37,18 +37,20 @@ const Content = styled(Box)(({ theme }) => ({
export const Home = () => { export const Home = () => {
return ( return (
<Layout> <Layout>
<AppBar position="static" enableColorOnDark> <AppBar position="static" enableColorOnDark sx={{ bgcolor: '#3f51b5' }}>
<Toolbar> <Toolbar>
<Typography <Typography
variant="h6" variant="h6"
noWrap noWrap
component="div" component={Link}
sx={{ flexGrow: 1, display: { xs: 'block' } }} to="/"
style={{ cursor: 'point' }}
sx={{ flexGrow: 1, display: { xs: 'block' }, color: 'white', }}
> >
<RCIcon style={{ marginRight: 4, verticalAlign: 'middle' }}/> <RCIcon style={{ marginRight: 4, verticalAlign: 'middle' }}/>
RingCentral AI Workflow Hub RingCentral AI Workflow Hub
</Typography> </Typography>
<Box sx={{ display: { xs: 'block' } }}> <Box sx={{ display: { xs: 'block' }, color: 'white' }}>
<IconButton <IconButton
size="large" size="large"
edge="end" edge="end"

View File

@ -5,28 +5,29 @@
* *
* *
*/ */
import React, { useEffect, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import CardActionArea from '@mui/material/CardActionArea'; import CardActionArea from '@mui/material/CardActionArea';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import { Link, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions'; import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent'; import CardContent from '@mui/material/CardContent';
import { RCIcon } from './RCIcon';
import axios from "axios"; import axios from "axios";
import Container from '@mui/material/Container'; import Container from '@mui/material/Container';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions'; import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText'; import DialogContentText from '@mui/material/DialogContentText';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import { getRandomEmoji } from "./randomEmoji";
import { formatTime } from "./timeFormat";
import { message } from "mui-message";
const Flex = styled(Box)(() => ({ const Flex = styled(Box)(() => ({
display: 'flex', display: 'flex',
@ -37,11 +38,13 @@ interface Workflow {
description: string; description: string;
id: string; id: string;
name: string; name: string;
lastModifiedTime: string;
} }
export const List = () => { export const List = () => {
const [list, setList] = useState<Workflow[]>([]); const [list, setList] = useState<Workflow[]>([]);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [showBindModal, setShowBindModel] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
@ -55,20 +58,38 @@ export const List = () => {
const handleOpen = () => { const handleOpen = () => {
setShowModal(true); setShowModal(true);
}; };
const handleOpenBind = (id: string) => {
setShowBindModel(id);
};
const handleClose = () => { const handleClose = () => {
setShowModal(false); setShowModal(false);
}; };
const handleCloseBindModel = () => {
setShowBindModel('');
};
const location = useLocation();
const debug = useMemo(() => {
return (new URLSearchParams(location.search)).get('debug') === 'true';
}, [location]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const formData = new FormData(event.currentTarget); const formData = new FormData(event.currentTarget);
const formJson = Object.fromEntries((formData as any).entries()); const formJson = Object.fromEntries((formData as any).entries());
axios.post('/bot/workflow', { name: formJson.name }).then(({ data }) => { axios.post('/bot/workflow', { name: formJson.name }).then(({ data }) => {
sessionStorage.setItem(`create-${data.visibleId}`, JSON.stringify({ ...data, name: formJson.name }));
handleClose(); handleClose();
navigate(`/create?id=${data.visibleId}`); navigate(`/create/${data.visibleId}${debug ? '?debug=true' : ''}`,);
}); });
}; };
const handleSubmitBind = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const formJson = Object.fromEntries((formData as any).entries());
const channelId = formJson.channelId.trim();
await axios.delete(`/bot/workflow/${showBindModal}/bind/${channelId}`);
await axios.post(`/bot/workflow/${showBindModal}/bind`, { channelId });
message.success('Success!');
handleCloseBindModel();
};
return ( return (
<Container maxWidth="lg"> <Container maxWidth="lg">
<Flex style={{ justifyContent: 'flex-end' }}> <Flex style={{ justifyContent: 'flex-end' }}>
@ -78,21 +99,30 @@ export const List = () => {
{list.map(item => ( {list.map(item => (
<Link <Link
key={item.id} key={item.id}
to={`/detail/${item.id}`} to={`/detail/${item.id}${debug ? '?debug=true' : ''}`}
> >
<Card sx={{ width: 263, borderRadius: 2, boxShadow: 2 }}> <Card sx={{ width: 263, borderRadius: 2, boxShadow: 2 }}>
<CardActionArea> <CardActionArea>
<CardContent> <CardContent style={{ height: 100 }}>
<Typography gutterBottom variant="h5" component="div"> <Typography gutterBottom variant="h5" component="div">
{item.name || 'My Workflow'} {getRandomEmoji()} {item.name || 'My Workflow'}
</Typography> </Typography>
<Box sx={{ minHeight: 60 }}>
<RCIcon/>
</Box>
</CardContent> </CardContent>
<CardActions <CardActions
sx={{ display: { xs: 'flex' }, justifyContent: "flex-end", borderTop: 1, borderColor: 'divider' }}> sx={{
<Switch defaultChecked/> display: { xs: 'flex' },
justifyContent: "space-between",
borderTop: 1,
borderColor: 'divider'
}}>
<Typography component="span" variant="caption" style={{ opacity: .6 }}>Last
modified: {formatTime(item.lastModifiedTime)}</Typography>
{debug ?
<Button size="small" onClick={(e: any) => {
e.preventDefault();
handleOpenBind(item.id);
}}>Bind</Button> :
<Switch defaultChecked/>}
</CardActions> </CardActions>
</CardActionArea> </CardActionArea>
</Card> </Card>
@ -121,7 +151,7 @@ export const List = () => {
label="Workflow Name" label="Workflow Name"
fullWidth fullWidth
focused focused
variant="standard" variant="outlined"
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@ -129,6 +159,27 @@ export const List = () => {
<Button type="submit">Create</Button> <Button type="submit">Create</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Dialog open={!!showBindModal} onClose={handleCloseBindModel}
PaperProps={{
component: 'form',
onSubmit: handleSubmitBind,
}}>
<DialogTitle>Bind Channel</DialogTitle>
<DialogContent>
<TextField
required
margin="dense"
id="channelId" focused
variant="outlined"
label="Channel Id" fullWidth name="channelId"
helperText="ChannelId is ${groupId}-${creatorId}"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseBindModel}>Cancel</Button>
<Button type="submit">Bind</Button>
</DialogActions>
</Dialog>
</Container> </Container>
); );
}; };

View File

@ -1,16 +1,19 @@
import React, { FC } from 'react' import React, { FC } from 'react';
import Box from '@mui/material/Box' import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper' import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography';
import { Editor } from './Editor' import { Editor } from './Editor';
export const ScriptBlock: FC<{ value: string; onChange: (value: string) => void }> = (props) => { export const ScriptBlock: FC<{ value: string; name?: string; onChange: (value: string) => void }> = ({
return ( name,
<Paper> ...props
<Typography variant="h6">Script</Typography> }) => {
<Box> return (
<Editor {...props} /> <Paper sx={{ p: 2, mb: 2 }}>
</Box> <Typography variant="subtitle1" sx={{ mb: 1 }}>{name ? `${name}` : 'Script'}</Typography>
</Paper> <Box>
) <Editor {...props} />
} </Box>
</Paper>
);
};

View File

@ -26,3 +26,7 @@ a {
height: 600px; height: 600px;
border: 1px solid #ccc; border: 1px solid #ccc;
} }
.MuiButton-contained:hover {
background-color: rgb(118, 126, 168) !important;
}

View File

@ -8,32 +8,34 @@ import ReactDOM from 'react-dom/client';
import App from './App'; import App from './App';
import { ThemeProvider, createTheme } from '@mui/material/styles'; import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import { MessageBox } from 'mui-message';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
); );
const theme = createTheme({ const theme = createTheme({
palette: { palette: {
mode: 'dark', mode: 'dark',
primary: { primary: {
main: '#3f51b5', // main: '#3f51b5',
}, main: '#aab5f1'
tonalOffset: 0.6,
secondary: {
main: '#f50057',
},
background: {
paper: '#404041',
default: '#242425',
},
}, },
tonalOffset: 0.6,
secondary: {
main: '#f50057',
},
background: {
paper: '#404041',
default: '#242425',
},
},
}); });
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline/> <CssBaseline/>
<App/> <MessageBox/>
</ThemeProvider> <App/>
</React.StrictMode> </ThemeProvider>
</React.StrictMode>
); );

View File

@ -27,20 +27,28 @@ export interface EventHandlerAction {
actionType: string; actionType: string;
targetState?: string; targetState?: string;
scriptSource?: string; scriptSource?: string;
transitionName?:string transitionName?: string;
} }
export interface Action { export interface Action {
actionType: string; actionType: string;
/* Script */
scriptSource?: string; scriptSource?: string;
/* Chat */
type?: string;
token?: string;
id?: string; id?: string;
question?: string; question?: string;
score?: null; score?: null;
token?: string; /*EmailSender*/
type?: string; content?: string;
content?:string; subject?: string;
subject?:string; to?: string;
to?:string /*GlipSender*/
url?: string;
creatorId?: string;
groupId?: string;
message?: string;
} }
export interface EventHandler { export interface EventHandler {
@ -63,7 +71,7 @@ export interface Definition {
export function Json2Preview(definition?: Definition): string { export function Json2Preview(definition?: Definition): string {
if (!definition) return ''; if (!definition) return '';
try { try {
console.log(transformDSL(definition)) console.log(transformDSL(definition));
return compress(transformDSL(definition), '/svg/'); return compress(transformDSL(definition), '/svg/');
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -75,7 +83,11 @@ export function DSL2Json(input: string): Definition {
return jsYaml.load(input) as Definition; return jsYaml.load(input) as Definition;
} }
export function Json2Yml(json: any): string {
return jsYaml.dump(json, { quotingType: '"' });
}
// @ts-ignore // @ts-ignore
window.ToJson = DSL2Json window.ToJson = DSL2Json;
// @ts-ignore // @ts-ignore
window.transform = transformDSL window.transform = transformDSL;

14
src/randomEmoji.ts Normal file

File diff suppressed because one or more lines are too long

29
src/timeFormat.ts Normal file
View File

@ -0,0 +1,29 @@
/*
* @Author : Eleanor Mao
* @Date : 2024-04-12 10:53:40
* @LastEditTime : 2024-04-12 10:53:40
*
* Copyright © RingCentral. All rights reserved.
*/
export const formatTime = (time?: string | number) => {
if (!time) return 'Never';
const date = new Date(typeof time === 'string' ? time : time * 1000);
const theTime = date.getTime();
const now = Date.now();
const diff = now - theTime;
const month = date.getMonth() + 1;
const daate = date.getDate();
const hours = date.getHours();
const mins = `${date.getMinutes() + 100}`.slice(1);
if (diff < 15 * 60 * 1000) {
return 'Just now';
} else if (diff < 60 * 60 * 1000) {
return `${Math.floor(diff / 1000 / 60 )}m ago`;
} else if (diff < 24 * 60 * 60 * 1000) {
return `${Math.floor(diff / 1000 / 60 / 60)}h ago`;
} else if (diff < 7 * 24 * 60 * 60 * 1000) {
return `${Math.floor(diff / 1000 / 60 / 60 / 24 )}d ago`;
}
return `${month}/${daate} ${hours}:${mins}`;
};

View File

@ -5,6 +5,8 @@ const HTTP_COLOR = "#Peru";
const SCRIPT_COLOR = "#Olive;text:white"; const SCRIPT_COLOR = "#Olive;text:white";
const BOT_COLOR = "#Blue;text:white"; const BOT_COLOR = "#Blue;text:white";
const EMAIL_SENDER_COLOR = "#Red;text:white"; const EMAIL_SENDER_COLOR = "#Red;text:white";
const CHAT_BOT_COLOR = "#Orange;text:white";
const GLIP_COLOR = "#Pink;text:white";
const DEFAULT_COLOR = '#Grey;text;white'; const DEFAULT_COLOR = '#Grey;text;white';
const EVENT_NAME = ": Event"; const EVENT_NAME = ": Event";
@ -12,6 +14,7 @@ const HTTP_NAME = ": Http";
const SCRIPT_NAME = ": Script"; const SCRIPT_NAME = ": Script";
const BOT_NAME = ": Chat"; const BOT_NAME = ": Chat";
const EMAIL_SENDER_NAME = ": EmailSender"; const EMAIL_SENDER_NAME = ": EmailSender";
const GLIP_SENDER_NAME = ': GlipSender';
const EXIT_NAME = ": Exit"; const EXIT_NAME = ": Exit";
function isHttp(actions: Action[]): boolean { function isHttp(actions: Action[]): boolean {
@ -26,7 +29,12 @@ function isScript(actions: Action[]): boolean {
function isChat(actions: Action[]): boolean { function isChat(actions: Action[]): boolean {
return actions return actions
.some((action) => action.actionType === 'Chat'); .some((action) => action.actionType === 'ChatBot');
}
function isGlipSender(actions: Action[]): boolean {
return actions
.some((action) => action.actionType === 'GlipSender');
} }
function isExit(actions: Action[]): boolean { function isExit(actions: Action[]): boolean {
@ -38,6 +46,10 @@ function isEmailSender(actions: Action[]): boolean {
.some((action) => action.actionType === "EmailSender"); .some((action) => action.actionType === "EmailSender");
} }
export function isChatBot(actions: Action[]): boolean {
return actions.some(action => action.actionType === 'ChatResponse');
}
function isEmpty(arr: any[]): boolean { function isEmpty(arr: any[]): boolean {
return arr === undefined || arr == null || arr.length === 0; return arr === undefined || arr == null || arr.length === 0;
} }
@ -71,6 +83,10 @@ function drawState(eventState: Map<string, Set<string>>, state: State): string {
sb += `state ${name} ${BOT_COLOR} ${BOT_NAME}\n`; sb += `state ${name} ${BOT_COLOR} ${BOT_NAME}\n`;
} else if (isEmailSender(actions)) { } else if (isEmailSender(actions)) {
sb += `state ${name} ${EMAIL_SENDER_COLOR} ${EMAIL_SENDER_NAME}\n`; sb += `state ${name} ${EMAIL_SENDER_COLOR} ${EMAIL_SENDER_NAME}\n`;
} else if (isChatBot(actions)) {
sb += `state ${name} ${CHAT_BOT_COLOR} ${BOT_NAME}\n`;
} else if (isGlipSender(actions)) {
sb += `state ${name} ${GLIP_COLOR} ${GLIP_SENDER_NAME}\n`;
} else { } else {
console.log('====', name, actions); console.log('====', name, actions);
// throw Error(`state ${name} actions is incorrect`); // throw Error(`state ${name} actions is incorrect`);

View File

@ -3551,6 +3551,11 @@ cliui@^7.0.2:
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
clsx@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
clsx@^2.1.0: clsx@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
@ -5416,7 +5421,7 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hoist-non-react-statics@^3.3.1: hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -7084,6 +7089,13 @@ ms@2.1.3, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
mui-message@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/mui-message/-/mui-message-1.0.3.tgz#0af2af1dfb2192a102a461e3193c2f27ef003d34"
integrity sha512-MW+CqmrwGqbNe/cG196zzm+fCdYb4yNCLyfCgTyYCI4NEm9gSLUmc+rbkFd2HoYmrKrMBa9KBmtEBMYMdjKYAQ==
dependencies:
notistack "^2.0.8"
multicast-dns@^7.2.5: multicast-dns@^7.2.5:
version "7.2.5" version "7.2.5"
resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced"
@ -7164,6 +7176,14 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
notistack@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/notistack/-/notistack-2.0.8.tgz#78cdf34c64e311bf1d1d71c2123396bcdea5e95b"
integrity sha512-/IY14wkFp5qjPgKNvAdfL5Jp6q90+MjgKTPh4c81r/lW70KeuX6b9pE/4f8L4FG31cNudbN9siiFS5ql1aSLRw==
dependencies:
clsx "^1.1.0"
hoist-non-react-statics "^3.3.0"
npm-run-path@^4.0.1: npm-run-path@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"