diff --git a/tutor-assistant-web/src/app/App.tsx b/tutor-assistant-web/src/app/App.tsx index 020d526..2a77b22 100644 --- a/tutor-assistant-web/src/app/App.tsx +++ b/tutor-assistant-web/src/app/App.tsx @@ -6,13 +6,15 @@ import { BrowserRouter } from 'react-router-dom' import { JoyTheme } from './JoyTheme' import { configureI18n } from './config/i18n-config.ts' import { Main } from './MainStyle.tsx' -import { HStack, MainContent } from '../lib/components/flex-layout.tsx' +import { HStack, MainContent } from '../common/components/containers/flex-layout.tsx' import { CalendarBar } from '../modules/calendar/components/CalendarBar.tsx' configureI18n(texts) - -const App = () => { +/** + * Configures routing and theme; renders CalendarBar for global access and Routing + */ +export function App() { return ( @@ -30,5 +32,3 @@ const App = () => { ) } - -export default App diff --git a/tutor-assistant-web/src/app/JoyTheme.tsx b/tutor-assistant-web/src/app/JoyTheme.tsx index 8561605..3bfec63 100644 --- a/tutor-assistant-web/src/app/JoyTheme.tsx +++ b/tutor-assistant-web/src/app/JoyTheme.tsx @@ -1,7 +1,11 @@ import * as React from 'react' import { CssVarsProvider } from '@mui/joy/styles' -import { ChildrenProps } from '../lib/types.ts' +import { ChildrenProps } from '../common/types.ts' +/** + * Provides the base theme for the app + * @param children to be rendered + */ export function JoyTheme({ children }: ChildrenProps) { return ( diff --git a/tutor-assistant-web/src/app/MainStyle.tsx b/tutor-assistant-web/src/app/MainStyle.tsx index 6ea0785..75f5654 100644 --- a/tutor-assistant-web/src/app/MainStyle.tsx +++ b/tutor-assistant-web/src/app/MainStyle.tsx @@ -1,5 +1,8 @@ import { Box, styled } from '@mui/joy' +/** + * Applies global styling according to the theme + */ export const Main = styled(Box)` width: 100%; height: 100%; diff --git a/tutor-assistant-web/src/app/auth/Auth.tsx b/tutor-assistant-web/src/app/auth/Auth.tsx index 1a7fc4a..946f57a 100644 --- a/tutor-assistant-web/src/app/auth/Auth.tsx +++ b/tutor-assistant-web/src/app/auth/Auth.tsx @@ -1,7 +1,7 @@ import { createContext } from 'react' import axios, { AxiosInstance } from 'axios' -import { chill, isNotPresent } from '../../lib/utils/utils.ts' -import { ChildrenProps } from '../../lib/types.ts' +import { chill, isNotPresent } from '../../common/utils/utils.ts' +import { ChildrenProps } from '../../common/types.ts' import { useKeycloak } from '@react-keycloak/web' type AuthContextType = { @@ -12,6 +12,16 @@ type AuthContextType = { getRoles: () => string[] } +/** + * Use only through useAuth + * + * Provides: + * getAuthHttp: returns a REST client with authorization headers set if the user is logged in + * isLoggedIn: if the user is logged in. + * openLogin: opens the login page of the identity provider. + * logout: logouts the user + * getRoles: returns the users roles + */ export const AuthContext = createContext({ getAuthHttp: () => axios, isLoggedIn: () => false, @@ -21,6 +31,11 @@ export const AuthContext = createContext({ }) +/** + * Applies AuthContext + * + * @param children wrapped by this context provider + */ export function Auth({ children }: ChildrenProps) { const { keycloak, initialized } = useKeycloak() diff --git a/tutor-assistant-web/src/app/auth/Authenticated.tsx b/tutor-assistant-web/src/app/auth/Authenticated.tsx index 675e3db..97195c2 100644 --- a/tutor-assistant-web/src/app/auth/Authenticated.tsx +++ b/tutor-assistant-web/src/app/auth/Authenticated.tsx @@ -1,12 +1,18 @@ -import { ChildrenProps } from '../../lib/types.ts' +import { ChildrenProps } from '../../common/types.ts' import { useAuth } from './useAuth.ts' -import { isPresent } from '../../lib/utils/utils.ts' -import { haveCommonElements } from '../../lib/utils/array-utils.ts' +import { isPresent } from '../../common/utils/utils.ts' +import { haveCommonElements } from '../../common/utils/array-utils.ts' interface Props extends ChildrenProps { roles?: string[] } +/** + * Protects a component from rendering based on the users authentication and authorization + * + * @param children rendered iff the user is logged in and has the required roles + * @param roles the user must have in order to render the children + */ export function Authenticated({ children, roles }: Props) { const { isLoggedIn, openLogin, getRoles } = useAuth() diff --git a/tutor-assistant-web/src/app/auth/useAuth.ts b/tutor-assistant-web/src/app/auth/useAuth.ts index e4193bb..4606af3 100644 --- a/tutor-assistant-web/src/app/auth/useAuth.ts +++ b/tutor-assistant-web/src/app/auth/useAuth.ts @@ -1,6 +1,9 @@ import { useContext } from 'react' import { AuthContext } from './Auth.tsx' +/** + * Provides common authentication functionality and common functionality that requires authentication + */ export function useAuth() { - return useContext(AuthContext) + return useContext(AuthContext) } diff --git a/tutor-assistant-web/src/app/base.ts b/tutor-assistant-web/src/app/base.ts index 12510d5..858af47 100644 --- a/tutor-assistant-web/src/app/base.ts +++ b/tutor-assistant-web/src/app/base.ts @@ -1,9 +1,18 @@ -import { getCurrentBaseUrl } from '../lib/utils/utils.ts' +import { getCurrentBaseUrl } from '../common/utils/utils.ts' const envApiBaseUrl = import.meta.env.VITE_API_BASE_URL as string const envKeycloakBaseUrl = import.meta.env.VITE_KEYCLOAK_BASE_URL as string const currentBaseUrl = getCurrentBaseUrl() +/** + * Backend api url the web frontend is supposed to communicate with + * Must be configured through env. 'default' uses the same protocol and host as this web frontend + */ export const apiBaseUrl = envApiBaseUrl !== 'default' ? envApiBaseUrl : `${currentBaseUrl}/api` + +/** + * Keycloak url for authentication and authorization + * Must be configured through env. 'default' uses the same protocol and host as this web frontend + */ export const keycloakBaseUrl = envKeycloakBaseUrl !== 'default' ? envKeycloakBaseUrl : `${currentBaseUrl}/auth` diff --git a/tutor-assistant-web/src/app/config/i18n-config.ts b/tutor-assistant-web/src/app/config/i18n-config.ts index d2ed291..7a5427a 100644 --- a/tutor-assistant-web/src/app/config/i18n-config.ts +++ b/tutor-assistant-web/src/app/config/i18n-config.ts @@ -2,31 +2,44 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' import LanguageDetector from 'i18next-browser-languagedetector' +/** + * Applies internationalization + * + * @param texts to be translated, requires the format: + * { + * textKey1: { + * "en": "value", + * "de": "value", + * ... + * }, + * ... + * } + */ export function configureI18n(texts: any) { - function restructureTranslations(texts: any) { - const result = {} as any + function restructureTranslations(texts: any) { + const result = {} as any - Object.keys(texts).forEach(key => { - Object.keys(texts[key]).forEach(lang => { - if (!result[lang]) { - result[lang] = { translation: {} } - } - result[lang].translation[key] = texts[key][lang] - }) - }) + Object.keys(texts).forEach(key => { + Object.keys(texts[key]).forEach(lang => { + if (!result[lang]) { + result[lang] = { translation: {} } + } + result[lang].translation[key] = texts[key][lang] + }) + }) - return result - } + return result + } - i18n - .use(initReactI18next) - .use(LanguageDetector) - .init({ - resources: restructureTranslations(texts), - fallbackLng: 'en', - interpolation: { - escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape - }, - }) + i18n + .use(initReactI18next) + .use(LanguageDetector) + .init({ + resources: restructureTranslations(texts), + fallbackLng: 'en', + interpolation: { + escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape + }, + }) } diff --git a/tutor-assistant-web/src/app/config/keycloak-config.ts b/tutor-assistant-web/src/app/config/keycloak-config.ts index 3f97d2d..6fea706 100644 --- a/tutor-assistant-web/src/app/config/keycloak-config.ts +++ b/tutor-assistant-web/src/app/config/keycloak-config.ts @@ -1,6 +1,9 @@ import Keycloak from 'keycloak-js' import { keycloakBaseUrl } from '../base.ts' +/** + * Returns configuration for accessing keycloak + */ export const keycloak = new Keycloak({ url: keycloakBaseUrl, realm: 'tutor-assistant', diff --git a/tutor-assistant-web/src/app/routing/Routing.tsx b/tutor-assistant-web/src/app/routing/Routing.tsx index 8eaa65e..877faaf 100644 --- a/tutor-assistant-web/src/app/routing/Routing.tsx +++ b/tutor-assistant-web/src/app/routing/Routing.tsx @@ -2,14 +2,13 @@ import { Navigate, Route, Routes } from 'react-router-dom' import { ChatPage } from '../../modules/chat/ChatPage.tsx' import { Authenticated } from '../auth/Authenticated.tsx' import { DocumentsPage } from '../../modules/documents/DocumentsPage.tsx' -import { HelperPage } from '../../modules/helper/HelperPage.tsx' import { ChatProvider } from '../../modules/chat/ChatProvider.tsx' -interface Props { - -} - -export function Routing({}: Props) { +/** + * Configures routing + * Sub routes might be configured in child components + */ +export function Routing() { return ( @@ -34,14 +33,6 @@ export function Routing({}: Props) { } /> - - - - } - /> - ) } diff --git a/tutor-assistant-web/src/common/components/Bar.tsx b/tutor-assistant-web/src/common/components/containers/Bar.tsx similarity index 69% rename from tutor-assistant-web/src/common/components/Bar.tsx rename to tutor-assistant-web/src/common/components/containers/Bar.tsx index 1e0d210..af8f6ac 100644 --- a/tutor-assistant-web/src/common/components/Bar.tsx +++ b/tutor-assistant-web/src/common/components/containers/Bar.tsx @@ -1,7 +1,13 @@ import { styled } from '@mui/joy' -import { VStack } from '../../lib/components/flex-layout.tsx' +import { VStack } from './flex-layout.tsx' const barWidth = '380px' + +/** + * Sidebar with surface background based on a VStack + * + * Supports className 'right' to change styling when used as a bar on the right + */ export const Bar = styled(VStack)` min-width: ${barWidth}; width: ${barWidth}; diff --git a/tutor-assistant-web/src/lib/components/ColumnLayout.tsx b/tutor-assistant-web/src/common/components/containers/ColumnLayout.tsx similarity index 87% rename from tutor-assistant-web/src/lib/components/ColumnLayout.tsx rename to tutor-assistant-web/src/common/components/containers/ColumnLayout.tsx index 8fd37e6..bb2ee4d 100644 --- a/tutor-assistant-web/src/lib/components/ColumnLayout.tsx +++ b/tutor-assistant-web/src/common/components/containers/ColumnLayout.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' -import { byNumberReverse, isNotPresent, stringToNumber } from '../utils/utils.ts' -import { last } from '../utils/array-utils.ts' +import { byNumberReverse, isNotPresent, stringToNumber } from '../../utils/utils.ts' +import { last } from '../../utils/array-utils.ts' import { HStack, MainContent, VStack } from './flex-layout.tsx' import { Scroller } from './Scroller.tsx' @@ -14,6 +14,17 @@ interface Props { spacing?: number } +/** + * Renders items in a column layout + * + * @param columnCounts defines the number of columns absolutely or dependent of the window width (in px): + * Examples: 3, {0: 1, 400: 2, 800: 3} + * @param fill either vertical-wise or horizontal-wise + * @param values to be rendered + * @param render applied on each value + * @param spacing between the rendered items. Defaults to 0 + * @constructor + */ export function ColumnLayout({ columnCounts, fill, values, render, spacing }: Props) { if (isNotPresent(spacing)) spacing = 0 diff --git a/tutor-assistant-web/src/common/components/Header.tsx b/tutor-assistant-web/src/common/components/containers/Header.tsx similarity index 69% rename from tutor-assistant-web/src/common/components/Header.tsx rename to tutor-assistant-web/src/common/components/containers/Header.tsx index b44657b..5c9024d 100644 --- a/tutor-assistant-web/src/common/components/Header.tsx +++ b/tutor-assistant-web/src/common/components/containers/Header.tsx @@ -1,7 +1,7 @@ -import { Row, Spacer } from '../../lib/components/flex-layout.tsx' +import { Row, Spacer } from './flex-layout.tsx' import { Divider, Typography } from '@mui/joy' import React, { ReactNode } from 'react' -import { isPresent } from '../../lib/utils/utils.ts' +import { isPresent } from '../../utils/utils.ts' interface Props { title: ReactNode | string @@ -9,6 +9,14 @@ interface Props { rightNode?: ReactNode } + +/** + * Horizontal bar to render a title with additional items at the top + * + * @param title rendered in the middle of the bar + * @param leftNode rendered at the left border + * @param rightNode rendered at the right border + */ export function Header({ title, leftNode, rightNode }: Props) { return ( <> diff --git a/tutor-assistant-web/src/lib/components/Scroller.tsx b/tutor-assistant-web/src/common/components/containers/Scroller.tsx similarity index 72% rename from tutor-assistant-web/src/lib/components/Scroller.tsx rename to tutor-assistant-web/src/common/components/containers/Scroller.tsx index 876ba63..1f055ba 100644 --- a/tutor-assistant-web/src/lib/components/Scroller.tsx +++ b/tutor-assistant-web/src/common/components/containers/Scroller.tsx @@ -1,7 +1,7 @@ import { Box } from '@mui/joy' import { ReactNode, useEffect, useRef } from 'react' -import { isNotPresent } from '../utils/utils.ts' -import { empty } from '../utils/array-utils.ts' +import { isNotPresent } from '../../utils/utils.ts' +import { empty } from '../../utils/array-utils.ts' interface Props { children: ReactNode @@ -9,6 +9,14 @@ interface Props { scrollToBottomOnChange?: unknown[] } +/** + * Renders items in a vertically scrollable container + * + * @param children to be rendered inside this wrapper + * @param padding between this wrapper and its content + * @param scrollToBottomOnChange dependencies on whose change to scroll down to the bottom + * @constructor + */ export function Scroller({ children, padding, scrollToBottomOnChange }: Props) { if (isNotPresent(scrollToBottomOnChange)) scrollToBottomOnChange = [] diff --git a/tutor-assistant-web/src/lib/components/flex-layout.tsx b/tutor-assistant-web/src/common/components/containers/flex-layout.tsx similarity index 67% rename from tutor-assistant-web/src/lib/components/flex-layout.tsx rename to tutor-assistant-web/src/common/components/containers/flex-layout.tsx index 61df386..cc3554f 100644 --- a/tutor-assistant-web/src/lib/components/flex-layout.tsx +++ b/tutor-assistant-web/src/common/components/containers/flex-layout.tsx @@ -1,5 +1,8 @@ import { Box, Stack, styled } from '@mui/joy' +/** + * Full-width, full-height vertical stack with flex layout + */ export const VStack = styled(Stack)` margin: 0 !important; flex-direction: column; @@ -8,6 +11,9 @@ export const VStack = styled(Stack)` overflow-y: hidden; ` +/** + * Full-width, full-height horizontal stack with flex layout + */ export const HStack = styled(Stack)` margin: 0 !important; flex-direction: row; @@ -23,12 +29,18 @@ export const Column = styled(Stack)` overflow-y: hidden; ` +/** + * Full-width, min-height horizontal stack with flex layout + */ export const Row = styled(Stack)` margin: 0 !important; flex-direction: row; width: 100%; ` +/** + * Extending content inside a flex container + */ export const MainContent = styled(Box)` width: 100%; height: 100%; @@ -36,6 +48,9 @@ export const MainContent = styled(Box)` overflow: hidden; ` +/** + * Creating empty space between items in a flex container in order to create maximum space between items + */ export const Spacer = styled('span')` flex: 1; ` diff --git a/tutor-assistant-web/src/modules/documents/components/FileButton.tsx b/tutor-assistant-web/src/common/components/widgets/FileButton.tsx similarity index 85% rename from tutor-assistant-web/src/modules/documents/components/FileButton.tsx rename to tutor-assistant-web/src/common/components/widgets/FileButton.tsx index 6e7f592..8dd976c 100644 --- a/tutor-assistant-web/src/modules/documents/components/FileButton.tsx +++ b/tutor-assistant-web/src/common/components/widgets/FileButton.tsx @@ -1,11 +1,17 @@ import { Button, ButtonProps, styled } from '@mui/joy' import React, { ChangeEvent, useRef } from 'react' -import { isNotPresent } from '../../../lib/utils/utils.ts' +import { isNotPresent } from '../../utils/utils.ts' interface Props { addFile: (file: File) => void } +/** + * Button for selecting files + * + * @param addFile callback to be called if a file is selected + * @param props of the Button + */ export function FileButton({ addFile, ...props }: Props & ButtonProps) { const fileInputRef = useRef(null) diff --git a/tutor-assistant-web/src/lib/components/Multiline.tsx b/tutor-assistant-web/src/common/components/widgets/Multiline.tsx similarity index 83% rename from tutor-assistant-web/src/lib/components/Multiline.tsx rename to tutor-assistant-web/src/common/components/widgets/Multiline.tsx index d7ccb76..7a03825 100644 --- a/tutor-assistant-web/src/lib/components/Multiline.tsx +++ b/tutor-assistant-web/src/common/components/widgets/Multiline.tsx @@ -5,6 +5,14 @@ interface Props { text: string } + +/** + * Renders \n as + * + * @param text to be rendered + * @param props of Typography + * @constructor + */ export function Multiline({ text, ...props }: Props & TypographyProps) { const lines = text.split('\n') const maxIndex = lines.length - 1 diff --git a/tutor-assistant-web/src/lib/components/StarRater.tsx b/tutor-assistant-web/src/common/components/widgets/StarRater.tsx similarity index 76% rename from tutor-assistant-web/src/lib/components/StarRater.tsx rename to tutor-assistant-web/src/common/components/widgets/StarRater.tsx index efec078..c5e5688 100644 --- a/tutor-assistant-web/src/lib/components/StarRater.tsx +++ b/tutor-assistant-web/src/common/components/widgets/StarRater.tsx @@ -1,8 +1,8 @@ -import { Row } from './flex-layout.tsx' -import { range } from '../utils/array-utils.ts' +import { Row } from '../containers/flex-layout.tsx' +import { range } from '../../utils/array-utils.ts' import { useState } from 'react' import { Star, StarOutline } from '@mui/icons-material' -import { isPresent } from '../utils/utils.ts' +import { isPresent } from '../../utils/utils.ts' interface Props { max: number @@ -10,6 +10,16 @@ interface Props { onSelect: (rating: number) => void } +/** + * Picker of star icons + * + * Controlled component + * + * @param max number of stars + * @param rating currently selected. Takes values from 1 to max (both inclusive) + * @param onSelect function called when a star is clicked. Takes the rating + * @constructor + */ export function StarRater({ max, rating, onSelect }: Props) { const [preview, setPreview] = useState() diff --git a/tutor-assistant-web/src/common/components/StyledDivider.tsx b/tutor-assistant-web/src/common/components/widgets/StyledDivider.tsx similarity index 64% rename from tutor-assistant-web/src/common/components/StyledDivider.tsx rename to tutor-assistant-web/src/common/components/widgets/StyledDivider.tsx index 18587d8..0b380c6 100644 --- a/tutor-assistant-web/src/common/components/StyledDivider.tsx +++ b/tutor-assistant-web/src/common/components/widgets/StyledDivider.tsx @@ -1,5 +1,8 @@ import { Divider, styled } from '@mui/joy' +/** + * Divider according to the theme with additional styling + */ export const StyledDivider = styled(Divider)` margin-top: 0 !important; ` diff --git a/tutor-assistant-web/src/common/components/StyledMarkdown.tsx b/tutor-assistant-web/src/common/components/widgets/StyledMarkdown.tsx similarity index 80% rename from tutor-assistant-web/src/common/components/StyledMarkdown.tsx rename to tutor-assistant-web/src/common/components/widgets/StyledMarkdown.tsx index 3a94dd8..e8e397c 100644 --- a/tutor-assistant-web/src/common/components/StyledMarkdown.tsx +++ b/tutor-assistant-web/src/common/components/widgets/StyledMarkdown.tsx @@ -1,6 +1,9 @@ import { styled } from '@mui/joy' import Markdown from 'react-markdown' +/** + * Styling Markdown according to the theme + */ export const StyledMarkdown = styled(Markdown)` code.hljs { background: ${props => props.theme.palette.background.surface}; diff --git a/tutor-assistant-web/src/common/components/SubmitTextarea.tsx b/tutor-assistant-web/src/common/components/widgets/SubmitTextarea.tsx similarity index 56% rename from tutor-assistant-web/src/common/components/SubmitTextarea.tsx rename to tutor-assistant-web/src/common/components/widgets/SubmitTextarea.tsx index 27a91ae..404ec1e 100644 --- a/tutor-assistant-web/src/common/components/SubmitTextarea.tsx +++ b/tutor-assistant-web/src/common/components/widgets/SubmitTextarea.tsx @@ -1,20 +1,27 @@ import { Button, Textarea, TextareaProps, Tooltip } from '@mui/joy' import React, { ReactNode } from 'react' -import { Row, Spacer } from '../../lib/components/flex-layout.tsx' +import { Row, Spacer } from '../containers/flex-layout.tsx' import { useTranslation } from 'react-i18next' interface Props { - onCtrlEnter: () => void + onSubmit: () => void additionEndDecorator?: ReactNode } -export function SubmitTextarea({ onCtrlEnter, additionEndDecorator, ...props }: TextareaProps & Props) { +/** + * Makes a Textarea submit on Ctrl + Enter and adds a submit button at the right border of the end decorator + * + * @param onSubmit action to be performed on Ctrl + Enter or on submit button clicked + * @param additionEndDecorator react node to be rendered at the left border of the endDecorator + * @param props of the Textarea + */ +export function SubmitTextarea({ onSubmit, additionEndDecorator, ...props }: TextareaProps & Props) { const { t } = useTranslation() function handleInput(e: React.KeyboardEvent) { if (e.ctrlKey && e.key === 'Enter') { e.preventDefault() - onCtrlEnter() + onSubmit() } } @@ -27,7 +34,7 @@ export function SubmitTextarea({ onCtrlEnter, additionEndDecorator, ...props }: {additionEndDecorator} - {t('Send')} + {t('Send')} diff --git a/tutor-assistant-web/src/common/types.ts b/tutor-assistant-web/src/common/types.ts new file mode 100644 index 0000000..9babeee --- /dev/null +++ b/tutor-assistant-web/src/common/types.ts @@ -0,0 +1,5 @@ +import { ReactNode } from 'react' + +export interface ChildrenProps { + children?: ReactNode; +} diff --git a/tutor-assistant-web/src/common/utils/array-utils.ts b/tutor-assistant-web/src/common/utils/array-utils.ts new file mode 100644 index 0000000..a492573 --- /dev/null +++ b/tutor-assistant-web/src/common/utils/array-utils.ts @@ -0,0 +1,129 @@ +import { isNotPresent } from './utils.ts' + +/** + * Appends an item to the end of an array + * + * Pure function + * @param item to append + * @param array to updated + * @returns a new array with the new item + */ +export function append(item: T, array: T[]) { + return [...array, item] +} + +/** + * Updates an object with an id in an array + * + * Finds the object to update by the id + * + * Pure function + * @param item to update + * @param array to update + * @returns a new array with the item updated if the id is present else the original array + */ +export function update(item: T, array: T[]) { + if (isNotPresent(item.id)) return array + return array.map(item => item.id === item.id ? item : item) +} + +/** + * Removes an object with an id in an array + * + * Pure function + * @param item either an id or an object with an id to be removed + * @param array to update + * @returns a new array without the object with the given id + */ +export function remove(item: T | string, array: T[]) { + const id = typeof item === 'string' ? item : item.id + return array.filter(it => it.id !== id) +} + +/** + * last index of a given array + * + * Pure function + * @param array + * @returns the last index of the array or -1 + */ +export function lastIndex(array: any[]) { + return array.length - 1 +} + +/** + * last element of a given array + * + * Pure function + * @param array + * @returns last element of the array or undefined + */ +export function last(array: T[]) { + if (empty(array)) return undefined + return array[lastIndex(array)] +} + +/** + * checks if a given array has no elements + * + * Pure function + * @param array + * @returns if the array is empty + */ +export function empty(array: unknown[]) { + return array.length === 0 +} + +export function notEmpty(array: unknown[]) { + return !empty(array) +} + + +/** + * Partitions an array into two arrays + * + * Pure function + * @param predicate to partition the array + * @param array to be partitioned + * @returns an array with two elements + * The first array contains all elements of the array for which predicate returns true, + * the second array contains all other elements + */ +export function partition(predicate: (item: T) => boolean, array: T[]) { + const result = [[], []] as [T[], T[]] + array.forEach(item => predicate(item) ? result[0].push(item) : result[1].push(item)) + return result +} + +/** + * Checks, if two arrays have at least one common element + * + * Pure function + * @param array1 first array + * @param array2 second array + * @returns true if the arrays have at least one common array else false + */ +export function haveCommonElements(array1: T[], array2: T[]): boolean { + const set1 = new Set(array1) + for (const item of array2) { + if (set1.has(item)) { + return true + } + } + return false +} + +/** + * Creates an array with all integers between the given integers in ascending order + * + * @param start integer inclusive + * @param end integer exclusive + * @returns an array with integers from start to end + */ +export function range(start: number, end: number) { + let result = [] + for (let i = start; i < end; i++) { + result.push(i) + } + return result +} diff --git a/tutor-assistant-web/src/common/utils/math-utils.ts b/tutor-assistant-web/src/common/utils/math-utils.ts new file mode 100644 index 0000000..d411155 --- /dev/null +++ b/tutor-assistant-web/src/common/utils/math-utils.ts @@ -0,0 +1,10 @@ +/** + * Rounds a value to decimals + * + * @param value to be rounded + * @param decimals to which the value is supposed to be rounded + */ +export function roundTo(value: number, decimals: number) { + const factor = Math.pow(10, decimals) + return Math.round(value * factor) / factor +} \ No newline at end of file diff --git a/tutor-assistant-web/src/common/utils/utils.ts b/tutor-assistant-web/src/common/utils/utils.ts new file mode 100644 index 0000000..e4c741e --- /dev/null +++ b/tutor-assistant-web/src/common/utils/utils.ts @@ -0,0 +1,50 @@ +/** + * @param value to check + * @returns if the given value is null or undefined + */ +export function isNotPresent(value: any): value is null | undefined { + return value === undefined || value === null +} + +/** + * @param value to check + * @returns if the given value is of its type but not undefined or null + */ +export function isPresent(value: T | undefined | null): value is T { + return !isNotPresent(value) +} + +/** + * Sorting value for revered soring + * + * @param a first value + * @param b second value + * @returns positive if a is less than b, negative if a is greater than b, zero if they are equal + */ +export function byNumberReverse(a: number, b: number) { + return b - a +} + +/** + * Converts a string to a number + * + * @param s to be converted + * @returns a number of the given string + */ +export function stringToNumber(s: string) { + return +s +} + +/** + * @returns this base url + */ +export function getCurrentBaseUrl() { + const { protocol, host } = window.location + return `${protocol}//${host}` +} + +/** + * Function that does nothing + * Can be used as default value + */ +export const chill = () => undefined diff --git a/tutor-assistant-web/src/index.css b/tutor-assistant-web/src/index.css index 356388f..ea9b311 100644 --- a/tutor-assistant-web/src/index.css +++ b/tutor-assistant-web/src/index.css @@ -29,10 +29,3 @@ div#root { height: 100%; overflow-y: hidden; } - -/* -display: flex; -place-items: center; -*/ - - diff --git a/tutor-assistant-web/src/lib/components/BaseLayout.tsx b/tutor-assistant-web/src/lib/components/BaseLayout.tsx deleted file mode 100644 index 59dcf9b..0000000 --- a/tutor-assistant-web/src/lib/components/BaseLayout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import {VStack} from "./flex-layout.tsx"; - -interface Props { - -} - -export function BaseLayout({}: Props) { - return ( - - - - ) -} diff --git a/tutor-assistant-web/src/lib/hooks/useCallOnce.ts b/tutor-assistant-web/src/lib/hooks/useCallOnce.ts deleted file mode 100644 index 72e1a51..0000000 --- a/tutor-assistant-web/src/lib/hooks/useCallOnce.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { EffectCallback, useEffect, useRef } from 'react' - -export function useCallOnce(callback: EffectCallback) { - const called = useRef(false) - useEffect(() => { - if (called.current) return - called.current = true - return callback() - }, []) -} diff --git a/tutor-assistant-web/src/lib/hooks/useInstantState.ts b/tutor-assistant-web/src/lib/hooks/useInstantState.ts deleted file mode 100644 index e52946a..0000000 --- a/tutor-assistant-web/src/lib/hooks/useInstantState.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useRef, useState } from 'react' - - -export function useInstantState(initial: T): [() => T, (value: T) => void] { - const [_, setState] = useState(initial) - const stateRef = useRef(initial) - - function setInstantState(value: T) { - stateRef.current = value - setState(value) - } - - function getInstantState() { - return stateRef.current - } - - return [getInstantState, setInstantState] -} diff --git a/tutor-assistant-web/src/lib/hooks/useOnChange.ts b/tutor-assistant-web/src/lib/hooks/useOnChange.ts deleted file mode 100644 index 8805db2..0000000 --- a/tutor-assistant-web/src/lib/hooks/useOnChange.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useRef } from 'react' - -export function useOnChange(callback: () => void, values: any[]) { - const prevRef = useRef([]) - - function hasChanged() { - const prev = prevRef.current - - if (prev.length !== values.length) { - return true - } - - for (let i = 0; i < prev.length; i++) { - if (values[i] !== prev[i]) { - return true - } - } - - return false - } - - if (hasChanged()) { - prevRef.current = values - callback() - } -} diff --git a/tutor-assistant-web/src/lib/types.ts b/tutor-assistant-web/src/lib/types.ts deleted file mode 100644 index d988669..0000000 --- a/tutor-assistant-web/src/lib/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Dispatch, FormEvent, ReactNode, SetStateAction } from 'react' - -export interface ChildrenProps { - children?: ReactNode; -} - -export type HTMLFormEvent = FormEvent; -export type DivMouseEvent = React.MouseEvent -export type StateCalculator = (prev: T) => T -export type StateChanger = (changer: StateCalculator) => void -export type StateSetter = React.Dispatch> -export type State = [S | undefined, Dispatch>] diff --git a/tutor-assistant-web/src/lib/utils/array-utils.ts b/tutor-assistant-web/src/lib/utils/array-utils.ts deleted file mode 100644 index 44dc08e..0000000 --- a/tutor-assistant-web/src/lib/utils/array-utils.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { isNotPresent } from './utils' -import { isBetweenExcluded, isBetweenIncluded } from './math-utils' - -export function append(item: T, array: T[]) { - return [...array, item] -} - -export function update(item: T, array: T[]) { - if (isNotPresent(item.id)) return array - return array.map(item => item.id === item.id ? item : item) -} - -export function remove(item: T | string, array: T[]) { - const id = typeof item === 'string' ? item : item.id - return array.filter(it => it.id !== id) -} - -export function lastIndex(array: any[]) { - return array.length - 1 -} - -export function last(array: T[]) { - if (empty(array)) return undefined - return array[lastIndex(array)] -} - -export function empty(array: unknown[]) { - return array.length === 0 -} - -export function notEmpty(array: unknown[]) { - return !empty(array) -} - -export function partition(predicate: (item: T) => boolean, array: T[]) { - const result = [[], []] as [T[], T[]] - array.forEach(item => predicate(item) ? result[0].push(item) : result[1].push(item)) - return result -} - -export function haveCommonElements(array1: T[], array2: T[]): boolean { - const set1 = new Set(array1) - for (const item of array2) { - if (set1.has(item)) { - return true - } - } - return false -} - -export function pairwise(array: T[], func: (current: T, next: T) => T) { - const result = [] - for (let i = 0; i < array.length - 1; i++) { - result.push(func(array[i], array[i + 1])) - } - return result -} - -export function pairwiseKeepingFirst(array: T[], func: (current: T, next: T) => T) { - if (array.length === 0) return [] - const result = [array[0]] - - return result.push(...pairwise(array, func)) -} - -export function pairwiseKeepingFirstInstantlyApplied( - array: T[], func: (current: T, next: T) => T) { - - const arrayCopy = [...array] - - if (arrayCopy.length === 0) return [] - - for (let i = 0; i < arrayCopy.length - 1; i++) { - arrayCopy[i] = (func(arrayCopy[i], arrayCopy[i + 1])) - } - return arrayCopy -} - -export function convertN(array: T[], n: number, func: (nItems: T[]) => R) { - const result: R[] = [] - - if (array.length % n !== 0) return result - - let nItems: T[] = [] - for (let i = 0; i < array.length; i++) { - nItems.push(array[i]) - - if (nItems.length % n === 0) { - result.push(func(nItems)) - nItems = [] - } - } - return result -} - -export function range(start: number, end: number) { - let result = [] - for (let i = start; i < end; i++) { - result.push(i) - } - return result -} - -export function getBestFit(value: number, array: T[], mapToNumber: (item: T) => number) { - function continueSearchIndex(index: number) { - return isBetweenExcluded(0, index, length - 1) && - !isBetweenIncluded(mapToNumber(array[index - 1]), value, mapToNumber(array[index])) - } - - function calculateNextIndex(a: number, b: number) { - return Math.floor((a + b) / 2) - } - - const length = array.length - - if (length === 0) throw Error('Array length must not be 0') - if (length === 1) return array[0] - - let lowerBound = 0 - let upperBound = length - 1 - let index = calculateNextIndex(upperBound, lowerBound) - let prevIndex = Infinity - - while (isBetweenExcluded(0, index, length - 1) && prevIndex !== index) { - - const current = mapToNumber(array[index]) - if (value < current) { - upperBound = index - } else if (value > current) { - lowerBound = index - } else { - return array[index] - } - - prevIndex = index - index = calculateNextIndex(lowerBound, upperBound) - } - - const nextIndex = index + 1 - const nextIndexValueDiff = mapToNumber(array[nextIndex]) - value - const indexValueDiff = value - mapToNumber(array[index]) - - return nextIndexValueDiff < indexValueDiff ? array[nextIndex] : array[index] -} diff --git a/tutor-assistant-web/src/lib/utils/math-utils.ts b/tutor-assistant-web/src/lib/utils/math-utils.ts deleted file mode 100644 index de5e10b..0000000 --- a/tutor-assistant-web/src/lib/utils/math-utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const randomId = () => - Math.random().toString().replace('.', '') - -export function isBetweenIncluded(lower: number, value: number, upper: number) { - return isBetweenExcluded(lower - 1, value, upper + 1) -} - -export function isBetweenExcluded(lower: number, value: number, upper: number) { - return lower < value && value < upper -} - -export function roundTo(value: number, decimals: number) { - const factor = Math.pow(10, decimals) - return Math.round(value * factor) / factor -} \ No newline at end of file diff --git a/tutor-assistant-web/src/lib/utils/scroll-utils.ts b/tutor-assistant-web/src/lib/utils/scroll-utils.ts deleted file mode 100644 index d47ac42..0000000 --- a/tutor-assistant-web/src/lib/utils/scroll-utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { isNotPresent, isPresent } from './utils' - -export function scrollDown(element: HTMLElement | null) { - const content = getScrollContainer(element) - if (isNotPresent(content)) return - content.scroll({ - top: content.scrollHeight, - behavior: 'smooth' - }) -} - -export function getScrollContainer(element: HTMLElement | null) { - let currentElement = element - - while (isPresent(currentElement)) { - if (currentElement.classList.contains('content-scroller')) { - return currentElement - } - - currentElement = currentElement.parentElement - } -} diff --git a/tutor-assistant-web/src/lib/utils/ui-utils.ts b/tutor-assistant-web/src/lib/utils/ui-utils.ts deleted file mode 100644 index e96f06c..0000000 --- a/tutor-assistant-web/src/lib/utils/ui-utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -export function isHslColorLightOrDark(h: number, s: number, l: number): boolean { - let r: number, g: number, b: number - if (s === 0) { - r = g = b = l // achromatic - } else { - const hue2rgb = (p: number, q: number, t: number): number => { - if (t < 0) t += 1 - if (t > 1) t -= 1 - if (t < 1 / 6) return p + (q - p) * 6 * t - if (t < 1 / 2) return q - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 - return p - } - const q = l < 0.5 ? l * (1 + s) : l + s - l * s - const p = 2 * l - q - r = hue2rgb(p, q, h + 1 / 3) - g = hue2rgb(p, q, h) - b = hue2rgb(p, q, h - 1 / 3) - } - - const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b - - return luminance > 0.5 -} - -export function parseHSL(hsl: string): [number, number, number] | undefined { - const match = hsl.match(/^hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)$/) - if (match) { - const h = parseFloat(match[1]) / 360 - const s = parseFloat(match[2]) / 100 - const l = parseFloat(match[3]) / 100 - return [h, s, l] - } -} diff --git a/tutor-assistant-web/src/lib/utils/utils.ts b/tutor-assistant-web/src/lib/utils/utils.ts deleted file mode 100644 index afc0b39..0000000 --- a/tutor-assistant-web/src/lib/utils/utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -export function isNotPresent(value: any): value is null | undefined { - return value === undefined || value === null -} - -export function isPresent(value: T | undefined | null): value is T { - return !isNotPresent(value) -} - -export const postDecode = (object: any) => { - const mutObject = { ...object } - for (const key in mutObject) { - if (typeof mutObject[key] === object) { - mutObject[key] = postDecode(mutObject[key]) - } - if (key.endsWith('Date')) { - mutObject[key] = new Date(key) - } - } - return mutObject -} - -export function getNavigationLocation() { - return window.location.href.substring(window.location.origin.length) -} - -export function byNumber(a: number, b: number) { - return a - b -} - -export function byNumberReverse(a: number, b: number) { - return b - a -} - -export function stringToNumber(s: string) { - return +s -} - -export function isAppleMobile() { - return /iPad|iPhone|iPod/.test(navigator.userAgent) -} - -export function getCurrentBaseUrl() { - const { protocol, host } = window.location - return `${protocol}//${host}` -} - -export const chill = () => undefined diff --git a/tutor-assistant-web/src/main.tsx b/tutor-assistant-web/src/main.tsx index 15a0d50..f3bff89 100644 --- a/tutor-assistant-web/src/main.tsx +++ b/tutor-assistant-web/src/main.tsx @@ -1,30 +1,19 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import App from './app/App.tsx' +import { App } from './app/App.tsx' import './index.css' import { ReactKeycloakProvider } from '@react-keycloak/web' import { keycloak } from './app/config/keycloak-config.ts' import '@fontsource/inter' -// import '@fontsource/inter/100.css' -// import '@fontsource/inter/200.css' -// import '@fontsource/inter/300.css' -// import '@fontsource/inter/400.css' import '@fontsource/inter/500.css' import '@fontsource/inter/600.css' import '@fontsource/inter/700.css' -// import '@fontsource/inter/800.css' -// import '@fontsource/inter/900.css' -// import '@fontsource/inter/100-italic.css' -// import '@fontsource/inter/200-italic.css' -// import '@fontsource/inter/300-italic.css' -// import '@fontsource/inter/400-italic.css' import '@fontsource/inter/500-italic.css' import '@fontsource/inter/600-italic.css' import '@fontsource/inter/700-italic.css' -// import '@fontsource/inter/800-italic.css' -// import '@fontsource/inter/900-italic.css' +// Keycloak must be outside strict mode because of side effects createRoot(document.getElementById('root')!).render( diff --git a/tutor-assistant-web/src/modules/calendar/calendar-model.ts b/tutor-assistant-web/src/modules/calendar/calendar-model.ts index 41c2f93..b266cda 100644 --- a/tutor-assistant-web/src/modules/calendar/calendar-model.ts +++ b/tutor-assistant-web/src/modules/calendar/calendar-model.ts @@ -1,3 +1,10 @@ +/** + * CalenderEntry based on backend data type + * @property title of the event + * @property date representation of the datetime of the event + * @property time representation of the datetime of the event + * @property isCurrentDate true if datetime is today else false + */ export interface CalendarEntry { title: string date: string diff --git a/tutor-assistant-web/src/modules/calendar/components/CalendarBar.tsx b/tutor-assistant-web/src/modules/calendar/components/CalendarBar.tsx index d5c1921..1d8c5f4 100644 --- a/tutor-assistant-web/src/modules/calendar/components/CalendarBar.tsx +++ b/tutor-assistant-web/src/modules/calendar/components/CalendarBar.tsx @@ -1,33 +1,32 @@ -import { Header } from '../../../common/components/Header.tsx' -import { MainContent, Row } from '../../../lib/components/flex-layout.tsx' +import { Header } from '../../../common/components/containers/Header.tsx' +import { MainContent, Row } from '../../../common/components/containers/flex-layout.tsx' import { Button, IconButton } from '@mui/joy' import { Cached, Logout } from '@mui/icons-material' -import { Bar } from '../../../common/components/Bar.tsx' +import { Bar } from '../../../common/components/containers/Bar.tsx' import React from 'react' import { useAuth } from '../../../app/auth/useAuth.ts' import { useCalendar } from '../useCalendar.ts' import { useTranslation } from 'react-i18next' -import { Scroller } from '../../../lib/components/Scroller.tsx' -import { StyledDivider } from '../../../common/components/StyledDivider.tsx' +import { Scroller } from '../../../common/components/containers/Scroller.tsx' +import { StyledDivider } from '../../../common/components/widgets/StyledDivider.tsx' import { CalendarTable } from './CalendarTable.tsx' -interface Props { - -} - -export function CalendarBar({}: Props) { +/** + * Left sidebar containing a calendar + * Loads and manages and displays the calendar + * Additionally renders logout button at the bottom + */ +export function CalendarBar() { const { t } = useTranslation() const { logout, getRoles } = useAuth() const { calendarEntries, loadNewCalendar } = useCalendar() - console.log(calendarEntries) - const canManage = getRoles().includes('document-manager') return ( + + diff --git a/tutor-assistant-web/src/modules/calendar/useCalendar.ts b/tutor-assistant-web/src/modules/calendar/useCalendar.ts index 0a573d8..325352b 100644 --- a/tutor-assistant-web/src/modules/calendar/useCalendar.ts +++ b/tutor-assistant-web/src/modules/calendar/useCalendar.ts @@ -5,6 +5,13 @@ import { CalendarEntry } from './calendar-model.ts' import { format } from 'date-fns' import { useTranslation } from 'react-i18next' +/** + * Loads and manages the calendar + * + * Provides + * @property calendarEntries loaded + * @property loadNewCalendar requests the creation of a new calendar and loads its entries + */ export function useCalendar() { const { t } = useTranslation() const { getAuthHttp } = useAuth() diff --git a/tutor-assistant-web/src/modules/chat/ChatPage.tsx b/tutor-assistant-web/src/modules/chat/ChatPage.tsx index b10e791..ba2e947 100644 --- a/tutor-assistant-web/src/modules/chat/ChatPage.tsx +++ b/tutor-assistant-web/src/modules/chat/ChatPage.tsx @@ -1,15 +1,15 @@ import React, { useEffect, useRef } from 'react' import { useNavigate, useParams } from 'react-router-dom' -import { isNotPresent, isPresent } from '../../lib/utils/utils.ts' +import { isNotPresent, isPresent } from '../../common/utils/utils.ts' import { useChatManager } from './hooks/useChatManager.ts' import { useTranslation } from 'react-i18next' import { useSelectedChat } from './hooks/useSelectedChat.ts' import { useAsyncActionTrigger } from './hooks/useAsyncActionTrigger.ts' -import { MainContent, Row, VStack } from '../../lib/components/flex-layout.tsx' +import { MainContent, Row, VStack } from '../../common/components/containers/flex-layout.tsx' import { Divider } from '@mui/joy' import { ChatDetails } from './components/details/ChatDetails.tsx' import { ChatOverview } from './components/overview/ChatOverview.tsx' -import { SubmitTextarea } from '../../common/components/SubmitTextarea.tsx' +import { SubmitTextarea } from '../../common/components/widgets/SubmitTextarea.tsx' export function ChatPage() { @@ -73,7 +73,7 @@ export function ChatPage() { {isSelected(message.id) && ( { diff --git a/tutor-assistant-web/src/modules/chat/components/details/ChatMessageList.tsx b/tutor-assistant-web/src/modules/chat/components/details/ChatMessageList.tsx index 7671e4a..6bb3b78 100644 --- a/tutor-assistant-web/src/modules/chat/components/details/ChatMessageList.tsx +++ b/tutor-assistant-web/src/modules/chat/components/details/ChatMessageList.tsx @@ -1,4 +1,4 @@ -import { VStack } from '../../../../lib/components/flex-layout.tsx' +import { VStack } from '../../../../common/components/containers/flex-layout.tsx' import { ChatMessage } from '../../chat-model.ts' import { ChatMessageItem } from './ChatMessageItem.tsx' diff --git a/tutor-assistant-web/src/modules/chat/components/overview/ChatOverview.tsx b/tutor-assistant-web/src/modules/chat/components/overview/ChatOverview.tsx index 73d6a8f..e01e1e9 100644 --- a/tutor-assistant-web/src/modules/chat/components/overview/ChatOverview.tsx +++ b/tutor-assistant-web/src/modules/chat/components/overview/ChatOverview.tsx @@ -1,10 +1,10 @@ import React from 'react' -import { MainContent, VStack } from '../../../../lib/components/flex-layout.tsx' +import { MainContent, VStack } from '../../../../common/components/containers/flex-layout.tsx' import { useTranslation } from 'react-i18next' -import { Scroller } from '../../../../lib/components/Scroller.tsx' -import { ColumnLayout } from '../../../../lib/components/ColumnLayout.tsx' +import { Scroller } from '../../../../common/components/containers/Scroller.tsx' +import { ColumnLayout } from '../../../../common/components/containers/ColumnLayout.tsx' import { useChatManager } from '../../hooks/useChatManager.ts' -import { Header } from '../../../../common/components/Header.tsx' +import { Header } from '../../../../common/components/containers/Header.tsx' import { ChatCard } from './ChatOverviewCard.tsx' import { Button } from '@mui/joy' import { useNavigate } from 'react-router-dom' diff --git a/tutor-assistant-web/src/modules/chat/components/overview/ChatOverviewCard.tsx b/tutor-assistant-web/src/modules/chat/components/overview/ChatOverviewCard.tsx index 134b3cb..99f87b1 100644 --- a/tutor-assistant-web/src/modules/chat/components/overview/ChatOverviewCard.tsx +++ b/tutor-assistant-web/src/modules/chat/components/overview/ChatOverviewCard.tsx @@ -13,7 +13,7 @@ import { MenuItem, Typography, } from '@mui/joy' -import { MainContent, Row, Spacer } from '../../../../lib/components/flex-layout.tsx' +import { MainContent, Row, Spacer } from '../../../../common/components/containers/flex-layout.tsx' import { MoreVert } from '@mui/icons-material' import React from 'react' diff --git a/tutor-assistant-web/src/modules/chat/hooks/useChatManager.ts b/tutor-assistant-web/src/modules/chat/hooks/useChatManager.ts index 8a3dc25..26b1030 100644 --- a/tutor-assistant-web/src/modules/chat/hooks/useChatManager.ts +++ b/tutor-assistant-web/src/modules/chat/hooks/useChatManager.ts @@ -2,8 +2,8 @@ import { Chat as ChatModel, Chat } from '../chat-model.ts' import { apiBaseUrl } from '../../../app/base.ts' import { useAuth } from '../../../app/auth/useAuth.ts' import { useEffect, useMemo } from 'react' -import { remove } from '../../../lib/utils/array-utils.ts' -import { isPresent } from '../../../lib/utils/utils.ts' +import { remove } from '../../../common/utils/array-utils.ts' +import { isPresent } from '../../../common/utils/utils.ts' import { useChatContext } from '../useChatContext.ts' export function useChatManager() { diff --git a/tutor-assistant-web/src/modules/chat/hooks/useChatMessageFeedback.ts b/tutor-assistant-web/src/modules/chat/hooks/useChatMessageFeedback.ts index a4603c9..39e9ac2 100644 --- a/tutor-assistant-web/src/modules/chat/hooks/useChatMessageFeedback.ts +++ b/tutor-assistant-web/src/modules/chat/hooks/useChatMessageFeedback.ts @@ -1,7 +1,7 @@ import { useAuth } from '../../../app/auth/useAuth.ts' import { apiBaseUrl } from '../../../app/base.ts' import { ChatMessageFeedback } from '../chat-model.ts' -import { isNotPresent } from '../../../lib/utils/utils.ts' +import { isNotPresent } from '../../../common/utils/utils.ts' import { useChatContext } from '../useChatContext.ts' export function useChatMessageFeedback() { diff --git a/tutor-assistant-web/src/modules/chat/hooks/useOpenContexts.ts b/tutor-assistant-web/src/modules/chat/hooks/useOpenContexts.ts index 7d293a1..b578e49 100644 --- a/tutor-assistant-web/src/modules/chat/hooks/useOpenContexts.ts +++ b/tutor-assistant-web/src/modules/chat/hooks/useOpenContexts.ts @@ -1,6 +1,6 @@ import { useFiles } from './useFiles.ts' import { ChatMessageContext } from '../chat-model.ts' -import { isNotPresent } from '../../../lib/utils/utils.ts' +import { isNotPresent } from '../../../common/utils/utils.ts' export function useOpenContexts() { const { loadFile } = useFiles() diff --git a/tutor-assistant-web/src/modules/chat/hooks/useSelectedChat.ts b/tutor-assistant-web/src/modules/chat/hooks/useSelectedChat.ts index 3d750dd..293059c 100644 --- a/tutor-assistant-web/src/modules/chat/hooks/useSelectedChat.ts +++ b/tutor-assistant-web/src/modules/chat/hooks/useSelectedChat.ts @@ -1,11 +1,11 @@ import { useEffect, useRef, useState } from 'react' import { Chat, ChatMessage } from '../chat-model.ts' -import { last, lastIndex } from '../../../lib/utils/array-utils.ts' +import { last, lastIndex } from '../../../common/utils/array-utils.ts' import { apiBaseUrl } from '../../../app/base.ts' import { useAuth } from '../../../app/auth/useAuth.ts' import { SSE, SSEvent } from 'sse.js' import { useKeycloak } from '@react-keycloak/web' -import { isNotPresent } from '../../../lib/utils/utils.ts' +import { isNotPresent } from '../../../common/utils/utils.ts' import { useChatContext } from '../useChatContext.ts' export function useSelectedChat(chatId: string | undefined) { diff --git a/tutor-assistant-web/src/modules/documents/DocumentsPage.tsx b/tutor-assistant-web/src/modules/documents/DocumentsPage.tsx index 54523bf..ed74252 100644 --- a/tutor-assistant-web/src/modules/documents/DocumentsPage.tsx +++ b/tutor-assistant-web/src/modules/documents/DocumentsPage.tsx @@ -1,5 +1,5 @@ -import { HStack, MainContent, VStack } from '../../lib/components/flex-layout.tsx' -import { Header } from '../../common/components/Header.tsx' +import { HStack, MainContent, VStack } from '../../common/components/containers/flex-layout.tsx' +import { Header } from '../../common/components/containers/Header.tsx' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { Button } from '@mui/joy' @@ -9,11 +9,12 @@ import { DocumentSettingsList } from './components/DocumentSettingsList.tsx' import { TutorAssistantDocumentsList } from './components/TutorAssistantDocumentsList.tsx' import { useAuth } from '../../app/auth/useAuth.ts' -interface Props { -} - -export function DocumentsPage({}: Props) { +/** + * View and manage documents. + * This includes uploading settings and perform indexing based on these settings. + */ +export function DocumentsPage() { const navigate = useNavigate() const { t } = useTranslation() diff --git a/tutor-assistant-web/src/modules/documents/components/DocumentSettingsList.tsx b/tutor-assistant-web/src/modules/documents/components/DocumentSettingsList.tsx index c5e6af0..425d700 100644 --- a/tutor-assistant-web/src/modules/documents/components/DocumentSettingsList.tsx +++ b/tutor-assistant-web/src/modules/documents/components/DocumentSettingsList.tsx @@ -1,12 +1,12 @@ -import { MainContent, Row, Spacer, VStack } from '../../../lib/components/flex-layout.tsx' +import { MainContent, Row, Spacer, VStack } from '../../../common/components/containers/flex-layout.tsx' import { useDocumentSettings } from '../hooks/useDocumentSettings.tsx' import { StandardList } from './StandardList.tsx' import { useTranslation } from 'react-i18next' import { Add } from '@mui/icons-material' -import { Scroller } from '../../../lib/components/Scroller.tsx' -import { FileButton } from './FileButton.tsx' -import { Bar } from '../../../common/components/Bar.tsx' -import { Header } from '../../../common/components/Header.tsx' +import { Scroller } from '../../../common/components/containers/Scroller.tsx' +import { FileButton } from '../../../common/components/widgets/FileButton.tsx' +import { Bar } from '../../../common/components/containers/Bar.tsx' +import { Header } from '../../../common/components/containers/Header.tsx' import React from 'react' import { useOpenContexts } from '../../chat/hooks/useOpenContexts.ts' @@ -14,6 +14,11 @@ interface Props { canManage: boolean } +/** + * Renders a list for displaying and managing the document settings: main setting, value settings, resources. + * + * @param canManage true if the user can manage the documents, false if they can only view. + */ export function DocumentSettingsList({ canManage }: Props) { const { @@ -83,7 +88,3 @@ export function DocumentSettingsList({ canManage }: Props) { ) } - - - - diff --git a/tutor-assistant-web/src/modules/documents/components/StandardList.tsx b/tutor-assistant-web/src/modules/documents/components/StandardList.tsx index e224940..a924ed0 100644 --- a/tutor-assistant-web/src/modules/documents/components/StandardList.tsx +++ b/tutor-assistant-web/src/modules/documents/components/StandardList.tsx @@ -2,7 +2,7 @@ import { Box, Divider, IconButton, List, ListItem, ListItemButton, Typography } import { Cached, Delete } from '@mui/icons-material' import React from 'react' import { useTranslation } from 'react-i18next' -import { isNotPresent, isPresent } from '../../../lib/utils/utils.ts' +import { isNotPresent, isPresent } from '../../../common/utils/utils.ts' interface Props { title?: string @@ -14,6 +14,18 @@ interface Props { canManage?: boolean } +/** + * Renders items in a list. + * Provides reload and delete icon with configurable action. + * + * @param title rendered above the list iff present. + * @param items to be rendered. + * @param getLabel returns text to be rendered in each list item for each given item. + * @param onClick performed on list item click. + * @param onReload performed on reload icon clicked. + * @param onDelete performed on delete icon clicked. + * @param canManage true if user can reload and delete, false if they can only view. + */ export function StandardList( { title, items, getLabel, onClick, onReload, onDelete, canManage }: Props, ) { diff --git a/tutor-assistant-web/src/modules/documents/components/TutorAssistantDocumentsList.tsx b/tutor-assistant-web/src/modules/documents/components/TutorAssistantDocumentsList.tsx index 46f2a08..264efc4 100644 --- a/tutor-assistant-web/src/modules/documents/components/TutorAssistantDocumentsList.tsx +++ b/tutor-assistant-web/src/modules/documents/components/TutorAssistantDocumentsList.tsx @@ -1,56 +1,31 @@ import { useTranslation } from 'react-i18next' import { useTutorAssistantDocuments } from '../hooks/useTutorAssistantDocuments.ts' -import { Row, Spacer, VStack } from '../../../lib/components/flex-layout.tsx' +import { Row, Spacer, VStack } from '../../../common/components/containers/flex-layout.tsx' import { Accordion, AccordionDetails, AccordionGroup, AccordionSummary, Button, Typography } from '@mui/joy' import { StandardList } from './StandardList.tsx' -import { Scroller } from '../../../lib/components/Scroller.tsx' +import { Scroller } from '../../../common/components/containers/Scroller.tsx' import { useOpenContexts } from '../../chat/hooks/useOpenContexts.ts' -import { useMemo } from 'react' -import { FileDocument, WebsiteDocument } from '../model.ts' interface Props { canManage: boolean } +/** + * Renders a list and buttons for viewing and managing file and website documents. + * + * @param canManage true if the user can index, reindex and delete, false if the user can only view. + */ export function TutorAssistantDocumentsList({ canManage }: Props) { const { t } = useTranslation() const { index, - files, - websites, + groupedDocuments, reindexFile, reindexWebsite, deleteFile, deleteWebsite, } = useTutorAssistantDocuments() - const generalKey = t('General') - const grouped = useMemo(() => { - const result = { - [generalKey]: { - files: [] as FileDocument[], - websites: [] as WebsiteDocument[], - }, - } - - files.forEach((file) => { - const key = file.collection ?? generalKey - if (!(key in result)) { - result[key] = { files: [], websites: [] } - } - result[key].files.push(file) - }) - - websites.forEach((website) => { - const key = website.collection ?? generalKey - if (!(key in result)) { - result[key] = { files: [], websites: [] } - } - result[key].websites.push(website) - }) - return result - }, [files, websites]) - const { openContexts } = useOpenContexts() return ( @@ -65,14 +40,14 @@ export function TutorAssistantDocumentsList({ canManage }: Props) { { - Object.keys(grouped).map((key, index) => ( + Object.keys(groupedDocuments).map((key, index) => ( {key} file.title} onClick={file => openContexts(file.fileStoreId)} onReload={file => reindexFile(file.id)} @@ -80,7 +55,7 @@ export function TutorAssistantDocumentsList({ canManage }: Props) { canManage={canManage} /> website.title} onClick={website => openContexts(website.url)} onReload={website => reindexWebsite(website.id)} diff --git a/tutor-assistant-web/src/modules/documents/hooks/useDocumentSettings.tsx b/tutor-assistant-web/src/modules/documents/hooks/useDocumentSettings.tsx index 8ca7257..75dc9e4 100644 --- a/tutor-assistant-web/src/modules/documents/hooks/useDocumentSettings.tsx +++ b/tutor-assistant-web/src/modules/documents/hooks/useDocumentSettings.tsx @@ -1,10 +1,23 @@ import { apiBaseUrl } from '../../../app/base.ts' -import { append, partition, remove } from '../../../lib/utils/array-utils.ts' +import { append, partition, remove } from '../../../common/utils/array-utils.ts' import { useAuth } from '../../../app/auth/useAuth.ts' import { useEffect, useMemo, useState } from 'react' import { Resource, Setting } from '../model.ts' -import { isNotPresent } from '../../../lib/utils/utils.ts' - +import { isNotPresent } from '../../../common/utils/utils.ts' + + +/** + * Manages main settings, value settings and resources. + * + * Provides: + * @property mainSettings array. + * @property valueSettings array. + * @property resources array. + * @property addSetting add a main setting or a value setting. + * @property deleteSetting delete a main setting or a value setting. + * @property addResource add a resource. + * @property deleteResource delete a resource. + */ export function useDocumentSettings() { const { getAuthHttp } = useAuth() diff --git a/tutor-assistant-web/src/modules/documents/hooks/useTutorAssistantDocuments.ts b/tutor-assistant-web/src/modules/documents/hooks/useTutorAssistantDocuments.ts index 8170270..325117e 100644 --- a/tutor-assistant-web/src/modules/documents/hooks/useTutorAssistantDocuments.ts +++ b/tutor-assistant-web/src/modules/documents/hooks/useTutorAssistantDocuments.ts @@ -1,15 +1,59 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useAuth } from '../../../app/auth/useAuth.ts' import { apiBaseUrl } from '../../../app/base.ts' -import { remove } from '../../../lib/utils/array-utils.ts' -import { isNotPresent } from '../../../lib/utils/utils.ts' +import { remove } from '../../../common/utils/array-utils.ts' +import { isNotPresent } from '../../../common/utils/utils.ts' import { FileDocument, WebsiteDocument } from '../model.ts' +import { useTranslation } from 'react-i18next' + +/** + * Manages file and website documents. + * + * @property index function to run the indexing process. + * @property groupedDocuments file and website documents grouped by their collection. + * Each collection contains its file and website documents. + * @property websites website documents. + * @property reindexFile function to reindex a file. + * @property reindexWebsite function to reindex a website. + * @property deleteFile function to delete a file. + * @property deleteWebsite function to delete a website. + */ export function useTutorAssistantDocuments() { + const { t } = useTranslation() + const { getAuthHttp } = useAuth() + const [files, setFiles] = useState([]) const [websites, setWebsites] = useState([]) - const { getAuthHttp } = useAuth() + const generalKey = t('General') + + const groupedDocuments = useMemo(() => { + const result = { + [generalKey]: { + files: [] as FileDocument[], + websites: [] as WebsiteDocument[], + }, + } + + files.forEach((file) => { + const key = file.collection ?? generalKey + if (!(key in result)) { + result[key] = { files: [], websites: [] } + } + result[key].files.push(file) + }) + + websites.forEach((website) => { + const key = website.collection ?? generalKey + if (!(key in result)) { + result[key] = { files: [], websites: [] } + } + result[key].websites.push(website) + }) + return result + }, [files, websites]) + useEffect(() => { loadFiles() @@ -57,8 +101,7 @@ export function useTutorAssistantDocuments() { return { index, - files, - websites, + groupedDocuments, reindexFile, reindexWebsite, deleteFile, diff --git a/tutor-assistant-web/src/modules/documents/model.ts b/tutor-assistant-web/src/modules/documents/model.ts index ef58331..c2ffd98 100644 --- a/tutor-assistant-web/src/modules/documents/model.ts +++ b/tutor-assistant-web/src/modules/documents/model.ts @@ -1,3 +1,11 @@ +/** + * Main or value setting + * + * @property id unique string + * @property name unique human-readable id + * @property content specifying what setting to perform + * @property type either 'MAIN' or 'VALUES' + */ export interface Setting { id: string name: string @@ -5,12 +13,25 @@ export interface Setting { type: 'MAIN' | 'VALUES' } +/** + * Resource like a file + * + * @property id unique string + * @property displayName unique human-readable id + */ export interface Resource { id: string displayName: string } - +/** + * Indexed document + * + * @property id unique string + * @property title human readable string, must not be unique + * @property loaderType specifying how the document is loaded + * @property collection for grouping + */ export interface TutorAssistantDocument { id: string title: string @@ -18,10 +39,20 @@ export interface TutorAssistantDocument { collection?: string } +/** + * Indexed file document + * + * @property fileStoreId unique string from the file store + */ export interface FileDocument extends TutorAssistantDocument { fileStoreId: string } +/** + * Indexed website document + * + * @property url of the website + */ export interface WebsiteDocument extends TutorAssistantDocument { url: string } \ No newline at end of file diff --git a/tutor-assistant-web/src/modules/helper/HelperPage.tsx b/tutor-assistant-web/src/modules/helper/HelperPage.tsx deleted file mode 100644 index 4b499b2..0000000 --- a/tutor-assistant-web/src/modules/helper/HelperPage.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Route, Routes } from 'react-router-dom' -import { AllInfos } from './components/AllInfos.tsx' - -interface Props { - -} - -export function HelperPage({}: Props) { - return ( - - >} /> - } /> - - ) -} diff --git a/tutor-assistant-web/src/modules/helper/components/AllInfos.tsx b/tutor-assistant-web/src/modules/helper/components/AllInfos.tsx deleted file mode 100644 index dd16130..0000000 --- a/tutor-assistant-web/src/modules/helper/components/AllInfos.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { useAuth } from '../../../app/auth/useAuth.ts' -import { apiBaseUrl } from '../../../app/base.ts' -import { HStack, MainContent, VStack } from '../../../lib/components/flex-layout.tsx' -import { Bar } from '../../../common/components/Bar.tsx' -import { Header } from '../../../common/components/Header.tsx' -import { IconButton } from '@mui/joy' -import { Cached } from '@mui/icons-material' -import { useCalendar } from '../../calendar/useCalendar.ts' -import { Scroller } from '../../../lib/components/Scroller.tsx' -import { range } from '../../../lib/utils/array-utils.ts' -import { StyledMarkdown } from '../../../common/components/StyledMarkdown.tsx' -import remarkGfm from 'remark-gfm' - -interface Props { - -} - -export function AllInfos({}: Props) { - - return <>> - /*const [infos, setInfos] = useState([]) - const { info, loadNewInfo } = useCalendar() - const { getAuthHttp } = useAuth() - - useEffect(() => { - loadInfos() - }, [info]) - - async function loadInfos() { - const { data } = await getAuthHttp().get(`${apiBaseUrl}/info/all`) - const transformedData = data.map(it => it.startsWith('"{') ? transformJson(it) : transformMarkdown(it)) - setInfos(transformedData) - } - - function transformJson(json: string) { - // @ts-ignore - const replaced = json.slice(1, json.length - 1).replaceAll('\\n', '').replaceAll('\\"', '"') - const value = JSON.parse(replaced).events.sort((a: { date: string }, b: { date: string }) => { - const reversedA = a.date.split('').reverse().join('') - const reversedB = b.date.split('').reverse().join('') - return reversedA.localeCompare(reversedB) - }) - return { type: 'json', value } - } - - function transformMarkdown(markdown: string) { - // @ts-ignore - return { type: 'markdown', value: markdown.slice(1, markdown.length - 1).replaceAll('\\n', '\n') } - } - - async function handleReload() { - for (let _ of range(0, 5)) { - await loadNewInfo() - } - } - - return ( - - - - - } - /> - - - {infos.filter((_: any, index: number) => index >= 0 && index < 40).map((info: any, index: any) => ( - info.type === 'json' - ? ( - - - - - - Nr. - Datum - Ereignis - - - - {info.value.map((entry: any, index: any) => ( - - {index + 1} - {entry.date} - {entry.title} um {entry.time} - - ))} - - - - - ) - : ( - - {info.value} - - ) - - ))} - - - - )*/ -} diff --git a/tutor-assistant/tutor_assistant/controller/config/domain_config.py b/tutor-assistant/tutor_assistant/controller/config/domain_config.py index c33cb33..25a9df9 100644 --- a/tutor-assistant/tutor_assistant/controller/config/domain_config.py +++ b/tutor-assistant/tutor_assistant/controller/config/domain_config.py @@ -7,14 +7,16 @@ from tutor_assistant.domain.domain_config import DomainConfig from tutor_assistant.domain.vector_stores.chroma_repo import ChromaRepo -_use_base_retriever = True +# _use_base_retriever = True _embeddings = OpenAIEmbeddings(model='text-embedding-3-large') -_store = ChromaRepo(f"{os.getenv('DATA_DIR')}/chroma_ws2324_with_meta_docs_index", _embeddings) +# _store = ChromaRepo(f"{os.getenv('DATA_DIR')}/chroma_ws2324_with_meta_docs_index", _embeddings) -if _use_base_retriever: - _store = ChromaRepo(f"{os.getenv('DATA_DIR')}/chroma_ws2324_no_meta_docs_index", _embeddings) +# if _use_base_retriever: +# _store = ChromaRepo(f"{os.getenv('DATA_DIR')}/chroma_ws2324_no_meta_docs_index", _embeddings) + +_store = ChromaRepo(f"{os.getenv('DATA_DIR')}/chroma_index", _embeddings) config = DomainConfig( ChatOpenAI(model='gpt-4o', temperature=0), @@ -24,5 +26,5 @@ load_resources(f'{os.getcwd()}/resources'), get_logger(), "Deutsch", - _use_base_retriever + # _use_base_retriever )