This commit is contained in:
Eleanor Mao 2024-04-11 17:17:37 +08:00
parent cd269b82b3
commit 6a0362e18f
11 changed files with 502 additions and 98 deletions

View File

@ -18,6 +18,7 @@
"@types/node": "^16.18.95", "@types/node": "^16.18.95",
"@types/react": "^18.2.74", "@types/react": "^18.2.74",
"@types/react-dom": "^18.2.24", "@types/react-dom": "^18.2.24",
"axios": "^1.6.8",
"babel-jest": "^27.4.2", "babel-jest": "^27.4.2",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",
"babel-plugin-named-asset-import": "^0.3.8", "babel-plugin-named-asset-import": "^0.3.8",
@ -52,6 +53,7 @@
"postcss-normalize": "^10.0.1", "postcss-normalize": "^10.0.1",
"postcss-preset-env": "^7.0.1", "postcss-preset-env": "^7.0.1",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"qs": "^6.12.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-app-polyfill": "^3.0.0", "react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
@ -102,6 +104,7 @@
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.0", "@types/lodash": "^4.17.0",
"@types/qs": "^6.9.14",
"monaco-editor-webpack-plugin": "^7.1.0" "monaco-editor-webpack-plugin": "^7.1.0"
}, },
"jest": { "jest": {
@ -159,5 +162,6 @@
"presets": [ "presets": [
"react-app" "react-app"
] ]
} },
"proxy": "http://rcw-mock-lab02.lab.nordigy.ru:8080"
} }

View File

