From 51d620de39e56faacaad71a2c85428e7e68d68cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Katar=C3=ADna=20Platkov=C3=A1?= <xplatkov@fi.muni.cz>
Date: Sat, 28 Sep 2024 10:37:30 +0200
Subject: [PATCH] Editor - tools and emails

---
 frontend/src/editor/EditorPage/index.tsx      |  10 +-
 .../src/editor/EmailAddressForm/index.tsx     |   4 +-
 .../index.tsx}                                |   8 +-
 .../editor/EmailAddresses/EmailAddress.tsx    |  81 ++++++++++
 frontend/src/editor/EmailAddresses/index.tsx  |  20 +++
 .../editor/EmailTemplateFormContent/index.tsx |  52 +++++++
 .../editor/EmailTemplateFormDialog/index.tsx  | 127 ++++++++++++++++
 .../editor/EmailTemplates/EmailTemplate.tsx   |  54 +++++++
 frontend/src/editor/EmailTemplates/index.tsx  |  35 +++++
 .../InjectSpecification/EmailInjectForm.tsx   |   2 +-
 .../EmailTemplateForm.tsx                     |  35 ++---
 .../ToolResponseForm.tsx                      |  48 ++----
 frontend/src/editor/Navbar/index.tsx          |   1 +
 frontend/src/editor/ToolForm/index.tsx        |  16 +-
 .../editor/ToolResponseFormContent/index.tsx  |  58 ++++++++
 .../editor/ToolResponseFormDialog/index.tsx   | 138 ++++++++++++++++++
 .../src/editor/ToolResponses/ToolResponse.tsx |  57 ++++++++
 frontend/src/editor/ToolResponses/index.tsx   |  29 ++++
 .../index.tsx}                                |   8 +-
 frontend/src/editor/Tools/Tool.tsx            |  76 ++++++++++
 frontend/src/editor/Tools/index.tsx           |  18 +++
 frontend/src/editor/indexeddb/types.tsx       |   4 +-
 frontend/src/pages/editor/create/emails.tsx   |  26 ++++
 .../pages/editor/create/final-information.tsx |   2 +-
 .../create/inject-specification/index.tsx     |   3 +-
 frontend/src/pages/editor/create/other.tsx    |  28 ++++
 frontend/src/pages/editor/create/tools.tsx    |  26 ++++
 frontend/src/router.ts                        |   3 +
 28 files changed, 874 insertions(+), 95 deletions(-)
 rename frontend/src/editor/{LearningActivitySpecification/EmailAddressSelector.tsx => EmailAddressSelector/index.tsx} (86%)
 create mode 100644 frontend/src/editor/EmailAddresses/EmailAddress.tsx
 create mode 100644 frontend/src/editor/EmailAddresses/index.tsx
 create mode 100644 frontend/src/editor/EmailTemplateFormContent/index.tsx
 create mode 100644 frontend/src/editor/EmailTemplateFormDialog/index.tsx
 create mode 100644 frontend/src/editor/EmailTemplates/EmailTemplate.tsx
 create mode 100644 frontend/src/editor/EmailTemplates/index.tsx
 create mode 100644 frontend/src/editor/ToolResponseFormContent/index.tsx
 create mode 100644 frontend/src/editor/ToolResponseFormDialog/index.tsx
 create mode 100644 frontend/src/editor/ToolResponses/ToolResponse.tsx
 create mode 100644 frontend/src/editor/ToolResponses/index.tsx
 rename frontend/src/editor/{LearningActivitySpecification/ToolSelector.tsx => ToolSelector/index.tsx} (88%)
 create mode 100644 frontend/src/editor/Tools/Tool.tsx
 create mode 100644 frontend/src/editor/Tools/index.tsx
 create mode 100644 frontend/src/pages/editor/create/emails.tsx
 create mode 100644 frontend/src/pages/editor/create/other.tsx
 create mode 100644 frontend/src/pages/editor/create/tools.tsx

