chatbot-editor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +21 -0
- package/dist/App.d.ts +10 -0
- package/dist/App.js +22 -0
- package/dist/App.js.map +1 -0
- package/dist/Canva.d.ts +11 -0
- package/dist/Canva.js +10 -0
- package/dist/Canva.js.map +1 -0
- package/dist/Components/Answers.d.ts +8 -0
- package/dist/Components/Answers.js +18 -0
- package/dist/Components/Answers.js.map +1 -0
- package/dist/Components/Arrow.d.ts +13 -0
- package/dist/Components/Arrow.js +54 -0
- package/dist/Components/Arrow.js.map +1 -0
- package/dist/Components/Bubble.d.ts +6 -0
- package/dist/Components/Bubble.js +5 -0
- package/dist/Components/Bubble.js.map +1 -0
- package/dist/Components/Button.d.ts +9 -0
- package/dist/Components/Button.js +5 -0
- package/dist/Components/Button.js.map +1 -0
- package/dist/Components/Input.d.ts +12 -0
- package/dist/Components/Input.js +12 -0
- package/dist/Components/Input.js.map +1 -0
- package/dist/Components/Question.d.ts +18 -0
- package/dist/Components/Question.js +93 -0
- package/dist/Components/Question.js.map +1 -0
- package/dist/Components/Select.d.ts +13 -0
- package/dist/Components/Select.js +24 -0
- package/dist/Components/Select.js.map +1 -0
- package/dist/Components/SideBar.d.ts +12 -0
- package/dist/Components/SideBar.js +18 -0
- package/dist/Components/SideBar.js.map +1 -0
- package/dist/CurrentQuestionHook.d.ts +10 -0
- package/dist/CurrentQuestionHook.js +12 -0
- package/dist/CurrentQuestionHook.js.map +1 -0
- package/dist/TQuestion.d.ts +23 -0
- package/dist/TQuestion.js +2 -0
- package/dist/TQuestion.js.map +1 -0
- package/dist/UI.d.ts +20 -0
- package/dist/UI.js +116 -0
- package/dist/UI.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +108 -0
- package/dist/main.js.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -0
- package/eslint.config.js +26 -0
- package/index.html +12 -0
- package/package.json +38 -0
- package/src/App.tsx +43 -0
- package/src/Canva.tsx +23 -0
- package/src/Components/Answers.tsx +27 -0
- package/src/Components/Arrow.tsx +60 -0
- package/src/Components/Bubble.tsx +13 -0
- package/src/Components/Button.tsx +14 -0
- package/src/Components/Input.tsx +25 -0
- package/src/Components/Question.tsx +104 -0
- package/src/Components/Select.tsx +45 -0
- package/src/Components/SideBar.tsx +31 -0
- package/src/CurrentQuestionHook.tsx +22 -0
- package/src/TQuestion.ts +23 -0
- package/src/UI.tsx +138 -0
- package/src/index.ts +7 -0
- package/src/main.tsx +114 -0
- package/src/utils.ts +3 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +16 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +8 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
|
2
|
+
import Bubble from "./Bubble";
|
|
3
|
+
import Answers from "./Answers";
|
|
4
|
+
import {useCurrentQuestion} from "../CurrentQuestionHook";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import Arrow from "./Arrow";
|
|
7
|
+
import type {SetState} from "../utils.ts";
|
|
8
|
+
import type {TAction, TAnswers} from "../TQuestion.ts";
|
|
9
|
+
import {CiWarning} from "react-icons/ci";
|
|
10
|
+
|
|
11
|
+
interface QuestionProps {
|
|
12
|
+
id: string;
|
|
13
|
+
text: string | string[];
|
|
14
|
+
goto?: string;
|
|
15
|
+
answers?: TAnswers[];
|
|
16
|
+
action?: TAction;
|
|
17
|
+
reloadArrow: boolean;
|
|
18
|
+
setReloadArrow: SetState<boolean>;
|
|
19
|
+
position: {x: number, y: number};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function Question(props: QuestionProps) {
|
|
23
|
+
const container = useRef<HTMLDivElement>(null);
|
|
24
|
+
const [position, setPosition] = useState(props.position);
|
|
25
|
+
const [clicked, setClicked] = useState(false);
|
|
26
|
+
const [, setOpen] = useState(false);
|
|
27
|
+
const [, setQuestion] = useCurrentQuestion();
|
|
28
|
+
const onMouseDown = useCallback((e: React.MouseEvent) => {
|
|
29
|
+
if(e.button !== 0) return;
|
|
30
|
+
setClicked(true);
|
|
31
|
+
setOpen(true);
|
|
32
|
+
}, []);
|
|
33
|
+
const onMouseUp = useCallback((e: MouseEvent) => {
|
|
34
|
+
if(e.button !== 0) return;
|
|
35
|
+
if(e.target !== container.current && (container.current && !container.current.contains(e.target as Node))) return;
|
|
36
|
+
setClicked(false);
|
|
37
|
+
setOpen(prev => {
|
|
38
|
+
if(prev) {
|
|
39
|
+
setQuestion(props);
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
})
|
|
43
|
+
}, [props, setQuestion]);
|
|
44
|
+
const onMouseMove = useCallback((e: MouseEvent) => {
|
|
45
|
+
setOpen(false);
|
|
46
|
+
setPosition(prev => {
|
|
47
|
+
return {
|
|
48
|
+
x: prev.x + e.movementX,
|
|
49
|
+
y: prev.y + e.movementY
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
props.setReloadArrow(prev => !prev);
|
|
53
|
+
}, [setPosition, props]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if(clicked) {
|
|
56
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
57
|
+
}
|
|
58
|
+
return () => {
|
|
59
|
+
document.removeEventListener("mousemove", onMouseMove)
|
|
60
|
+
}
|
|
61
|
+
}, [clicked, onMouseMove]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
64
|
+
return () => document.removeEventListener("mouseup", onMouseUp);
|
|
65
|
+
}, [onMouseUp]);
|
|
66
|
+
const gotoPosition = useMemo(() => {
|
|
67
|
+
let pos: {x: number, y: number} | undefined = undefined;
|
|
68
|
+
if(props.goto || props.action?.goto) {
|
|
69
|
+
const toElement = document.getElementById(props.goto ?? props.action?.goto ?? "");
|
|
70
|
+
if(toElement) {
|
|
71
|
+
pos = {x: toElement.offsetLeft, y: toElement.offsetTop};
|
|
72
|
+
pos.x -= (position.x - position.x % 20);
|
|
73
|
+
pos.y -= (position.y - position.y % 20);
|
|
74
|
+
pos.y += 18
|
|
75
|
+
} else {
|
|
76
|
+
pos = undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return pos;
|
|
80
|
+
}, [props.goto, props.action?.goto, position, props.reloadArrow]);
|
|
81
|
+
return (
|
|
82
|
+
<div id={props.id} className={"w-50 h-64 bg-stone-900 border border-neutral-400/20 rounded z-10 absolute flex flex-col gap-1 pb-1"} style={{top: position.y - (position.y % 20), left: position.x - (position.x % 20)}} onMouseDown={onMouseDown} ref={container}>
|
|
83
|
+
<h2 title={props.id} className={"text-stone-100 m-1 min-h-8 bg-neutral-900 px-2 py-1 font-mono overflow-hidden text-ellipsis"}>{props.id}</h2>
|
|
84
|
+
<div className={"p-1 flex flex-col gap-1 max-h-32 overflow-x-hidden overflow-y-auto"}>
|
|
85
|
+
{ typeof props.text === "string" ? <Bubble text={props.text}/> : props.text.map((v, i) => <Bubble text={v} key={i}/>)}
|
|
86
|
+
</div>
|
|
87
|
+
<div className={"absolute -right-15 top-1 flex flex-col gap-1.5"}>
|
|
88
|
+
{props.answers?.map((v, i) => <Answers text={v.text} key={i} next={v.next} reloadArrows={props.reloadArrow}/>)}
|
|
89
|
+
</div>
|
|
90
|
+
{props.id === "start" && <div className={"absolute top-2 -left-4 w-6 h-6 rounded-full border-2 border-green-800 text-center flex items-center justify-center"}><div className={"bg-green-800 w-4 h-4 rounded-full"}/></div>}
|
|
91
|
+
{(props.goto || props.action?.goto) && <div>
|
|
92
|
+
<div className={"absolute top-2 -right-3 w-6 h-6 rounded-full border-2 border-violet-900 text-center flex justify-center items-center"}><div className={"bg-violet-900 w-4 h-4 rounded-full"}/></div>
|
|
93
|
+
{gotoPosition && <Arrow from={{x: 200, y: 20}} to={gotoPosition}/> || <CiWarning className={"text-orange-500 absolute top-3 -right-7"}/>}
|
|
94
|
+
</div>}
|
|
95
|
+
<div className={"grow"}/>
|
|
96
|
+
{ props.action && <div className={"font-mono text-stone-200 px-4 mx-1 rounded bg-neutral-800 overflow-hidden"}>
|
|
97
|
+
<p className={"font-sans"}>Action</p>
|
|
98
|
+
{props.action.action === "set" && <p><span>{props.action.property}</span> == {props.action.value}</p> }
|
|
99
|
+
{props.action.action === "ask" && <p><span>{props.action.property}</span> == user input</p> }
|
|
100
|
+
{props.action.action === "end" && <p>Chatbot end</p> }
|
|
101
|
+
</div>}
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {useMemo, useRef, useState} from "react";
|
|
3
|
+
import {clsx} from "clsx";
|
|
4
|
+
import {BsChevronDown} from "react-icons/bs";
|
|
5
|
+
|
|
6
|
+
export interface Option {
|
|
7
|
+
value: string;
|
|
8
|
+
label: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SelectProps {
|
|
12
|
+
options: Option[];
|
|
13
|
+
id?: string;
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (v: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function Select(props: SelectProps) {
|
|
19
|
+
const select = useRef<HTMLSelectElement | null>(null);
|
|
20
|
+
const id = useMemo(() => {
|
|
21
|
+
return props.id ?? (Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15));
|
|
22
|
+
}, [props.id]);
|
|
23
|
+
const [visible, setVisible] = useState(false);
|
|
24
|
+
return (
|
|
25
|
+
<label htmlFor={id}>
|
|
26
|
+
<div className={"relative bg-stone-800 rounded-lg h-8 flex justify-between items-center px-2"} onClick={() => setVisible(prev => !prev)}>
|
|
27
|
+
<p>{props.options.find(o => o.value == props.value)?.label ?? props.value}</p>
|
|
28
|
+
<button><BsChevronDown/></button>
|
|
29
|
+
<div className={clsx("absolute bg-stone-800 pt-2 -mt-2 z-0 w-full rounded-b-lg top-full left-0", visible || "hidden")}>
|
|
30
|
+
{props.options.map((v, i) =>
|
|
31
|
+
<p key={i} className={"p-1 hover:bg-stone-900"} onClick={() => {
|
|
32
|
+
if(select.current) {
|
|
33
|
+
select.current.value = v.value;
|
|
34
|
+
props.onChange(v.value);
|
|
35
|
+
}
|
|
36
|
+
}}>{v.label}</p>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<select className={"hidden"} id={id} onChange={e => props.onChange(e.target.value)} value={props.value} ref={select}>
|
|
41
|
+
{props.options.map((v, i) => <option value={v.value} key={i}>{v.label}</option>)}
|
|
42
|
+
</select>
|
|
43
|
+
</label>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {type ReactNode} from "react";
|
|
2
|
+
import type {SetState} from "../utils.ts";
|
|
3
|
+
import {clsx} from "clsx";
|
|
4
|
+
import {BsCheck, BsX} from "react-icons/bs";
|
|
5
|
+
import React from "react";
|
|
6
|
+
|
|
7
|
+
interface SideBarProps {
|
|
8
|
+
display: boolean;
|
|
9
|
+
setDisplay: SetState<boolean>;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
onClose?: () => void;
|
|
12
|
+
onValidate?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function SideBar(props: SideBarProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className={clsx("fixed transition w-1/3 h-screen top-0 left-0 bg-stone-900 z-50 flex flex-col", props.display && "translate-x-0" || "-translate-x-full")}>
|
|
18
|
+
<div className={"flex justify-end"}>
|
|
19
|
+
<button className={"mx-2 my-1 p-1 rounded cursor-pointer border border-stone-200/30 aspect-square w-8"} onClick={() => {
|
|
20
|
+
props.setDisplay(false);
|
|
21
|
+
if(props.onClose) {
|
|
22
|
+
props.onClose();
|
|
23
|
+
}}}>
|
|
24
|
+
<BsX className={"w-6 h-6"}/>
|
|
25
|
+
</button>
|
|
26
|
+
{props.onValidate && <button className={"mx-2 my-1 p-1 rounded cursor-pointer border border-stone-200/30 aspect-square w-8"} onClick={() => props.onValidate && props.onValidate()}><BsCheck className={"w-6 h-6"}/></button>}
|
|
27
|
+
</div>
|
|
28
|
+
<div className={"p-2 grow overflow-hidden"}>{props.children}</div>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {createContext, type ReactNode, useContext, useState} from "react";
|
|
3
|
+
import type TQuestion from "./TQuestion.ts";
|
|
4
|
+
import type {SetState} from "./utils.ts";
|
|
5
|
+
|
|
6
|
+
const CurrentQuestionContext = createContext<[TQuestion | null, SetState<TQuestion | null>]>([null, () => {}]);
|
|
7
|
+
|
|
8
|
+
interface CurrentQuestionProviderProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CurrentQuestionProvider(props: CurrentQuestionProviderProps) {
|
|
13
|
+
const [question, setQuestion] = useState<TQuestion | null>(null);
|
|
14
|
+
return <CurrentQuestionContext.Provider value={[question, setQuestion]}>
|
|
15
|
+
{props.children}
|
|
16
|
+
</CurrentQuestionContext.Provider>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
20
|
+
export function useCurrentQuestion() {
|
|
21
|
+
return useContext(CurrentQuestionContext);
|
|
22
|
+
}
|
package/src/TQuestion.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default interface TQuestion {
|
|
2
|
+
id: string;
|
|
3
|
+
text: string | string[];
|
|
4
|
+
answers?: TAnswers[];
|
|
5
|
+
goto?: string;
|
|
6
|
+
action?: TAction;
|
|
7
|
+
position: {x: number, y: number};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TAnswers {
|
|
11
|
+
text: string;
|
|
12
|
+
next: string;
|
|
13
|
+
action?: TAction
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TAction {
|
|
17
|
+
action: TActionValue
|
|
18
|
+
property?: string;
|
|
19
|
+
value?: any;
|
|
20
|
+
goto?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TActionValue = "set" | "ask" | "end";
|
package/src/UI.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import SideBar from "./Components/SideBar";
|
|
2
|
+
import {useEffect, useState} from "react";
|
|
3
|
+
import {useCurrentQuestion} from "./CurrentQuestionHook";
|
|
4
|
+
import type TQuestion from "./TQuestion.ts";
|
|
5
|
+
import type { TAnswers, TAction } from "./TQuestion.ts";
|
|
6
|
+
import type {SetState} from "./utils.ts";
|
|
7
|
+
import Button from "./Components/Button";
|
|
8
|
+
import Input from "./Components/Input";
|
|
9
|
+
import {clsx} from "clsx";
|
|
10
|
+
import Select from "./Components/Select";
|
|
11
|
+
import {BsPlus} from "react-icons/bs";
|
|
12
|
+
import {LuSave} from "react-icons/lu";
|
|
13
|
+
import {MdOutlineFileOpen} from "react-icons/md";
|
|
14
|
+
import {BiDownload, BiUpload} from "react-icons/bi";
|
|
15
|
+
import React from "react";
|
|
16
|
+
|
|
17
|
+
interface UIProps {
|
|
18
|
+
questions: TQuestion[];
|
|
19
|
+
setQuestions: SetState<TQuestion[]>;
|
|
20
|
+
addQuestion: (initPos: {x: number, y: number}) => void;
|
|
21
|
+
editQuestion: (id: string, data: TQuestion) => void;
|
|
22
|
+
deleteQuestion: (id: string) => void;
|
|
23
|
+
setReloadArrow: SetState<boolean>;
|
|
24
|
+
save?: (questions: TQuestion[]) => void;
|
|
25
|
+
load?: () => Promise<TQuestion[]>;
|
|
26
|
+
export?: (questions: TQuestion[]) => void;
|
|
27
|
+
import?: () => Promise<TQuestion[]>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function UI(props: UIProps) {
|
|
31
|
+
const [question, setQuestion] = useCurrentQuestion();
|
|
32
|
+
const [id, setId] = useState("");
|
|
33
|
+
const [goto, setGoto] = useState<string>();
|
|
34
|
+
const [text, setText] = useState<string[]>([""]);
|
|
35
|
+
const [answers, setAnswers] = useState<TAnswers[]>([]);
|
|
36
|
+
const [action, setAction] = useState<TAction | undefined>(undefined);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if(question) {
|
|
39
|
+
setId(question.id);
|
|
40
|
+
if(typeof question.text === "string") {
|
|
41
|
+
setText([question.text]);
|
|
42
|
+
} else {
|
|
43
|
+
setText(question.text);
|
|
44
|
+
}
|
|
45
|
+
setGoto(question.goto);
|
|
46
|
+
setAnswers(question.answers ?? []);
|
|
47
|
+
setAction(question.action);
|
|
48
|
+
}
|
|
49
|
+
}, [question]);
|
|
50
|
+
return (
|
|
51
|
+
<div className={"fixed top-0 left-0 w-0 h-0 overflow-visible text-white z-10"}>
|
|
52
|
+
<SideBar display={question !== null} setDisplay={(value) => { if(typeof value === "function") value = value(question !== null); if(!value) setQuestion(null) }} onClose={() => setQuestion(null)} onValidate={() => {
|
|
53
|
+
const t = text.length === 1 ? text[0] : text;
|
|
54
|
+
props.editQuestion(question?.id ?? "", {id: id, text: t, goto: goto, answers: answers, position: {x: 0, y: 0}, action: action});
|
|
55
|
+
setQuestion(null);
|
|
56
|
+
}}>
|
|
57
|
+
<div className={"flex flex-col gap-2 h-full pb-8 overflow-y-auto overflow-x-hidden"}>
|
|
58
|
+
<Input type={"text"} value={id} onChange={(e) => setId(e.target.value)} label={"ID"}/>
|
|
59
|
+
<div className={"bg-stone-950 p-2 rounded-lg flex flex-col gap-1"}>
|
|
60
|
+
<p className={"text-lg font-bold "}>Text</p>
|
|
61
|
+
{text.map((value, index) => <div className={"flex gap-1"} key={index}>
|
|
62
|
+
<Input type={"text"} value={value} onChange={(e) => setText(prev => prev.map((v, i) => i === index ? e.target.value : v))}/>
|
|
63
|
+
<Button onClick={() => setText(prev => prev.filter((_, i) => i !== index))}>-</Button>
|
|
64
|
+
</div>)}
|
|
65
|
+
<Button onClick={() => setText(prev => [...prev, ""])}>+</Button>
|
|
66
|
+
</div>
|
|
67
|
+
<div className={"bg-stone-950 p-2 rounded-lg flex flex-col gap-1"}>
|
|
68
|
+
<p className={"text-lg font-bold"}>Answers</p>
|
|
69
|
+
{answers.map((value, index) => <div className={"flex ga flex-col p-1"} key={index}>
|
|
70
|
+
<div className={"flex gap-1 items-end"}>
|
|
71
|
+
<Input type={"text"} value={value.text} onChange={(e) => setAnswers(prev => prev.map((v, i) => i === index ? {...v, text: e.target.value} : v))} label={"Text"}/>
|
|
72
|
+
<Input type={"text"} value={value.next} onChange={(e) => setAnswers(prev => prev.map((v, i) => i === index ? {...v, next: e.target.value} : v))} label={"Next"}/>
|
|
73
|
+
<Button onClick={() => setAnswers(prev => prev.filter((_, i) => i !== index))}>-</Button>
|
|
74
|
+
</div>
|
|
75
|
+
<div className={"flex flex-col gap-1"}>
|
|
76
|
+
<p>Action</p>
|
|
77
|
+
<Select options={[{value: "none", label: "None"}, {value: "set", label: "Set"}]} value={value.action?.action ?? "none"} onChange={ (v) => v === "none" ?
|
|
78
|
+
setAnswers(prev => prev.map((v, i) => i === index ? {...v, action: undefined} : v)) :
|
|
79
|
+
setAnswers(prev => prev.map((v, i) => i === index ? {...v, action: {action: "set"}} : v))
|
|
80
|
+
}/>
|
|
81
|
+
<Input className={clsx(value.action === undefined && "hidden")} label={"Property"} value={value.action?.property ?? ""} onChange={e => setAnswers(prev => prev.map((v, i) => i === index ? {...v, action: {action: value.action!.action, property: e.target.value, value: value.action?.value}} : v))}/>
|
|
82
|
+
<Input className={clsx(value.action === undefined && "hidden")} label={"Value"} value={value.action?.value ?? ""} onChange={e => setAnswers(prev => prev.map((v, i) => i === index ? {...v, action: {action: value.action!.action, value: e.target.value, property: value.action?.property}} : v))}/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>)}
|
|
85
|
+
<Button disabled={goto !== undefined} onClick={() => setAnswers(prev => [...prev, {text: "", next: ""}])}>+</Button>
|
|
86
|
+
</div>
|
|
87
|
+
<div className={"bg-stone-950 p-2 rounded-lg flex flex-col gap-1"}>
|
|
88
|
+
<p className={"text-lg font-bold"}>Action</p>
|
|
89
|
+
<Select options={[{value: "ask", label: "Ask"}, {value: "set", label: "Set"}, {value: "end", label: "End"}, {value: "none", label: "None"}]} value={action?.action ?? "none"} onChange={(v) => {
|
|
90
|
+
switch (v) {
|
|
91
|
+
case "ask":
|
|
92
|
+
setAction(prev => ({action: "ask", property: prev?.property, value: undefined}));
|
|
93
|
+
break;
|
|
94
|
+
case "end":
|
|
95
|
+
setAction({action: "end", property: undefined, value: undefined});
|
|
96
|
+
break;
|
|
97
|
+
case "set":
|
|
98
|
+
setAction(prev => ({action: "set", property: prev?.property, value: prev?.value}));
|
|
99
|
+
break;
|
|
100
|
+
case "none":
|
|
101
|
+
setAction(undefined);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}}/>
|
|
105
|
+
<Input className={clsx((action === undefined || action.action === "end") && "hidden")} type={"text"} label={"Property"} value={action?.property ?? ""} onChange={(e) => setAction(prev => prev ? ({...prev, property: e.target.value === "" ? undefined : e.target.value}) : undefined)}/>
|
|
106
|
+
<Input className={clsx((action === undefined || action.action !== "set") && "hidden")} type={"text"} label={"Value"} value={action?.value ?? ""} onChange={(e) => setAction(prev => prev ? ({...prev, value: e.target.value === "" ? undefined : e.target.value}) : undefined)}/>
|
|
107
|
+
<Input className={clsx((action === undefined || action.action !== "ask") && "hidden")} type={"text"} label={"Goto"} value={action?.goto ?? ""} onChange={(e) => setAction(prev => prev ? ({...prev, goto: e.target.value === "" ? undefined : e.target.value}) : undefined)}/>
|
|
108
|
+
</div>
|
|
109
|
+
<Input disabled={answers.length !== 0} type={"text"} label={"Goto"} value={goto ?? ""} onChange={(e) => setGoto(e.target.value === "" ? undefined : e.target.value)}/>
|
|
110
|
+
<div className={"grow"}/>
|
|
111
|
+
<Button onClick={() => {
|
|
112
|
+
props.deleteQuestion(question?.id ?? "")
|
|
113
|
+
setQuestion(null);
|
|
114
|
+
}}>Delete</Button>
|
|
115
|
+
</div>
|
|
116
|
+
</SideBar>
|
|
117
|
+
<div className={"p-2 fixed right-8 top-0 flex gap-2"}>
|
|
118
|
+
<Button onClick={() => props.addQuestion({x: 20, y: 20})}><BsPlus className={"w-6 h-6"}/></Button>
|
|
119
|
+
{props.save && <Button onClick={() => props.save && props.save(props.questions)}><LuSave className={"w-6 h-6"}/></Button>}
|
|
120
|
+
{props.load && <Button onClick={async () => {
|
|
121
|
+
if(props.load) {
|
|
122
|
+
const questions = await props.load();
|
|
123
|
+
props.setQuestions(questions);
|
|
124
|
+
props.setReloadArrow(prev => !prev);
|
|
125
|
+
}
|
|
126
|
+
}}><MdOutlineFileOpen className={"w-6 h-6"}/></Button>}
|
|
127
|
+
{props.export && <Button onClick={() => props.export && props.export(props.questions)}><BiDownload className={"w-6 h-6"}/></Button>}
|
|
128
|
+
{ props.import &&<Button onClick={async () => {
|
|
129
|
+
if(props.import) {
|
|
130
|
+
const questions = await props.import();
|
|
131
|
+
props.setQuestions(questions);
|
|
132
|
+
props.setReloadArrow(prev => !prev);
|
|
133
|
+
}
|
|
134
|
+
}}><BiUpload className={"w-6 h-6"}/></Button>}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
package/src/index.ts
ADDED
package/src/main.tsx
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {StrictMode} from 'react'
|
|
2
|
+
import {createRoot} from 'react-dom/client'
|
|
3
|
+
import './index.css'
|
|
4
|
+
import App from "./App";
|
|
5
|
+
import type TQuestion from "./TQuestion.ts";
|
|
6
|
+
import type { TAction, TAnswers } from "./TQuestion.ts";
|
|
7
|
+
import React from "react";
|
|
8
|
+
|
|
9
|
+
const save = (questions: TQuestion[]) => {
|
|
10
|
+
const save = questions.map(q => {
|
|
11
|
+
const pos = document.getElementById(q.id)!.getBoundingClientRect();
|
|
12
|
+
q.position.x = pos.x;
|
|
13
|
+
q.position.y = pos.y;
|
|
14
|
+
return q;
|
|
15
|
+
});
|
|
16
|
+
const file = URL.createObjectURL(new Blob([JSON.stringify(save)], {type: "application/json"}));
|
|
17
|
+
const a = document.createElement("a");
|
|
18
|
+
a.href = file;
|
|
19
|
+
a.download = "questions.json";
|
|
20
|
+
a.click();
|
|
21
|
+
URL.revokeObjectURL(file);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const download = (questions: TQuestion[]) => {
|
|
25
|
+
const result = {} as Record<string, {question: string | string[], goto?: string, answers: TAnswers[] | undefined, action?: TAction}>;
|
|
26
|
+
questions.forEach(q => result[q.id] = {
|
|
27
|
+
question: q.text,
|
|
28
|
+
goto: q.goto,
|
|
29
|
+
action: q.action,
|
|
30
|
+
answers: q.answers
|
|
31
|
+
});
|
|
32
|
+
const file = URL.createObjectURL(new Blob([JSON.stringify(result)], {type: "application/json"}));
|
|
33
|
+
const a = document.createElement("a");
|
|
34
|
+
a.href = file;
|
|
35
|
+
a.download = "questions.json";
|
|
36
|
+
a.click();
|
|
37
|
+
URL.revokeObjectURL(file);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const load = async () => {
|
|
41
|
+
const fileLoader = document.createElement("input");
|
|
42
|
+
fileLoader.type = "file";
|
|
43
|
+
fileLoader.accept = ".json";
|
|
44
|
+
const p = new Promise<TQuestion[]>((resolve, reject) => {
|
|
45
|
+
fileLoader.addEventListener("change", () => {
|
|
46
|
+
if(fileLoader.files?.length === 1) {
|
|
47
|
+
fileLoader.files[0].text().then(v => {
|
|
48
|
+
const questions = JSON.parse(v);
|
|
49
|
+
if(typeof questions === "object" && questions instanceof Array) {
|
|
50
|
+
const validate = questions.filter(q => {
|
|
51
|
+
return typeof q["id"] === "string" && typeof q["text"] === "string" && typeof q["position"] === "object" && typeof q["position"]["x"] === "number" && typeof q["position"]["y"] === "number"
|
|
52
|
+
});
|
|
53
|
+
if(validate.length !== questions.length) {
|
|
54
|
+
console.error("Invalid file format");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
resolve(questions as TQuestion[]);
|
|
58
|
+
} else {
|
|
59
|
+
reject("Invalid file format");
|
|
60
|
+
}
|
|
61
|
+
}).catch(reject);
|
|
62
|
+
} else {
|
|
63
|
+
reject("You must select only one file");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
fileLoader.click();
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const importFile = async () => {
|
|
72
|
+
const fileLoader = document.createElement("input");
|
|
73
|
+
fileLoader.type = "file";
|
|
74
|
+
fileLoader.accept = ".json";
|
|
75
|
+
const p = new Promise<TQuestion[]>((resolve, reject) => {
|
|
76
|
+
fileLoader.addEventListener("change", () => {
|
|
77
|
+
if (fileLoader.files?.length === 1) {
|
|
78
|
+
fileLoader.files[0].text().then(v => {
|
|
79
|
+
const questions = JSON.parse(v);
|
|
80
|
+
if (typeof questions === "object") {
|
|
81
|
+
const validate = Object.values<any>(questions).filter(q => {
|
|
82
|
+
return typeof q["question"] === "string" || (typeof q["question"] === "object" && q["question"] instanceof Array)
|
|
83
|
+
});
|
|
84
|
+
if (validate.length !== Object.values(questions).length) {
|
|
85
|
+
reject("Invalid file format");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const tmp = Object.keys(questions).map((id, index) => {
|
|
89
|
+
return {
|
|
90
|
+
id: id,
|
|
91
|
+
text: questions[id]["question"],
|
|
92
|
+
goto: questions[id]["goto"] ?? undefined,
|
|
93
|
+
answers: questions[id]["answers"] ?? [],
|
|
94
|
+
position: {x: index % 5 * 260 + 20, y: Math.floor(index / 5) * 300 + 20},
|
|
95
|
+
action: questions[id]["action"] ?? undefined
|
|
96
|
+
} as TQuestion;
|
|
97
|
+
});
|
|
98
|
+
resolve(tmp);
|
|
99
|
+
} else {
|
|
100
|
+
reject("Invalid file format");
|
|
101
|
+
}
|
|
102
|
+
}).catch(reject);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
fileLoader.click();
|
|
107
|
+
return p;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
createRoot(document.getElementById('root')!).render(
|
|
111
|
+
<StrictMode>
|
|
112
|
+
<App save={save} load={load} export={download} import={importFile}/>
|
|
113
|
+
</StrictMode>,
|
|
114
|
+
)
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["src"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"jsx": "react",
|
|
14
|
+
"moduleResolution": "bundler"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|