@ -11,10 +11,12 @@ import ListItem from '@mui/material/ListItem';
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined'; import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import FaceIcon from '@mui/icons-material/Face'; import FaceIcon from '@mui/icons-material/Face';
import CircularProgress from '@mui/material/CircularProgress';
interface Props { interface Props {
isMe: boolean; isMe: boolean;
message: string; message: string;
loading?: boolean;
} }
@ -43,8 +45,9 @@ const ChatMessageWrap = styled(Box)<{ isMe: boolean }>(({ isMe }) => ({
const ChatMessage = styled(Box)(() => ({ const ChatMessage = styled(Box)(() => ({
maxWidth: '80%', maxWidth: '80%',
whiteSpace: 'pre-line'
})); }));
export const ChatItem: FC<Props> = ({ isMe, message }) => { export const ChatItem: FC<Props> = ({ isMe, message, loading }) => {
return ( return (
<ChatWrap isMe={isMe} alignItems="flex-start"> <ChatWrap isMe={isMe} alignItems="flex-start">
<ChatAvatar> <ChatAvatar>
@ -53,7 +56,8 @@ export const ChatItem: FC<Props> = ({ isMe, message }) => {
</ChatAvatar> </ChatAvatar>
<ChatMessageWrap isMe={isMe}> <ChatMessageWrap isMe={isMe}>
<ChatMessage sx={{ borderRadius: 2, p: 2, bgcolor: isMe ? 'primary.light' : 'secondary.light' }}> <ChatMessage sx={{ borderRadius: 2, p: 2, bgcolor: isMe ? 'primary.light' : 'secondary.light' }}>
{message} {loading && <CircularProgress color="secondary" size={20}/>}
{!loading && message}
</ChatMessage> </ChatMessage>
</ChatMessageWrap> </ChatMessageWrap>
</ChatWrap> </ChatWrap>

View File

@ -5,7 +5,7 @@
* *
* *
*/ */
import React, { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from "react"; import React, { ChangeEvent, 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';
@ -19,21 +19,72 @@ import { ChatItem } from "./ChatItem";
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { Editor } from "./Editor"; import { useLocation } from "react-router-dom";
import axios from 'axios';
import { Definition, DSL2Json, Json2Preview } from "./loadYml";
import RefreshIcon from '@mui/icons-material/Refresh';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
interface Message { interface Message {
id: string; id: string;
message: string; message: string;
isMe: boolean; isMe: boolean;
loading?: boolean;
} }
const SupportedBlock = ['BotMessage', 'Chat', 'EmailSender', 'Script']; const SupportedBlock = ['BotMessage', 'Chat', 'GlipSender', 'Script'];
export const Create = () => { export const Create = () => {
const [chatList, setChatList] = useState<Message[]>([]); const location = useLocation();
const [workflowName, setWorkFlowName] = useState('My workflow'); const id = useMemo(() => new URLSearchParams(location.search).get('id'), [location]);
const [formJson, setFormJson] = useState(() => {
let value = {
id: 'abc',
description: '',
name: 'My workflow',
versionId: '',
};
try {
value = JSON.parse(`create-${id}`);
} catch (e) {
}
return value;
});
const [chatList, setChatList] = useState<Message[]>([{
id: uniqueId('r'),
message: [
'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
}]);
const [chatInput, setChatInput] = useState(''); const [chatInput, setChatInput] = useState('');
const [workflowContent, setWorkflowContent] = useState('');
// eslint-disable-next-line no-template-curly-in-string
const flowDefinition = useMemo(() => DSL2Json(workflowContent), [workflowContent]);
const previewImg = useMemo(() => Json2Preview(flowDefinition), [flowDefinition]);
const chatContentRef = useRef<HTMLUListElement>(null); const chatContentRef = useRef<HTMLUListElement>(null);
const hasEmailSender = useMemo(() => workflowContent.includes('"EmailSender"'), [workflowContent]);
const hasBotAddin = useMemo(() => workflowContent.includes('"Chat"'), [workflowContent]);
const hasScript = useMemo(() => workflowContent.includes('"Script"'), [workflowContent]);
const [emailData, setEmailData] = useState({
to: '',
subject: '',
content: `${content}`
});
useEffect(() => {
console.log(workflowContent);
console.log(flowDefinition);
}, [flowDefinition]);
// useEffect(() => {
// axios.post('/chat/start', {
// accountId: 10086,
// channelId: "10000",
// dialogId: "1",
// segmentId: "1"
// });
// }, []);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setChatInput(e.target.value); setChatInput(e.target.value);
}; };
@ -41,13 +92,35 @@ export const Create = () => {
const handleClickChip = (text: string) => () => { const handleClickChip = (text: string) => () => {
setChatInput(s => s + text); setChatInput(s => s + text);
}; };
const handleSend = () => { const handleSend = async () => {
const rId = uniqueId('r');
setChatList([...chatList, { setChatList([...chatList, {
id: uniqueId(), id: uniqueId('m'),
message: chatInput, message: chatInput,
isMe: true isMe: true
}, {
id: rId,
message: '',
isMe: false,
loading: true
}]); }]);
setChatInput(''); setChatInput('');
try {
const { data } = await axios.post<{ answer: string; workflowContent: string }>(`/bot/workflow/${id}/messages`, {
"question": chatInput
});
setWorkflowContent(data.workflowContent);
setChatList(list => list.map(l => {
if (l.id === rId) {
l.loading = false;
l.message = data.answer;
}
return l;
}));
} catch (e) {
setChatList(list => list.filter(l => l.id !== rId));
}
}; };
useEffect(() => { useEffect(() => {
@ -56,25 +129,54 @@ export const Create = () => {
} }
}, [chatList]); }, [chatList]);
const handleRefresh = async () => {
await axios.delete(`/bot/workflow/${id}/messages`);
setChatList([{
id: uniqueId('r'),
message: [
'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
}]);
setChatInput('');
setWorkflowContent('');
};
return ( return (
<> <>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}> <Grid item xs={12}>
<TextField value={workflowName} focused fullWidth variant="outlined" label="Workflow Name" <TextField value={formJson.name} fullWidth variant="outlined" label="Workflow Name"
placeholder="Please name your workflow" onChange={e => { placeholder="Please name your workflow" onChange={e => {
setWorkFlowName(e.target.value); setFormJson(json => ({
...json,
name: e.target.value
}));
}}/> }}/>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs> <Grid item xs>
<Box sx={{ border: 1, height: 600, borderRadius: 1, mt: 4, borderColor: 'rgba(0,0,0,0.23)' }}> <Box sx={{
border: 1,
height: 600,
borderRadius: 1,
mt: 4,
borderColor: 'rgba(255,255,255,0.23)',
position: 'relative'
}}>
<Tooltip title="Refresh AI">
<IconButton onClick={handleRefresh} style={{ position: 'absolute', right: 10, top: 10, zIndex: 5 }}>
<RefreshIcon/>
</IconButton>
</Tooltip>
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}> <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<List ref={chatContentRef} <List ref={chatContentRef}
style={{ flex: '1 1 auto', overflowY: 'auto', overflowX: 'hidden' }} style={{ flex: '1 1 auto', overflowY: 'auto', overflowX: 'hidden' }}
sx={{ p: 2 }}> sx={{ p: 2 }}>
{chatList.map(chat => <ChatItem isMe={chat.isMe} message={chat.message} key={chat.id}/>)} {chatList.map(chat => <ChatItem isMe={chat.isMe} message={chat.message} key={chat.id}
loading={chat.loading}/>)}
</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}>
@ -84,7 +186,7 @@ export const Create = () => {
Supported Action Types: Supported Action Types:
</Typography> </Typography>
{SupportedBlock.map(label => ( {SupportedBlock.map(label => (
<Chip label={label} key={label} size="small" clickable <Chip label={label} key={label} size="small" clickable color="primary"
onClick={handleClickChip(label)}/>))} onClick={handleClickChip(label)}/>))}
</Stack> </Stack>
</Grid> </Grid>
@ -96,6 +198,7 @@ export const Create = () => {
handleSend(); handleSend();
} }
}} }}
autoFocus
multiline maxRows={3} multiline maxRows={3}
value={chatInput}/> value={chatInput}/>
</Grid> </Grid>
@ -110,13 +213,13 @@ export const Create = () => {
</Box> </Box>
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
<Box sx={{ border: 1, height: 600, borderRadius: 1, p: 2, mt: 4, borderColor: 'rgba(0,0,0,0.23)' }}> <Box sx={{ border: 1, height: 600, borderRadius: 1, p: 2, mt: 4, borderColor: 'rgba(255,255,255,0.23)' }}>
Preview {!previewImg ? 'Workflow 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 }}/>
<Editor/>
</Grid> </Grid>
</Grid> </Grid>
@ -129,6 +232,7 @@ export const Create = () => {
p: 2, p: 2,
boxShadow: 2, boxShadow: 2,
borderTop: 1, borderTop: 1,
bgcolor: 'background.default',
borderColor: 'divider', borderColor: 'divider',
display: { xs: 'flex' }, justifyContent: "flex-end" display: { xs: 'flex' }, justifyContent: "flex-end"
}}> }}>

View File

@ -15,6 +15,7 @@ import Paper from '@mui/material/Paper';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import AccountCircle from '@mui/icons-material/AccountCircle'; import AccountCircle from '@mui/icons-material/AccountCircle';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { RCIcon } from './RCIcon';
const Layout = styled(Box)(({ theme }) => ( const Layout = styled(Box)(({ theme }) => (
{ {
@ -36,7 +37,7 @@ const Content = styled(Box)(({ theme }) => ({
export const Home = () => { export const Home = () => {
return ( return (
<Layout> <Layout>
<AppBar position="static"> <AppBar position="static" enableColorOnDark>
<Toolbar> <Toolbar>
<Typography <Typography
variant="h6" variant="h6"
@ -44,6 +45,7 @@ export const Home = () => {
component="div" component="div"
sx={{ flexGrow: 1, display: { xs: 'block' } }} sx={{ flexGrow: 1, display: { xs: 'block' } }}
> >
<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' } }}>

View File

@ -5,66 +5,130 @@
* *
* *
*/ */
import React, { useState } from "react"; import React, { useEffect, 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 } from "react-router-dom"; import { Link, 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 Container from '@mui/material/Container';
import Dialog from '@mui/material/Dialog';
interface ListItem { import DialogTitle from '@mui/material/DialogTitle';
id: string; import DialogActions from '@mui/material/DialogActions';
active: boolean; import DialogContent from '@mui/material/DialogContent';
title: string; import DialogContentText from '@mui/material/DialogContentText';
} import TextField from '@mui/material/TextField';
const Flex = styled(Box)(() => ({ const Flex = styled(Box)(() => ({
display: 'flex', display: 'flex',
flexWrap: 'wrap'
})); }));
interface Workflow {
description: string;
id: string;
name: string;
}
export const List = () => { export const List = () => {
const [list] = useState<ListItem[]>([{ const [list, setList] = useState<Workflow[]>([]);
id: 'a', const [showModal, setShowModal] = useState(false);
title: 'Workflow 1',
active: true const navigate = useNavigate();
}]); useEffect(() => {
axios.get<{
total: number
workflows: Workflow[]
}>('/api/public/wf/config/v1/account/10086/workflows').then(({ data }) => {
setList(data.workflows);
});
}, []);
const handleOpen = () => {
setShowModal(true);
};
const handleClose = () => {
setShowModal(false);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const formJson = Object.fromEntries((formData as any).entries());
axios.post('/bot/workflow', { name: formJson.name }).then(({ data }) => {
sessionStorage.setItem(`create-${data.visibleId}`, JSON.stringify({ ...data, name: formJson.name }));
handleClose();
navigate(`/create?id=${data.visibleId}`);
});
};
return ( return (
<> <Container maxWidth="lg">
<Flex style={{ justifyContent: 'flex-end' }}> <Flex style={{ justifyContent: 'flex-end' }}>
<Button startIcon={<AddIcon/>} variant="contained" href={`/create`}>Create Workflow</Button> <Button startIcon={<AddIcon/>} variant="contained" onClick={handleOpen}>Create Workflow</Button>
</Flex> </Flex>
<Flex sx={{ gap: 2 }}> <Flex sx={{ gap: 4, mt: 4 }}>
{list.map(item => ( {list.map(item => (
<Link <Link
key={item.id} key={item.id}
to={`/detail/${item.id}`} to={`/detail/${item.id}`}
> >
<Card sx={{ width: 280, borderRadius: 2, boxShadow: 2 }}> <Card sx={{ width: 263, borderRadius: 2, boxShadow: 2 }}>
<CardActionArea> <CardActionArea>
<CardContent> <CardContent>
<Typography gutterBottom variant="h5" component="div"> <Typography gutterBottom variant="h5" component="div">
{item.title} {item.name || 'My Workflow'}
</Typography> </Typography>
<Box sx={{ minHeight: 60 }}> <Box sx={{ minHeight: 60 }}>
<img style={{ width: 20, height: 20 }} <RCIcon/>
src="https://netstorage.ringcentral.com/appext/gallery-page/images/25e9f677-5605-4092-a6c3-69de29a81d8d.svg"
alt="rc"/>
</Box> </Box>
</CardContent> </CardContent>
<CardActions sx={{ display: { xs: 'flex' }, justifyContent: "flex-end" }}> <CardActions
sx={{ display: { xs: 'flex' }, justifyContent: "flex-end", borderTop: 1, borderColor: 'divider' }}>
<Switch defaultChecked/> <Switch defaultChecked/>
</CardActions> </CardActions>
</CardActionArea> </CardActionArea>
</Card> </Card>
</Link>))} </Link>))}
</Flex> </Flex>
</> <Dialog
open={showModal}
onClose={handleClose}
PaperProps={{
component: 'form',
onSubmit: handleSubmit,
}}
>
<DialogTitle>Create Workflow</DialogTitle>
<DialogContent>
<DialogContentText>
To create a workflow, please name your workflow first.
</DialogContentText>
<TextField
autoFocus
required
margin="dense"
id="name"
name="name"
defaultValue="My workflow"
label="Workflow Name"
fullWidth
focused
variant="standard"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button type="submit">Create</Button>
</DialogActions>
</Dialog>
</Container>
); );
}; };

16
src/RCIcon.tsx Normal file
View File

@ -0,0 +1,16 @@
/*
* @Author : Eleanor Mao
* @Date : 2024-04-11 10:41:51
* @LastEditTime : 2024-04-11 10:41:51
*
* Copyright © RingCentral. All rights reserved.
*/
import React, { CSSProperties, FC } from "react";
export const RCIcon: FC<{ size?: number; style?: CSSProperties }> = ({ size = 20, style }) => {
return (
<img style={{ width: size, height: size, ...style }}
src="https://netstorage.ringcentral.com/appext/gallery-page/images/25e9f677-5605-4092-a6c3-69de29a81d8d.svg"
alt="rc"/>
);
};

127
src/ajax.ts Normal file
View File

@ -0,0 +1,127 @@
/*
* @Author : Eleanor Mao
* @Date : 2024-04-11 10:14:35
* @LastEditTime : 2024-04-11 10:14:35
*
* Copyright © RingCentral. All rights reserved.
*/
import axios from 'axios';
import type { AxiosHeaders, AxiosError } from 'axios';
import qs from 'qs';
import { isFunction } from 'lodash';
type Params = {
[key: string]: string
}
interface Props {
urlParams?: {
[key: string]: string
};
paramsSerializer?: (params: Params) => string;
timeoutCallback?: (error: any) => void;
cancelCallback?: (error: any) => void;
arrayFormat?: 'repeat' | 'brackets' | 'comma' | 'indices';
allowDots?: boolean;
headers?: AxiosHeaders;
}
type IFunc = (url: string, data?: any, options?: Props) => Promise<any>
export interface Ajax {
post?: IFunc;
get?: IFunc;
put?: IFunc;
delete?: IFunc;
patch?: IFunc;
}
type METHODS = ['post', 'get', 'put', 'delete', 'patch']
const methods: METHODS = ['post', 'get', 'put', 'delete', 'patch'];
const ins: Ajax = {};
axios.defaults.headers.post['Content-Type'] = 'application/json';
axios.defaults.headers.get.Accept = 'application/json';
axios.defaults.timeout = 9500;
const format = function (
url: string,
obj: {
[key: string]: string
}
) {
let ret = url;
for (const key of Object.keys(obj)) {
ret = ret.replace('{' + key + '}', obj[key]);
}
return ret;
};
methods.forEach(method => {
ins[method] = (url, data, options = {}) => {
data = data || {};
if (options.urlParams) {
url = format(url, options.urlParams);
}
if (method === 'get') {
data = { params: data };
} else {
if (
!(data instanceof FormData) &&
options.headers &&
options.headers['Content-Type'] === 'application/x-www-form-urlencoded'
) {
data = qs.stringify(data, {
arrayFormat: options.arrayFormat ? options.arrayFormat : 'repeat',
allowDots: options.allowDots
});
}
data = { data };
}
const promise = new Promise((resolve, reject) => {
return axios({
url,
method,
...data,
withCredentials: true,
timeout: 9500,
...options
})
.then(response => {
return response.data;
})
.then(response => {
if (response instanceof Blob) {
resolve(response);
}
if (response.code === 0 || response.code === 200) {
resolve(response.data);
} else {
reject(response);
}
})
.catch((error: AxiosError) => {
if (axios.isCancel(error)) {
return isFunction(options.cancelCallback) ? options.cancelCallback(error) : undefined;
}
// https://github.com/axios/axios/issues/660
// @ts-ignore
if (error.code === 'ECONNABORTED' && isFunction(options.timeoutCallback)) {
return options.timeoutCallback(error);
}
reject(error);
});
});
return promise;
};
});

View File

@ -7,30 +7,33 @@ import React from 'react';
import ReactDOM from 'react-dom/client'; 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 {} from '@mui/material/colors';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
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: 'light', mode: 'dark',
primary: { primary: {
main: '#3f51b5', main: '#3f51b5',
},
tonalOffset: 0.6,
secondary: {
main: '#f50057',
},
background: {
paper: '#404041',
default: '#242425',
},
}, },
tonalOffset: 0.6,
secondary: {
main: '#f50057',
},
},
}); });
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline/> <CssBaseline/>
<App/> <App/>
</ThemeProvider> </ThemeProvider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -23,8 +23,59 @@ function compress(s: string, pre: string): string {
return "http://www.plantuml.com/plantuml" + pre + encode64_(compressed); return "http://www.plantuml.com/plantuml" + pre + encode64_(compressed);
} }
export function loadYml(input: string): string { export interface EventHandlerAction {
const definition = jsYaml.load(input); actionType: string;
let outputValue = transformDSL(definition); targetState?: string;
return compress(outputValue, '/svg/'); scriptSource?: string;
transitionName?:string
} }
export interface Action {
actionType: string;
scriptSource?: string;
id?: string;
question?: string;
score?: null;
token?: string;
type?: string;
content?:string;
subject?:string;
to?:string
}
export interface EventHandler {
eventType: string;
condition?: any;
actions: EventHandlerAction[];
}
export interface State {
name: string;
actions: Action[];
eventHandlers: EventHandler[];
}
export interface Definition {
states: State[];
workflowName: string;
}
export function Json2Preview(definition?: Definition): string {
if (!definition) return '';
try {
console.log(transformDSL(definition))
return compress(transformDSL(definition), '/svg/');
} catch (e) {
console.error(e);
return '';
}
}
export function DSL2Json(input: string): Definition {
return jsYaml.load(input) as Definition;
}
// @ts-ignore
window.ToJson = DSL2Json
// @ts-ignore
window.transform = transformDSL

View File

@ -1,24 +1,18 @@
import { Action, Definition, State } from "./loadYml";
const EVENT_COLOR = "#LightYellow"; const EVENT_COLOR = "#LightYellow";
const HTTP_COLOR = "#Peru"; 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 DEFAULT_COLOR = '#Grey;text;white';
const EVENT_NAME = ": Event"; const EVENT_NAME = ": Event";
const HTTP_NAME = ": Http"; 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 EXIT_NAME = ": Exit";
interface Action {
actionType: string;
}
interface State {
name: string;
actions: Action[];
eventHandlers: any;
}
function isHttp(actions: Action[]): boolean { function isHttp(actions: Action[]): boolean {
return actions return actions
@ -35,6 +29,10 @@ function isChat(actions: Action[]): boolean {
.some((action) => action.actionType === 'Chat'); .some((action) => action.actionType === 'Chat');
} }
function isExit(actions: Action[]): boolean {
return actions.some((action) => action.actionType === 'Exit');
}
function isEmailSender(actions: Action[]): boolean { function isEmailSender(actions: Action[]): boolean {
return actions return actions
.some((action) => action.actionType === "EmailSender"); .some((action) => action.actionType === "EmailSender");
@ -48,7 +46,7 @@ function isNotBlank(str: string): boolean {
return str !== undefined && str !== null && str.trim() !== ""; return str !== undefined && str !== null && str.trim() !== "";
} }
function drawState(eventState: Map<any, any>, state: State): string { function drawState(eventState: Map<string, Set<string>>, state: State): string {
let sb = ""; let sb = "";
const name = state.name; const name = state.name;
const actions = state.actions; const actions = state.actions;
@ -57,7 +55,7 @@ function drawState(eventState: Map<any, any>, state: State): string {
if (isEmpty(handlers)) { if (isEmpty(handlers)) {
throw Error(`state ${name} handlers must not empty`); throw Error(`state ${name} handlers must not empty`);
} }
let events = new Set(); let events = new Set<string>();
for (const handle of handlers) { for (const handle of handlers) {
const stateName = handle.eventType; const stateName = handle.eventType;
events.add(stateName); events.add(stateName);
@ -74,20 +72,21 @@ function drawState(eventState: Map<any, any>, state: State): string {
} 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 { } else {
throw Error(`state ${name} actions is incorrect`); console.log('====', name, actions);
// throw Error(`state ${name} actions is incorrect`);
} }
} }
return sb; return sb;
} }
function append(from:any, to:any, description:any) { function append(from: any, to: any, description: any) {
if (isNotBlank(description)) { if (isNotBlank(description)) {
description = ": " + description; description = ": " + description;
} }
return `${from} ---> ${to}${description}\n`; return `${from} ---> ${to || ''}${description}\n`;
} }
function wrapCond(cond:any) { function wrapCond(cond: any) {
if (isNotBlank(cond)) { if (isNotBlank(cond)) {
return "(" + cond + ")"; return "(" + cond + ")";
} else { } else {
@ -95,7 +94,7 @@ function wrapCond(cond:any) {
} }
} }
function drawTransitionCondition(eventCondition:any, transition:any) { function drawTransitionCondition(eventCondition: any, transition: any) {
const list = []; const list = [];
if (eventCondition !== undefined && eventCondition !== null) { if (eventCondition !== undefined && eventCondition !== null) {
list.push(eventCondition); list.push(eventCondition);
@ -111,7 +110,7 @@ function drawTransitionCondition(eventCondition:any, transition:any) {
return list.join(" && "); return list.join(" && ");
} }
function drawExitCondition(eventCondition:any, exit:any) { function drawExitCondition(eventCondition: any, exit: any) {
const list = []; const list = [];
if (eventCondition !== undefined && eventCondition !== null) { if (eventCondition !== undefined && eventCondition !== null) {
list.push(eventCondition); list.push(eventCondition);
@ -122,7 +121,7 @@ function drawExitCondition(eventCondition:any, exit:any) {
return list.join(" && "); return list.join(" && ");
} }
function drawStateLine(eventState:any, state:any) { function drawStateLine(eventState: Map<string, Set<string>>, state: State) {
const handlers = state.eventHandlers; const handlers = state.eventHandlers;
if (isEmpty(handlers)) { if (isEmpty(handlers)) {
return; return;
@ -141,13 +140,13 @@ function drawStateLine(eventState:any, state:any) {
const actions = handler.actions; const actions = handler.actions;
for (const action of actions) { for (const action of actions) {
const actionType = action.actionType; const actionType = action.actionType;
let targetState = action.targetState; let targetState = action.targetState || '';
if (actionType === "Transition") { if (actionType === "Transition") {
if (eventState.has(targetState)) { if (eventState.has(targetState)) {
let events = eventState.get(targetState); let events = eventState.get(targetState);
if (events.size > 1) { if (events && events.size > 1) {
continue; continue;
} else { } else if (events) {
targetState = events.values().next().value; targetState = events.values().next().value;
} }
} }
@ -162,15 +161,15 @@ function drawStateLine(eventState:any, state:any) {
return sb; return sb;
} }
export function transformDSL(definition: any): string { export function transformDSL(definition: Definition): string {
let sb = ""; let sb = "";
const eventState = new Map(); const eventState = new Map<string, Set<string>>();
const states = definition["states"]; const states = definition["states"] || [];
for (const state of states) { for (const state of states) {
sb += drawState(eventState, state); sb += drawState(eventState, state) || '';
} }
for (const state of states) { for (const state of states) {
sb += drawStateLine(eventState, state); sb += drawStateLine(eventState, state) || '';
} }
return `@startuml return `@startuml

View File

@ -2407,7 +2407,7 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837"
integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==
"@types/qs@*": "@types/qs@*", "@types/qs@^6.9.14":
version "6.9.14" version "6.9.14"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b"
integrity sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA== integrity sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==
@ -3123,6 +3123,15 @@ axe-core@=4.7.0:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf"
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
axios@^1.6.8:
version "1.6.8"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66"
integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^3.2.1: axobject-query@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
@ -5062,7 +5071,7 @@ flatted@^3.2.9:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
follow-redirects@^1.0.0: follow-redirects@^1.0.0, follow-redirects@^1.15.6:
version "1.15.6" version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
@ -5110,6 +5119,15 @@ form-data@^3.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
forwarded@0.2.0: forwarded@0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@ -8161,6 +8179,11 @@ proxy-addr@~2.0.7:
forwarded "0.2.0" forwarded "0.2.0"
ipaddr.js "1.9.1" ipaddr.js "1.9.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
psl@^1.1.33: psl@^1.1.33:
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
@ -8183,6 +8206,13 @@ qs@6.11.0:
dependencies: dependencies:
side-channel "^1.0.4" side-channel "^1.0.4"
qs@^6.12.0:
version "6.12.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.0.tgz#edd40c3b823995946a8a0b1f208669c7a200db77"
integrity sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==
dependencies:
side-channel "^1.0.6"
querystringify@^2.1.1: querystringify@^2.1.1:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"