diff --git a/frontend/src/editor/EditorPage/index.tsx b/frontend/src/editor/EditorPage/index.tsx
index 63d77e9f4..afb081b54 100644
--- a/frontend/src/editor/EditorPage/index.tsx
+++ b/frontend/src/editor/EditorPage/index.tsx
@@ -53,8 +53,8 @@ const EditorPage: FC<EditorPageProps> = ({
           style={{
             marginRight: '0.5rem',
           }}
-        />{' '}
-        {nextVisible && (
+        />
+        {(nextVisible === undefined || nextVisible) && (
           <Button
             type='button'
             onClick={() => nav(nextPath || '/')}
@@ -69,10 +69,4 @@ const EditorPage: FC<EditorPageProps> = ({
   )
 }
 
-EditorPage.defaultProps = {
-  nextPath: '/',
-  nextDisabled: false,
-  nextVisible: true,
-}
-
 export default memo(EditorPage)
diff --git a/frontend/src/editor/EmailAddressForm/index.tsx b/frontend/src/editor/EmailAddressForm/index.tsx
index 35cc8d43c..dde59bee9 100644
--- a/frontend/src/editor/EmailAddressForm/index.tsx
+++ b/frontend/src/editor/EmailAddressForm/index.tsx
@@ -130,7 +130,7 @@ const EmailAddressForm: FC<EmailAddressFormProps> = ({
           actions={
             emailAddressInfo ? (
               <Button
-                disabled={!address || !description}
+                disabled={!address}
                 onClick={() =>
                   handleUpdateButton({
                     id: emailAddressInfo.id,
@@ -146,7 +146,7 @@ const EmailAddressForm: FC<EmailAddressFormProps> = ({
               />
             ) : (
               <Button
-                disabled={!address || !description}
+                disabled={!address}
                 onClick={() =>
                   handleAddButton({
                     address,
diff --git a/frontend/src/editor/LearningActivitySpecification/EmailAddressSelector.tsx b/frontend/src/editor/EmailAddressSelector/index.tsx
similarity index 86%
rename from frontend/src/editor/LearningActivitySpecification/EmailAddressSelector.tsx
rename to frontend/src/editor/EmailAddressSelector/index.tsx
index bd3d25631..5c9e53c9f 100644
--- a/frontend/src/editor/LearningActivitySpecification/EmailAddressSelector.tsx
+++ b/frontend/src/editor/EmailAddressSelector/index.tsx
@@ -1,7 +1,7 @@
 import type { OptionProps } from '@blueprintjs/core'
 import { HTMLSelect, Label } from '@blueprintjs/core'
 import { useLiveQuery } from 'dexie-react-hooks'
-import { memo, useMemo, type FC } from 'react'
+import { memo, useEffect, useMemo, type FC } from 'react'
 import EmailAddressForm from '../EmailAddressForm'
 import { db } from '../indexeddb/db'
 import type { EmailAddressInfo } from '../indexeddb/types'
@@ -34,6 +34,12 @@ const EmailAddressSelector: FC<EmailAddressFormProps> = ({
     }))
   }, [emailAddresses])
 
+  useEffect(() => {
+    if (!emailAddressId && emailAddresses && emailAddresses.length > 0) {
+      onChange(Number(emailAddresses[0].id))
+    }
+  }, [emailAddresses, emailAddressId])
+
   return (
     <div style={{ display: 'flex', width: '100%' }}>
       <Label style={{ flexGrow: '1' }}>
diff --git a/frontend/src/editor/EmailAddresses/EmailAddress.tsx b/frontend/src/editor/EmailAddresses/EmailAddress.tsx
new file mode 100644
index 000000000..f01aff35f
--- /dev/null
+++ b/frontend/src/editor/EmailAddresses/EmailAddress.tsx
@@ -0,0 +1,81 @@
+import { Button, ButtonGroup, Card } from '@blueprintjs/core'
+import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
+import type { FC } from 'react'
+import { memo, useCallback } from 'react'
+import EmailAddressForm from '../EmailAddressForm'
+import EmailTemplateFormDialog from '../EmailTemplateFormDialog'
+import EmailTemplates from '../EmailTemplates'
+import { deleteEmailAddress } from '../indexeddb/operations'
+import type { EmailAddressInfo } from '../indexeddb/types'
+
+interface EmailAddressProps {
+  emailAddress: EmailAddressInfo
+}
+
+const EmailAddressItem: FC<EmailAddressProps> = ({ emailAddress }) => {
+  const { notify } = useNotifyContext()
+
+  const handleDeleteButton = useCallback(
+    async (emailAddressInfo: EmailAddressInfo) => {
+      try {
+        await deleteEmailAddress(emailAddressInfo.id)
+      } catch (err) {
+        notify(
+          `Failed to delete email address '${emailAddressInfo.address}': ${err}`,
+          {
+            intent: 'danger',
+          }
+        )
+      }
+    },
+    [notify]
+  )
+
+  return (
+    <Card style={{ flexDirection: 'column' }}>
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'space-between',
+          alignItems: 'center',
+          width: '100%',
+          padding: '0rem 1rem 1rem 0',
+        }}
+      >
+        <span style={{ height: '100%', flexGrow: 1 }}>
+          {emailAddress.address}
+        </span>
+        <ButtonGroup>
+          <EmailAddressForm
+            emailAddressInfo={emailAddress}
+            buttonProps={{
+              minimal: true,
+              icon: 'edit',
+              style: { marginRight: '1rem' },
+            }}
+          />
+          <Button
+            minimal
+            icon='cross'
+            onClick={() => handleDeleteButton(emailAddress)}
+          />
+        </ButtonGroup>
+      </div>
+      <div style={{ width: '100%', paddingLeft: '2rem' }}>
+        <EmailTemplates emailAddressId={Number(emailAddress.id)} />
+        <EmailTemplateFormDialog
+          emailAddressId={Number(emailAddress.id)}
+          buttonProps={{
+            minimal: true,
+            text: 'Add template',
+            alignText: 'left',
+            icon: 'plus',
+            style: { padding: '1rem', width: '100%' },
+          }}
+        />
+      </div>
+    </Card>
+  )
+}
+
+export default memo(EmailAddressItem)
diff --git a/frontend/src/editor/EmailAddresses/index.tsx b/frontend/src/editor/EmailAddresses/index.tsx
new file mode 100644
index 000000000..56e82bab1
--- /dev/null
+++ b/frontend/src/editor/EmailAddresses/index.tsx
@@ -0,0 +1,20 @@
+import { db } from '@/editor/indexeddb/db'
+import type { EmailAddressInfo } from '@/editor/indexeddb/types'
+import { CardList } from '@blueprintjs/core'
+import { useLiveQuery } from 'dexie-react-hooks'
+import { memo } from 'react'
+import EmailAddress from './EmailAddress'
+
+const EmailAddresses = () => {
+  const emailAddresses = useLiveQuery(() => db.emailAddresses.toArray(), [], [])
+
+  return (
+    <CardList>
+      {emailAddresses?.map((emailAddress: EmailAddressInfo) => (
+        <EmailAddress key={emailAddress.id} emailAddress={emailAddress} />
+      ))}
+    </CardList>
+  )
+}
+
+export default memo(EmailAddresses)
diff --git a/frontend/src/editor/EmailTemplateFormContent/index.tsx b/frontend/src/editor/EmailTemplateFormContent/index.tsx
new file mode 100644
index 000000000..00b9f9e4d
--- /dev/null
+++ b/frontend/src/editor/EmailTemplateFormContent/index.tsx
@@ -0,0 +1,52 @@
+import { InputGroup, Label, TextArea } from '@blueprintjs/core'
+import { memo, type FC } from 'react'
+import EmailAddressSelector from '../EmailAddressSelector'
+
+interface EmailTemplateFormProps {
+  context: string
+  onContextChange: (value: string) => void
+  content: string
+  onContentChange: (value: string) => void
+  emailAddressId: number
+  onEmailAddressIdChange: (value: number) => void
+}
+
+const EmailTemplateForm: FC<EmailTemplateFormProps> = ({
+  context,
+  onContextChange,
+  content,
+  onContentChange,
+  emailAddressId,
+  onEmailAddressIdChange,
+}) => (
+  <div>
+    <EmailAddressSelector
+      emailAddressId={emailAddressId}
+      onChange={id => onEmailAddressIdChange(id)}
+    />
+    <Label>
+      Context
+      <InputGroup
+        placeholder='Input text'
+        value={context}
+        onChange={e => onContextChange(e.target.value)}
+      />
+    </Label>
+    <Label>
+      Content
+      <TextArea
+        value={content}
+        style={{
+          width: '100%',
+          height: '10rem',
+          resize: 'none',
+          overflowY: 'auto',
+        }}
+        placeholder='Input text'
+        onChange={e => onContentChange(e.target.value)}
+      />
+    </Label>
+  </div>
+)
+
+export default memo(EmailTemplateForm)
diff --git a/frontend/src/editor/EmailTemplateFormDialog/index.tsx b/frontend/src/editor/EmailTemplateFormDialog/index.tsx
new file mode 100644
index 000000000..604aa5254
--- /dev/null
+++ b/frontend/src/editor/EmailTemplateFormDialog/index.tsx
@@ -0,0 +1,127 @@
+import type { ButtonProps } from '@blueprintjs/core'
+import { Button, Dialog, DialogBody, DialogFooter } from '@blueprintjs/core'
+import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
+import { memo, useCallback, useEffect, useState, type FC } from 'react'
+import EmailTemplateFormContent from '../EmailTemplateFormContent'
+import { addEmailTemplate, updateEmailTemplate } from '../indexeddb/operations'
+import type { EmailTemplate } from '../indexeddb/types'
+
+interface EmailTemplateFormDialogProps {
+  template?: EmailTemplate
+  emailAddressId: number
+  buttonProps: ButtonProps
+}
+
+const EmailTemplateFormDialog: FC<EmailTemplateFormDialogProps> = ({
+  template,
+  emailAddressId,
+  buttonProps,
+}) => {
+  const { notify } = useNotifyContext()
+  const [isOpen, setIsOpen] = useState(false)
+
+  const [context, setContext] = useState<string>('')
+  const [content, setContent] = useState<string>('')
+  const [selectedAddressId, setSelectedAddressId] = useState<number>(0)
+
+  useEffect(() => {
+    setContext(template?.context || '')
+    setContent(template?.content || '')
+    setSelectedAddressId(template?.emailAddressId || emailAddressId)
+  }, [template, isOpen])
+
+  const clearInput = useCallback(() => {
+    setContext('')
+    setContent('')
+  }, [])
+
+  const handleAddButton = useCallback(
+    async (emailTemplate: Omit<EmailTemplate, 'id'>) => {
+      try {
+        await addEmailTemplate(emailTemplate)
+        clearInput()
+        setIsOpen(false)
+      } catch (err) {
+        notify(`Failed to add template '${emailTemplate.context}': ${err}`, {
+          intent: 'danger',
+        })
+      }
+    },
+    [notify]
+  )
+
+  const handleUpdateButton = useCallback(
+    async (emailTemplate: EmailTemplate) => {
+      try {
+        await updateEmailTemplate(emailTemplate)
+        setIsOpen(false)
+      } catch (err) {
+        notify(`Failed to update template '${emailTemplate.context}': ${err}`, {
+          intent: 'danger',
+        })
+      }
+    },
+    [notify]
+  )
+
+  return (
+    <>
+      <Button {...buttonProps} onClick={() => setIsOpen(true)} />
+      <Dialog
+        isOpen={isOpen}
+        onClose={() => setIsOpen(false)}
+        icon={template ? 'edit' : 'plus'}
+        title={template ? 'Edit template' : 'New template'}
+      >
+        <DialogBody>
+          <EmailTemplateFormContent
+            context={context}
+            content={content}
+            emailAddressId={selectedAddressId}
+            onContextChange={(value: string) => setContext(value)}
+            onContentChange={(value: string) => setContent(value)}
+            onEmailAddressIdChange={(value: number) =>
+              setSelectedAddressId(value)
+            }
+          />
+        </DialogBody>
+        <DialogFooter
+          actions={
+            template ? (
+              <Button
+                disabled={!context || !selectedAddressId}
+                onClick={() =>
+                  handleUpdateButton({
+                    id: template.id,
+                    context,
+                    content,
+                    emailAddressId: selectedAddressId,
+                  })
+                }
+                intent='primary'
+                icon='edit'
+                text='Save changes'
+              />
+            ) : (
+              <Button
+                disabled={!context || !selectedAddressId}
+                onClick={() =>
+                  handleAddButton({
+                    context,
+                    content,
+                    emailAddressId: selectedAddressId,
+                  })
+                }
+                intent='primary'
+                icon='plus'
+                text='Add'
+              />
+            )
+          }
+        />
+      </Dialog>
+    </>
+  )
+}
+
+export default memo(EmailTemplateFormDialog)
diff --git a/frontend/src/editor/EmailTemplates/EmailTemplate.tsx b/frontend/src/editor/EmailTemplates/EmailTemplate.tsx
new file mode 100644
index 000000000..f61b34673
--- /dev/null
+++ b/frontend/src/editor/EmailTemplates/EmailTemplate.tsx
@@ -0,0 +1,54 @@
+import { Button, ButtonGroup, Card } from '@blueprintjs/core'
+import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
+import type { FC } from 'react'
+import { memo, useCallback } from 'react'
+import EmailTemplateFormDialog from '../EmailTemplateFormDialog'
+import { deleteEmailTemplate } from '../indexeddb/operations'
+import type { EmailTemplate } from '../indexeddb/types'
+
+interface EmailTemplateProps {
+  emailTemplate: EmailTemplate
+}
+
+const EmailTemplateItem: FC<EmailTemplateProps> = ({ emailTemplate }) => {
+  const { notify } = useNotifyContext()
+
+  const handleDeleteButton = useCallback(
+    async (template: EmailTemplate) => {
+      try {
+        await deleteEmailTemplate(template.id)
+      } catch (err) {
+        notify(`Failed to delete template '${template.context}': ${err}`, {
+          intent: 'danger',
+        })
+      }
+    },
+    [notify]
+  )
+
+  return (
+    <Card style={{ display: 'flex', justifyContent: 'space-between' }}>
+      <span style={{ height: '100%', flexGrow: 1 }}>
+        {emailTemplate.context}
+      </span>
+      <ButtonGroup>
+        <EmailTemplateFormDialog
+          template={emailTemplate}
+          emailAddressId={emailTemplate.emailAddressId}
+          buttonProps={{
+            minimal: true,
+            icon: 'edit',
+            style: { marginRight: '1rem' },
+          }}
+        />
+        <Button
+          minimal
+          icon='cross'
+          onClick={() => handleDeleteButton(emailTemplate)}
+        />
+      </ButtonGroup>
+    </Card>
+  )
+}
+
+export default memo(EmailTemplateItem)
diff --git a/frontend/src/editor/EmailTemplates/index.tsx b/frontend/src/editor/EmailTemplates/index.tsx
new file mode 100644
index 000000000..93c4b0e08
--- /dev/null
+++ b/frontend/src/editor/EmailTemplates/index.tsx
@@ -0,0 +1,35 @@
+import { db } from '@/editor/indexeddb/db'
+import type { EmailTemplate } from '@/editor/indexeddb/types'
+import { CardList } from '@blueprintjs/core'
+import { useLiveQuery } from 'dexie-react-hooks'
+import type { FC } from 'react'
+import { memo } from 'react'
+import EmailTemplateItem from './EmailTemplate'
+
+interface EmailTemplatesProps {
+  emailAddressId: number
+}
+
+const EmailTemplates: FC<EmailTemplatesProps> = ({ emailAddressId }) => {
+  const emailTemplates = useLiveQuery(
+    () =>
+      db.emailTemplates
+        .where({ emailAddressId: Number(emailAddressId) })
+        .toArray(),
+    [emailAddressId],
+    []
+  )
+
+  return (
+    <CardList>
+      {emailTemplates?.map((emailTemplate: EmailTemplate) => (
+        <EmailTemplateItem
+          key={emailTemplate.id}
+          emailTemplate={emailTemplate}
+        />
+      ))}
+    </CardList>
+  )
+}
+
+export default memo(EmailTemplates)
diff --git a/frontend/src/editor/InjectSpecification/EmailInjectForm.tsx b/frontend/src/editor/InjectSpecification/EmailInjectForm.tsx
index c9bc58818..0163856bf 100644
--- a/frontend/src/editor/InjectSpecification/EmailInjectForm.tsx
+++ b/frontend/src/editor/InjectSpecification/EmailInjectForm.tsx
@@ -8,7 +8,7 @@ import {
 import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
 import { useLiveQuery } from 'dexie-react-hooks'
 import { memo, useCallback, useEffect, useState, type FC } from 'react'
-import EmailAddressSelector from '../LearningActivitySpecification/EmailAddressSelector'
+import EmailAddressSelector from '../EmailAddressSelector'
 import {
   addEmailInject,
   getEmailInjectByInjectInfoId,
diff --git a/frontend/src/editor/LearningActivitySpecification/EmailTemplateForm.tsx b/frontend/src/editor/LearningActivitySpecification/EmailTemplateForm.tsx
index cbd4c24d6..a9ba887d6 100644
--- a/frontend/src/editor/LearningActivitySpecification/EmailTemplateForm.tsx
+++ b/frontend/src/editor/LearningActivitySpecification/EmailTemplateForm.tsx
@@ -1,14 +1,14 @@
-import { Button, InputGroup, Label, TextArea } from '@blueprintjs/core'
+import { Button } from '@blueprintjs/core'
 import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
 import { useLiveQuery } from 'dexie-react-hooks'
 import { memo, useCallback, useEffect, useState, type FC } from 'react'
+import EmailTemplateFormContent from '../EmailTemplateFormContent'
 import {
   addEmailTemplate,
   getEmailTemplateByActivityId,
   updateEmailTemplate,
 } from '../indexeddb/operations'
 import type { EmailTemplate } from '../indexeddb/types'
-import EmailAddressSelector from './EmailAddressSelector'
 
 interface EmailTemplateFormProps {
   learningActivityId: number
@@ -54,33 +54,16 @@ const EmailTemplateForm: FC<EmailTemplateFormProps> = ({
 
   return (
     <div>
-      <EmailAddressSelector
+      <EmailTemplateFormContent
+        context={context}
+        content={content}
         emailAddressId={selectedAddressId}
-        onChange={id => setSelectedAddressId(id)}
+        onContextChange={(value: string) => setContext(value)}
+        onContentChange={(value: string) => setContent(value)}
+        onEmailAddressIdChange={(value: number) => setSelectedAddressId(value)}
       />
-      <Label>
-        Context
-        <InputGroup
-          placeholder='Input text'
-          value={context}
-          onChange={e => setContext(e.target.value)}
-        />
-      </Label>
-      <Label>
-        Content
-        <TextArea
-          value={content}
-          style={{
-            width: '100%',
-            height: '10rem',
-            resize: 'none',
-            overflowY: 'auto',
-          }}
-          placeholder='Input text'
-          onChange={e => setContent(e.target.value)}
-        />
-      </Label>
       <Button
+        disabled={!context || !selectedAddressId}
         onClick={() =>
           handleUpdateButton({
             learningActivityId,
diff --git a/frontend/src/editor/LearningActivitySpecification/ToolResponseForm.tsx b/frontend/src/editor/LearningActivitySpecification/ToolResponseForm.tsx
index 324c7d109..4387df86f 100644
--- a/frontend/src/editor/LearningActivitySpecification/ToolResponseForm.tsx
+++ b/frontend/src/editor/LearningActivitySpecification/ToolResponseForm.tsx
@@ -1,20 +1,14 @@
-import {
-  Button,
-  Checkbox,
-  InputGroup,
-  Label,
-  TextArea,
-} from '@blueprintjs/core'
+import { Button } from '@blueprintjs/core'
 import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
 import { useLiveQuery } from 'dexie-react-hooks'
 import { memo, useCallback, useEffect, useState, type FC } from 'react'
+import ToolResponseFormContent from '../ToolResponseFormContent'
 import {
   addToolResponse,
   getToolResponseByActivityId,
   updateToolResponse,
 } from '../indexeddb/operations'
 import type { ToolResponse } from '../indexeddb/types'
-import ToolSelector from './ToolSelector'
 
 interface ToolResponseFormProps {
   learningActivityId: number
@@ -62,38 +56,18 @@ const ToolResponseForm: FC<ToolResponseFormProps> = ({
 
   return (
     <div>
-      <ToolSelector
+      <ToolResponseFormContent
+        parameter={parameter}
+        content={content}
+        isRegex={isRegex}
         toolId={selectedToolId}
-        onChange={id => setSelectedToolId(id)}
-      />
-      <Label>
-        Parameter
-        <InputGroup
-          placeholder='Input text'
-          value={parameter}
-          onChange={e => setParameter(e.target.value)}
-        />
-      </Label>
-      <Checkbox
-        label='Is parameter regex?'
-        checked={isRegex}
-        onChange={e => setIsRegex(e.target.checked)}
+        onParameterChange={(value: string) => setParameter(value)}
+        onContentChange={(value: string) => setContent(value)}
+        onIsRegexChange={(value: boolean) => setIsRegex(value)}
+        onToolIdChange={(value: number) => setSelectedToolId(value)}
       />
-      <Label>
-        Response
-        <TextArea
-          value={content}
-          style={{
-            width: '100%',
-            height: '10rem',
-            resize: 'none',
-            overflowY: 'auto',
-          }}
-          placeholder='Input text'
-          onChange={e => setContent(e.target.value)}
-        />
-      </Label>
       <Button
+        disabled={!parameter || !selectedToolId}
         onClick={() =>
           handleUpdateButton({
             learningActivityId,
diff --git a/frontend/src/editor/Navbar/index.tsx b/frontend/src/editor/Navbar/index.tsx
index 2bc48d11d..0b6ae3b55 100644
--- a/frontend/src/editor/Navbar/index.tsx
+++ b/frontend/src/editor/Navbar/index.tsx
@@ -20,6 +20,7 @@ const Navbar = () => (
       path='/editor/create/inject-specification'
       name='Injects specification'
     />
+    <NavbarButton path='/editor/create/other' name='Other' />
     <NavbarButton
       path='/editor/create/final-information'
       name='Final Information'
diff --git a/frontend/src/editor/ToolForm/index.tsx b/frontend/src/editor/ToolForm/index.tsx
index 1b0b96a9e..56899e07d 100644
--- a/frontend/src/editor/ToolForm/index.tsx
+++ b/frontend/src/editor/ToolForm/index.tsx
@@ -137,13 +137,7 @@ const ToolForm: FC<ToolFormProps> = ({ toolInfo, buttonProps, onAdd }) => {
           actions={
             toolInfo ? (
               <Button
-                disabled={
-                  !name ||
-                  !category ||
-                  !tooltipDescription ||
-                  !hint ||
-                  !defaultResponse
-                }
+                disabled={!name || !defaultResponse}
                 onClick={() =>
                   handleUpdateButton({
                     id: toolInfo.id,
@@ -160,13 +154,7 @@ const ToolForm: FC<ToolFormProps> = ({ toolInfo, buttonProps, onAdd }) => {
               />
             ) : (
               <Button
-                disabled={
-                  !name ||
-                  !category ||
-                  !tooltipDescription ||
-                  !hint ||
-                  !defaultResponse
-                }
+                disabled={!name || !defaultResponse}
                 onClick={() =>
                   handleAddButton({
                     name,
diff --git a/frontend/src/editor/ToolResponseFormContent/index.tsx b/frontend/src/editor/ToolResponseFormContent/index.tsx
new file mode 100644
index 000000000..167139611
--- /dev/null
+++ b/frontend/src/editor/ToolResponseFormContent/index.tsx
@@ -0,0 +1,58 @@
+import { Checkbox, InputGroup, Label, TextArea } from '@blueprintjs/core'
+import { memo, type FC } from 'react'
+import ToolSelector from '../ToolSelector'
+
+interface ToolResponseFormProps {
+  parameter: string
+  onParameterChange: (value: string) => void
+  content: string
+  onContentChange: (value: string) => void
+  isRegex: boolean
+  onIsRegexChange: (value: boolean) => void
+  toolId: number
+  onToolIdChange: (value: number) => void
+}
+
+const ToolResponseForm: FC<ToolResponseFormProps> = ({
+  parameter,
+  onParameterChange,
+  content,
+  onContentChange,
+  isRegex,
+  onIsRegexChange,
+  toolId,
+  onToolIdChange,
+}) => (
+  <div>
+    <ToolSelector toolId={toolId} onChange={id => onToolIdChange(id)} />
+    <Label>
+      Parameter
+      <InputGroup
+        placeholder='Input text'
+        value={parameter}
+        onChange={e => onParameterChange(e.target.value)}
+      />
+    </Label>
+    <Checkbox
+      label='Is parameter regex?'
+      checked={isRegex}
+      onChange={e => onIsRegexChange(e.target.checked)}
+    />
+    <Label>
+      Response
+      <TextArea
+        value={content}
+        style={{
+          width: '100%',
+          height: '10rem',
+          resize: 'none',
+          overflowY: 'auto',
+        }}
+        placeholder='Input text'
+        onChange={e => onContentChange(e.target.value)}
+      />
+    </Label>
+  </div>
+)
+
+export default memo(ToolResponseForm)
diff --git a/frontend/src/editor/ToolResponseFormDialog/index.tsx b/frontend/src/editor/ToolResponseFormDialog/index.tsx
new file mode 100644
index 000000000..be96fc4c7
--- /dev/null
+++ b/frontend/src/editor/ToolResponseFormDialog/index.tsx
@@ -0,0 +1,138 @@
+import type { ButtonProps } from '@blueprintjs/core'
+import { Button, Dialog, DialogBody, DialogFooter } from '@blueprintjs/core'
+import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
+import { memo, useCallback, useEffect, useState, type FC } from 'react'
+import ToolResponseForm from '../ToolResponseFormContent'
+import { addToolResponse, updateToolResponse } from '../indexeddb/operations'
+import type { ToolResponse } from '../indexeddb/types'
+
+interface ToolResponseFormDialogProps {
+  response?: ToolResponse
+  toolId: number
+  buttonProps: ButtonProps
+}
+
+const ToolResponseFormDialog: FC<ToolResponseFormDialogProps> = ({
+  response,
+  toolId,
+  buttonProps,
+}) => {
+  const { notify } = useNotifyContext()
+  const [isOpen, setIsOpen] = useState(false)
+
+  const [parameter, setParameter] = useState<string>('')
+  const [content, setContent] = useState<string>('')
+  const [isRegex, setIsRegex] = useState<boolean>(false)
+  const [selectedToolId, setSelectedToolId] = useState<number>(0)
+
+  const clearInput = useCallback(() => {
+    setParameter('')
+    setContent('')
+    setIsRegex(false)
+  }, [])
+
+  useEffect(() => {
+    setParameter(response?.parameter || '')
+    setContent(response?.content || '')
+    setIsRegex(response?.isRegex || false)
+    setSelectedToolId(response?.toolId || toolId)
+  }, [response, isOpen])
+
+  const handleAddButton = useCallback(
+    async (toolResponse: Omit<ToolResponse, 'id'>) => {
+      try {
+        await addToolResponse(toolResponse)
+        clearInput()
+        setIsOpen(false)
+      } catch (err) {
+        notify(
+          `Failed to add tool response '${toolResponse.parameter}': ${err}`,
+          {
+            intent: 'danger',
+          }
+        )
+      }
+    },
+    [notify]
+  )
+
+  const handleUpdateButton = useCallback(
+    async (toolResponse: ToolResponse) => {
+      try {
+        await updateToolResponse(toolResponse)
+        setIsOpen(false)
+      } catch (err) {
+        notify(
+          `Failed to update tool response '${toolResponse.parameter}': ${err}`,
+          {
+            intent: 'danger',
+          }
+        )
+      }
+    },
+    [notify]
+  )
+
+  return (
+    <>
+      <Button {...buttonProps} onClick={() => setIsOpen(true)} />
+      <Dialog
+        isOpen={isOpen}
+        onClose={() => setIsOpen(false)}
+        icon={response ? 'edit' : 'plus'}
+        title={response ? 'Edit tool response' : 'New tool response'}
+      >
+        <DialogBody>
+          <ToolResponseForm
+            parameter={parameter}
+            content={content}
+            isRegex={isRegex}
+            toolId={selectedToolId}
+            onParameterChange={(value: string) => setParameter(value)}
+            onContentChange={(value: string) => setContent(value)}
+            onIsRegexChange={(value: boolean) => setIsRegex(value)}
+            onToolIdChange={(value: number) => setSelectedToolId(value)}
+          />
+        </DialogBody>
+        <DialogFooter
+          actions={
+            response ? (
+              <Button
+                disabled={!parameter}
+                onClick={() =>
+                  handleUpdateButton({
+                    id: response.id,
+                    parameter,
+                    content,
+                    isRegex,
+                    toolId: selectedToolId,
+                  })
+                }
+                intent='primary'
+                icon='edit'
+                text='Save changes'
+              />
+            ) : (
+              <Button
+                disabled={!parameter}
+                onClick={() =>
+                  handleAddButton({
+                    parameter,
+                    content,
+                    isRegex,
+                    toolId: selectedToolId,
+                  })
+                }
+                intent='primary'
+                icon='plus'
+                text='Add'
+              />
+            )
+          }
+        />
+      </Dialog>
+    </>
+  )
+}
+
+export default memo(ToolResponseFormDialog)
diff --git a/frontend/src/editor/ToolResponses/ToolResponse.tsx b/frontend/src/editor/ToolResponses/ToolResponse.tsx
new file mode 100644
index 000000000..025d57952
--- /dev/null
+++ b/frontend/src/editor/ToolResponses/ToolResponse.tsx
@@ -0,0 +1,57 @@
+import { Button, ButtonGroup, Card } from '@blueprintjs/core'
+import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
+import type { FC } from 'react'
+import { memo, useCallback } from 'react'
+import ToolResponseFormDialog from '../ToolResponseFormDialog'
+import { deleteToolResponse } from '../indexeddb/operations'
+import type { ToolResponse } from '../indexeddb/types'
+
+interface ToolResponseProps {
+  toolResponse: ToolResponse
+}
+
+const ToolResponseItem: FC<ToolResponseProps> = ({ toolResponse }) => {
+  const { notify } = useNotifyContext()
+
+  const handleDeleteButton = useCallback(
+    async (response: ToolResponse) => {
+      try {
+        await deleteToolResponse(response.id)
+      } catch (err) {
+        notify(
+          `Failed to delete tool response '${response.parameter}': ${err}`,
+          {
+            intent: 'danger',
+          }
+        )
+      }
+    },
+    [notify]
+  )
+
+  return (
+    <Card style={{ display: 'flex', justifyContent: 'space-between' }}>
+      <span style={{ height: '100%', flexGrow: 1 }}>
+        {toolResponse.parameter}
+      </span>
+      <ButtonGroup>
+        <ToolResponseFormDialog
+          response={toolResponse}
+          toolId={toolResponse.toolId}
+          buttonProps={{
+            minimal: true,
+            icon: 'edit',
+            style: { marginRight: '1rem' },
+          }}
+        />
+        <Button
+          minimal
+          icon='cross'
+          onClick={() => handleDeleteButton(toolResponse)}
+        />
+      </ButtonGroup>
+    </Card>
+  )
+}
+
+export default memo(ToolResponseItem)
diff --git a/frontend/src/editor/ToolResponses/index.tsx b/frontend/src/editor/ToolResponses/index.tsx
new file mode 100644
index 000000000..1c96632cc
--- /dev/null
+++ b/frontend/src/editor/ToolResponses/index.tsx
@@ -0,0 +1,29 @@
+import { db } from '@/editor/indexeddb/db'
+import type { ToolResponse } from '@/editor/indexeddb/types'
+import { CardList } from '@blueprintjs/core'
+import { useLiveQuery } from 'dexie-react-hooks'
+import type { FC } from 'react'
+import { memo } from 'react'
+import ToolResponseItem from './ToolResponse'
+
+interface ToolResponsesProps {
+  toolId: number
+}
+
+const ToolResponses: FC<ToolResponsesProps> = ({ toolId }) => {
+  const toolResponses = useLiveQuery(
+    () => db.toolResponses.where({ toolId: Number(toolId) }).toArray(),
+    [toolId],
+    []
+  )
+
+  return (
+    <CardList>
+      {toolResponses?.map((toolResponse: ToolResponse) => (
+        <ToolResponseItem key={toolResponse.id} toolResponse={toolResponse} />
+      ))}
+    </CardList>
+  )
+}
+
+export default memo(ToolResponses)
diff --git a/frontend/src/editor/LearningActivitySpecification/ToolSelector.tsx b/frontend/src/editor/ToolSelector/index.tsx
similarity index 88%
rename from frontend/src/editor/LearningActivitySpecification/ToolSelector.tsx
rename to frontend/src/editor/ToolSelector/index.tsx
index e3a194a2d..e6a2d7bdd 100644
--- a/frontend/src/editor/LearningActivitySpecification/ToolSelector.tsx
+++ b/frontend/src/editor/ToolSelector/index.tsx
@@ -1,7 +1,7 @@
 import type { OptionProps } from '@blueprintjs/core'
 import { HTMLSelect, Label } from '@blueprintjs/core'
 import { useLiveQuery } from 'dexie-react-hooks'
-import { memo, useMemo, type FC } from 'react'
+import { memo, useEffect, useMemo, type FC } from 'react'
 import ToolForm from '../ToolForm'
 import { db } from '../indexeddb/db'
 import type { ToolInfo } from '../indexeddb/types'
@@ -31,6 +31,12 @@ const ToolSelector: FC<ToolFormProps> = ({ toolId, onChange }) => {
     }))
   }, [tools])
 
+  useEffect(() => {
+    if (!toolId && tools && tools.length > 0) {
+      onChange(tools[0].id)
+    }
+  }, [tools, toolId])
+
   return (
     <div style={{ display: 'flex', width: '100%' }}>
       <Label style={{ flexGrow: '1' }}>
diff --git a/frontend/src/editor/Tools/Tool.tsx b/frontend/src/editor/Tools/Tool.tsx
new file mode 100644
index 000000000..a9effbb81
--- /dev/null
+++ b/frontend/src/editor/Tools/Tool.tsx
@@ -0,0 +1,76 @@
+import { Button, ButtonGroup, Card } from '@blueprintjs/core'
+import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
+import type { FC } from 'react'
+import { memo, useCallback } from 'react'
+import ToolForm from '../ToolForm'
+import ToolResponseFormDialog from '../ToolResponseFormDialog'
+import ToolResponses from '../ToolResponses'
+import { deleteTool } from '../indexeddb/operations'
+import type { ToolInfo } from '../indexeddb/types'
+
+interface ToolProps {
+  tool: ToolInfo
+}
+
+const ToolItem: FC<ToolProps> = ({ tool }) => {
+  const { notify } = useNotifyContext()
+
+  const handleDeleteButton = useCallback(
+    async (toolInfo: ToolInfo) => {
+      try {
+        await deleteTool(toolInfo.id)
+      } catch (err) {
+        notify(`Failed to delete tool '${toolInfo.name}': ${err}`, {
+          intent: 'danger',
+        })
+      }
+    },
+    [notify]
+  )
+
+  return (
+    <Card style={{ flexDirection: 'column' }}>
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'space-between',
+          alignItems: 'center',
+          width: '100%',
+          padding: '0rem 1rem 1rem 0',
+        }}
+      >
+        <span style={{ height: '100%', flexGrow: 1 }}>{tool.name}</span>
+        <ButtonGroup>
+          <ToolForm
+            toolInfo={tool}
+            buttonProps={{
+              minimal: true,
+              icon: 'edit',
+              style: { marginRight: '1rem' },
+            }}
+          />
+          <Button
+            minimal
+            icon='cross'
+            onClick={() => handleDeleteButton(tool)}
+          />
+        </ButtonGroup>
+      </div>
+      <div style={{ width: '100%', paddingLeft: '2rem' }}>
+        <ToolResponses toolId={tool.id} />
+        <ToolResponseFormDialog
+          toolId={tool.id}
+          buttonProps={{
+            minimal: true,
+            text: 'Add tool response',
+            alignText: 'left',
+            icon: 'plus',
+            style: { padding: '1rem', width: '100%' },
+          }}
+        />
+      </div>
+    </Card>
+  )
+}
+
+export default memo(ToolItem)
diff --git a/frontend/src/editor/Tools/index.tsx b/frontend/src/editor/Tools/index.tsx
new file mode 100644
index 000000000..ef1a97342
--- /dev/null
+++ b/frontend/src/editor/Tools/index.tsx
@@ -0,0 +1,18 @@
+import { db } from '@/editor/indexeddb/db'
+import type { ToolInfo } from '@/editor/indexeddb/types'
+import { CardList } from '@blueprintjs/core'
+import { useLiveQuery } from 'dexie-react-hooks'
+import { memo } from 'react'
+import Tool from './Tool'
+
+const Tools = () => {
+  const tools = useLiveQuery(() => db.tools.toArray(), [], [])
+
+  return (
+    <CardList>
+      {tools?.map((tool: ToolInfo) => <Tool key={tool.id} tool={tool} />)}
+    </CardList>
+  )
+}
+
+export default memo(Tools)
diff --git a/frontend/src/editor/indexeddb/types.tsx b/frontend/src/editor/indexeddb/types.tsx
index d80b9e54c..d308ae7f2 100644
--- a/frontend/src/editor/indexeddb/types.tsx
+++ b/frontend/src/editor/indexeddb/types.tsx
@@ -41,7 +41,7 @@ export type ToolInfo = {
 
 export type ToolResponse = {
   id: number
-  learningActivityId: number
+  learningActivityId?: number
   toolId: number
   parameter: string
   isRegex: boolean
@@ -52,7 +52,7 @@ export type EmailAddressInfo = Omit<EmailAddress, 'control'>
 
 export type EmailTemplate = {
   id: number
-  learningActivityId: number
+  learningActivityId?: number
   emailAddressId: number
   context: string
   content: string
diff --git a/frontend/src/pages/editor/create/emails.tsx b/frontend/src/pages/editor/create/emails.tsx
new file mode 100644
index 000000000..afac8110e
--- /dev/null
+++ b/frontend/src/pages/editor/create/emails.tsx
@@ -0,0 +1,26 @@
+import EditorPage from '@/editor/EditorPage'
+import EmailAddressForm from '@/editor/EmailAddressForm'
+import EmailAddresses from '@/editor/EmailAddresses'
+import { memo } from 'react'
+
+const EmailAddressesPage = () => (
+  <EditorPage
+    title='Define email addresses'
+    description='Description.'
+    prevPath='/editor/create/other'
+    nextVisible={false}
+  >
+    <EmailAddresses />
+    <EmailAddressForm
+      buttonProps={{
+        minimal: true,
+        text: 'Add email address',
+        alignText: 'left',
+        icon: 'plus',
+        style: { padding: '1rem', width: '100%' },
+      }}
+    />
+  </EditorPage>
+)
+
+export default memo(EmailAddressesPage)
diff --git a/frontend/src/pages/editor/create/final-information.tsx b/frontend/src/pages/editor/create/final-information.tsx
index 5140d3c6f..44e199ef3 100644
--- a/frontend/src/pages/editor/create/final-information.tsx
+++ b/frontend/src/pages/editor/create/final-information.tsx
@@ -9,7 +9,7 @@ const FinalInformationPage = () => {
     <EditorPage
       title='Final information'
       description='Description.'
-      prevPath='/editor/create/activity-specification'
+      prevPath='/editor/create/other'
       nextPath='/editor/create/conclusion'
       nextDisabled={nextDisabled}
     >
diff --git a/frontend/src/pages/editor/create/inject-specification/index.tsx b/frontend/src/pages/editor/create/inject-specification/index.tsx
index 8dc412ee9..a76a44b36 100644
--- a/frontend/src/pages/editor/create/inject-specification/index.tsx
+++ b/frontend/src/pages/editor/create/inject-specification/index.tsx
@@ -7,8 +7,7 @@ const ActivitiesSpecificationPage = () => (
     title='Define injects'
     description='Description.'
     prevPath='/editor/create/activity-specification'
-    nextPath='/editor'
-    nextDisabled
+    nextPath='/editor/create/other'
   >
     <InjectsOverview />
   </EditorPage>
diff --git a/frontend/src/pages/editor/create/other.tsx b/frontend/src/pages/editor/create/other.tsx
new file mode 100644
index 000000000..ce24da83b
--- /dev/null
+++ b/frontend/src/pages/editor/create/other.tsx
@@ -0,0 +1,28 @@
+import EditorPage from '@/editor/EditorPage'
+import { useNavigate } from '@/router'
+import { Card, CardList } from '@blueprintjs/core'
+import { memo } from 'react'
+
+const OtherPage = () => {
+  const nav = useNavigate()
+
+  return (
+    <EditorPage
+      title='Define other information'
+      description='Add new tools and email addresses to your exercise.'
+      prevPath='/editor/create/inject-specification'
+      nextPath='/editor/create/final-information'
+    >
+      <CardList>
+        <Card interactive onClick={() => nav(`/editor/create/tools`)}>
+          Tools
+        </Card>
+        <Card interactive onClick={() => nav(`/editor/create/emails`)}>
+          Email addresses
+        </Card>
+      </CardList>
+    </EditorPage>
+  )
+}
+
+export default memo(OtherPage)
diff --git a/frontend/src/pages/editor/create/tools.tsx b/frontend/src/pages/editor/create/tools.tsx
new file mode 100644
index 000000000..c5ba6977e
--- /dev/null
+++ b/frontend/src/pages/editor/create/tools.tsx
@@ -0,0 +1,26 @@
+import EditorPage from '@/editor/EditorPage'
+import ToolForm from '@/editor/ToolForm'
+import Tools from '@/editor/Tools'
+import { memo } from 'react'
+
+const ToolsPage = () => (
+  <EditorPage
+    title='Define tools'
+    description='Description.'
+    prevPath='/editor/create/other'
+    nextVisible={false}
+  >
+    <Tools />
+    <ToolForm
+      buttonProps={{
+        minimal: true,
+        text: 'Add tool',
+        alignText: 'left',
+        icon: 'plus',
+        style: { padding: '1rem', width: '100%' },
+      }}
+    />
+  </EditorPage>
+)
+
+export default memo(ToolsPage)
diff --git a/frontend/src/router.ts b/frontend/src/router.ts
index bc19fedf1..b2ef20e1b 100644
--- a/frontend/src/router.ts
+++ b/frontend/src/router.ts
@@ -16,6 +16,7 @@ export type Path =
   | `/editor/create/activity-specification`
   | `/editor/create/activity-specification/:activityId`
   | `/editor/create/conclusion`
+  | `/editor/create/emails`
   | `/editor/create/exercise-information`
   | `/editor/create/final-information`
   | `/editor/create/inject-specification`
@@ -23,6 +24,8 @@ export type Path =
   | `/editor/create/injects`
   | `/editor/create/introduction`
   | `/editor/create/learning-objectives`
+  | `/editor/create/other`
+  | `/editor/create/tools`
   | `/exercise-panel`
   | `/exercise-panel/definition/:definitionId`
   | `/exercise-panel/exercise/:exerciseId`
-- 
GitLab