diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76ff3c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +.DS_STORE diff --git a/tutor-assistant-app-service/Dockerfile b/tutor-assistant-app-service/Dockerfile new file mode 100644 index 0000000..38fb207 --- /dev/null +++ b/tutor-assistant-app-service/Dockerfile @@ -0,0 +1,16 @@ +FROM eclipse-temurin:21-alpine AS build +WORKDIR /app + +COPY gradlew . +COPY gradle gradle + +COPY build.gradle.kts settings.gradle.kts ./ +RUN chmod +x gradlew && ./gradlew dependencies + +COPY src src +RUN chmod +x gradlew && ./gradlew build -x test + +FROM eclipse-temurin:21-alpine +COPY --from=build /app/build/libs/tutor-assistant-app-service-0.0.1-SNAPSHOT.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/app.jar"] diff --git a/tutor-assistant-app-service/build.gradle.kts b/tutor-assistant-app-service/build.gradle.kts new file mode 100644 index 0000000..91658a7 --- /dev/null +++ b/tutor-assistant-app-service/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + kotlin("jvm") version "1.9.25" + kotlin("plugin.spring") version "1.9.25" + id("org.springframework.boot") version "3.3.3" + id("io.spring.dependency-management") version "1.1.6" + kotlin("plugin.jpa") version "1.9.25" +} + +group = "de.niklaskerkhoff" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + runtimeOnly("org.postgresql:postgresql") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("io.projectreactor:reactor-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + implementation("io.ktor:ktor-client-core:3.0.0") + implementation("io.ktor:ktor-client-cio:3.0.0") + implementation("io.ktor:ktor-client-content-negotiation:3.0.0") + implementation("io.ktor:ktor-serialization-jackson:3.0.0") + + implementation("org.apache.commons:commons-text:1.12.0") +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/tutor-assistant-app-service/gradle/wrapper/gradle-wrapper.jar b/tutor-assistant-app-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/tutor-assistant-app-service/gradle/wrapper/gradle-wrapper.jar differ diff --git a/tutor-assistant-app-service/gradle/wrapper/gradle-wrapper.properties b/tutor-assistant-app-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..df97d72 --- /dev/null +++ b/tutor-assistant-app-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/tutor-assistant-app-service/gradlew b/tutor-assistant-app-service/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/tutor-assistant-app-service/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/tutor-assistant-app-service/gradlew.bat b/tutor-assistant-app-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/tutor-assistant-app-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/tutor-assistant-app-service/settings.gradle.kts b/tutor-assistant-app-service/settings.gradle.kts new file mode 100644 index 0000000..2ad1a46 --- /dev/null +++ b/tutor-assistant-app-service/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "tutor-assistant-app-service" diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/TutorAssistantAppServiceApplication.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/TutorAssistantAppServiceApplication.kt new file mode 100644 index 0000000..0f52d6e --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/TutorAssistantAppServiceApplication.kt @@ -0,0 +1,13 @@ +package de.niklaskerkhoff.tutorassistantappservice + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.data.jpa.repository.config.EnableJpaAuditing + +@SpringBootApplication +@EnableJpaAuditing +class TutorAssistantAppServiceApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/config/SecurityConfig.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/config/SecurityConfig.kt new file mode 100644 index 0000000..c8d9b27 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/config/SecurityConfig.kt @@ -0,0 +1,58 @@ +package de.niklaskerkhoff.tutorassistantappservice.config + +import de.niklaskerkhoff.tutorassistantappservice.lib.security.KeycloakJwtAuthenticationConverter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.SecurityFilterChain +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import org.springframework.web.filter.CorsFilter + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +class SecurityConfig { + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + csrf { disable() } + cors { } + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2ResourceServer { + jwt { + jwtAuthenticationConverter = KeycloakJwtAuthenticationConverter() + } + } + sessionManagement { + sessionCreationPolicy = SessionCreationPolicy.STATELESS + } + } + return http.build() + } + + @Bean + fun corsFilter(): CorsFilter { + val source = UrlBasedCorsConfigurationSource() + val config = CorsConfiguration() + config.allowCredentials = true + config.allowedOrigins = listOf( + "http://localhost:5173", + "http://localhost:5174", + ) + config.allowedHeaders = listOf( + "Origin", "Content-Type", "Accept", "Authorization", + "Access-Control-Allow-Origin" + ) + config.allowedMethods = + listOf("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + source.registerCorsConfiguration("/**", config) + return CorsFilter(source) + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/config/WebClientConfig.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/config/WebClientConfig.kt new file mode 100644 index 0000000..b604036 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/config/WebClientConfig.kt @@ -0,0 +1,15 @@ +package de.niklaskerkhoff.tutorassistantappservice.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.reactive.function.client.WebClient + +@Configuration +class WebClientConfig { + @Bean + fun webClient(): WebClient { + return WebClient.builder() + .codecs { it.defaultCodecs().maxInMemorySize(10 * 1024 * 1024) } + .build() + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/app_components/AppController.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/app_components/AppController.kt new file mode 100644 index 0000000..b8dad03 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/app_components/AppController.kt @@ -0,0 +1,6 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.app_components + +import de.niklaskerkhoff.tutorassistantappservice.lib.logging.Logger + +abstract class AppController : Logger { +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/app_components/AppService.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/app_components/AppService.kt new file mode 100644 index 0000000..e6672a9 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/app_components/AppService.kt @@ -0,0 +1,6 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.app_components + +import de.niklaskerkhoff.tutorassistantappservice.lib.logging.Logger + +abstract class AppService : Logger { +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/AppEntity.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/AppEntity.kt new file mode 100644 index 0000000..d4640fa --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/AppEntity.kt @@ -0,0 +1,38 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.entities + +import jakarta.persistence.* +import org.springframework.data.annotation.CreatedBy +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedBy +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import java.time.Instant +import java.util.* + +@Entity +@EntityListeners(AuditingEntityListener::class) +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +abstract class AppEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + open val id: UUID? = null + +// @Version +// open val version = 0 + + @CreatedDate + open var createdDate: Instant? = null + protected set + + @LastModifiedDate + open var lastModifiedDate: Instant? = null + protected set + + @CreatedBy + open var createdBy: String? = null + protected set + + @LastModifiedBy + open var lastModifiedBy: String? = null + protected set +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/AppEntityRepo.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/AppEntityRepo.kt new file mode 100644 index 0000000..7fe8d8a --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/AppEntityRepo.kt @@ -0,0 +1,8 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.entities + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.NoRepositoryBean +import java.util.* + +@NoRepositoryBean +interface AppEntityRepo : JpaRepository diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/AppEntityRepoExtensions.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/AppEntityRepoExtensions.kt new file mode 100644 index 0000000..4fe4810 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/AppEntityRepoExtensions.kt @@ -0,0 +1,6 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.entities + +import org.springframework.data.repository.findByIdOrNull +import java.util.* + +fun AppEntityRepo.findByIdOrThrow(id: UUID) = findByIdOrNull(id) ?: throw IllegalArgumentException(id.toString()) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/DefaultJpaAuditingConfig.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/DefaultJpaAuditingConfig.kt new file mode 100644 index 0000000..67c121b --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/entities/DefaultJpaAuditingConfig.kt @@ -0,0 +1,17 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.entities + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.domain.AuditorAware +import org.springframework.security.core.context.SecurityContextHolder +import java.util.* + +@Configuration +class DefaultJpaAuditingConfig { + @Bean + @ConditionalOnMissingBean + fun auditorProvider() = AuditorAware { + Optional.ofNullable(SecurityContextHolder.getContext().authentication?.name) + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/exceptions/ResponseStatusExceptions.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/exceptions/ResponseStatusExceptions.kt new file mode 100644 index 0000000..f8cd5cd --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/exceptions/ResponseStatusExceptions.kt @@ -0,0 +1,6 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.exceptions + +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException + +class BadRequestException(message: String) : ResponseStatusException(HttpStatus.BAD_REQUEST, message) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreDtos.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreDtos.kt new file mode 100644 index 0000000..424a9a0 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreDtos.kt @@ -0,0 +1,15 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.filestore + +import java.util.* + +data class FileStoreFileReferenceDto( + val id: UUID?, + val displayName: String +) { + constructor(fileReference: FileStoreFileReference) : this( + id = fileReference.id, + displayName = fileReference.displayName + ) +} + +fun FileStoreFileReference.toDto() = FileStoreFileReferenceDto(this) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreFileReferenceDefaultRepo.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreFileReferenceDefaultRepo.kt new file mode 100644 index 0000000..cfc5cfc --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreFileReferenceDefaultRepo.kt @@ -0,0 +1,7 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.filestore + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntityRepo + +interface FileStoreFileReferenceDefaultRepo : AppEntityRepo { + fun findAllByDisplayName(name: String): List +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreModel.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreModel.kt new file mode 100644 index 0000000..2c5d5b4 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreModel.kt @@ -0,0 +1,33 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.filestore + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntity +import jakarta.persistence.Embeddable +import jakarta.persistence.Embedded +import jakarta.persistence.Entity + +@Entity +class FileStoreFileReference( + @Embedded val assignment: FileStoreAssignment, + @Embedded val upload: FileStoreUpload, + val storeUrl: String, + val displayName: String = upload.name +) : AppEntity() + +@Embeddable +data class FileStoreAssignment( + val fid: String, + val url: String, + val publicUrl: String, + val count: Int, +) + +@Embeddable +data class FileStoreUpload( + val name: String, + val size: Long, + val eTag: String, +) + +data class FileStoreDelete( + val size: Long, +) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreService.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreService.kt new file mode 100644 index 0000000..b1df683 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/filestore/FileStoreService.kt @@ -0,0 +1,81 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.filestore + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.findByIdOrThrow +import de.niklaskerkhoff.tutorassistantappservice.lib.webclient.EmptyResponseBodyException +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.io.InputStreamResource +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import org.springframework.web.multipart.MultipartFile +import org.springframework.web.reactive.function.client.WebClient +import java.util.* + +@Service +class FileStoreService( + private val webClient: WebClient, + private val fileStoreFileReferenceDefaultRepo: FileStoreFileReferenceDefaultRepo +) { + @Value("\${app.seaweedfs.master-url}") + private lateinit var masterUrl: String + + fun listFiles(): List = fileStoreFileReferenceDefaultRepo.findAll() + + fun getFileById(id: UUID): Pair { + val fileReference = fileStoreFileReferenceDefaultRepo.findByIdOrThrow(id) + + val fileBytes = webClient.get() + .uri(fileReference.storeUrl) + .retrieve() + .bodyToMono(ByteArray::class.java) + .block() ?: throw EmptyResponseBodyException() + + return Pair(fileReference, InputStreamResource(fileBytes.inputStream())) + } + + fun assignAndUpload(file: MultipartFile, displayName: String? = null): FileStoreFileReference { + val assignment = assign() + val storeUrl = "http://${assignment.publicUrl}/${assignment.fid}" + val upload = upload(storeUrl, file) + + val fileReference = FileStoreFileReference(assignment, upload, storeUrl, displayName ?: upload.name) + + return fileStoreFileReferenceDefaultRepo.save(fileReference) + } + + fun assign(): FileStoreAssignment { + return webClient.get() + .uri(masterUrl) + .retrieve() + .bodyToMono(FileStoreAssignment::class.java) + .block() ?: throw EmptyResponseBodyException() + } + + fun upload(storeUrl: String, file: MultipartFile): FileStoreUpload { + val body: MultiValueMap = LinkedMultiValueMap() + body.add("file", file.resource) + + return webClient.post() + .uri(storeUrl) + .contentType(MediaType.MULTIPART_FORM_DATA) + .bodyValue(body) + .retrieve() + .bodyToMono(FileStoreUpload::class.java) + .block() ?: throw EmptyResponseBodyException() + } + + fun deleteById(id: UUID): FileStoreDelete { + val fileReference = fileStoreFileReferenceDefaultRepo.findByIdOrThrow(id) + + val result = webClient.delete() + .uri(fileReference.storeUrl) + .retrieve() + .bodyToMono(FileStoreDelete::class.java) + .block() ?: throw EmptyResponseBodyException() + + fileStoreFileReferenceDefaultRepo.delete(fileReference) + + return result + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/logging/Logger.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/logging/Logger.kt new file mode 100644 index 0000000..bd8c9e9 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/logging/Logger.kt @@ -0,0 +1,8 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.logging + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +interface Logger { + val log: Logger get() = LoggerFactory.getLogger(javaClass) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/logging/RequestLoggingFilter.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/logging/RequestLoggingFilter.kt new file mode 100644 index 0000000..910785d --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/logging/RequestLoggingFilter.kt @@ -0,0 +1,27 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.logging + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class RequestLoggingFilter : OncePerRequestFilter(), Logger { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val method = request.method + val url = request.requestURL.toString() + + log.info("Incoming request: Method=$method, URL=$url") + + filterChain.doFilter(request, response) + + val statusCode = response.status + logger.info("Outgoing response: StatusCode=$statusCode, Method=$method, URL=$url") + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/security/KeycloakJwtAuthenticationConverter.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/security/KeycloakJwtAuthenticationConverter.kt new file mode 100644 index 0000000..055971a --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/security/KeycloakJwtAuthenticationConverter.kt @@ -0,0 +1,27 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.security + +import org.springframework.core.convert.converter.Converter +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter + + +class KeycloakJwtAuthenticationConverter : Converter { + + override fun convert(jwt: Jwt): AbstractAuthenticationToken? { + val converter = JwtAuthenticationConverter() + + converter.setJwtGrantedAuthoritiesConverter { + jwt.claims["realm_access"] + ?.let { + val realmAccess = it as? Map<*, *> + realmAccess?.get("roles") as? List<*> + } + ?.map { SimpleGrantedAuthority("ROLE_$it") } + ?: emptyList() + } + + return converter.convert(jwt) + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/webclient/WebClientExceptions.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/webclient/WebClientExceptions.kt new file mode 100644 index 0000000..27911cd --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/lib/webclient/WebClientExceptions.kt @@ -0,0 +1,3 @@ +package de.niklaskerkhoff.tutorassistantappservice.lib.webclient + +class EmptyResponseBodyException : RuntimeException("Response body must not be null") diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/CalendarController.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/CalendarController.kt new file mode 100644 index 0000000..513962e --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/CalendarController.kt @@ -0,0 +1,23 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.calendar + +import de.niklaskerkhoff.tutorassistantappservice.modules.calendar.entities.Calendar +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("calendar") +class CalendarController( + private val calendarService: CalendarService +) { + @GetMapping + fun getInfo(@RequestParam currentDate: String, @RequestParam currentTitle: String): List = + calendarService.getCalendar(currentDate, currentTitle) + + @GetMapping("all") + @PreAuthorize("hasRole('document-manager')") + fun getAllInfos(): List = calendarService.getAllCalendars() + + @PostMapping + @PreAuthorize("hasRole('document-manager')") + fun reloadInfo() = calendarService.loadNewCalendar() +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/CalendarData.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/CalendarData.kt new file mode 100644 index 0000000..1cf2128 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/CalendarData.kt @@ -0,0 +1,17 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.calendar + +import de.niklaskerkhoff.tutorassistantappservice.modules.calendar.entities.CalendarEntry + +data class CalendarFrontendData( + val title: String, + val date: String, + val time: String?, + val isCurrentDate: Boolean, +) { + constructor(entry: CalendarEntry, isCurrentDate: Boolean) : this( + entry.title, + entry.date, + entry.time, + isCurrentDate, + ) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/CalendarService.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/CalendarService.kt new file mode 100644 index 0000000..f0bfd80 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/CalendarService.kt @@ -0,0 +1,49 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.calendar + +import de.niklaskerkhoff.tutorassistantappservice.lib.app_components.AppService +import de.niklaskerkhoff.tutorassistantappservice.lib.webclient.EmptyResponseBodyException +import de.niklaskerkhoff.tutorassistantappservice.modules.calendar.entities.Calendar +import de.niklaskerkhoff.tutorassistantappservice.modules.calendar.entities.CalendarEntry +import de.niklaskerkhoff.tutorassistantappservice.modules.calendar.entities.CalendarRepo +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient + +@Service +class CalendarService( + private val webClient: WebClient, + private val calendarRepo: CalendarRepo +) : AppService() { + @Value("\${app.tutor-assistant.base-url}") + private lateinit var baseUrl: String + + fun getCalendar(currentDate: String, currentTitle: String) = + calendarRepo.findFirstByOrderByCreatedDateDesc() + ?.let { listOf(CalendarEntry(currentTitle, currentDate, null)) + it.entries } + ?.sorted() + ?.map { CalendarFrontendData(it, it.date == currentDate) } + ?: emptyList() + + fun getAllCalendars() = calendarRepo.findAll().sortedBy { it.createdDate } + + fun loadNewCalendar(): List { + + data class Response(val entries: List) + + val response = webClient.post() + .uri("$baseUrl/calendar") + .retrieve() + .bodyToMono(Response::class.java) + .block() ?: throw EmptyResponseBodyException() + + log.info("Retrieved calendar from Tutor Assistant. Size: ${response.entries.size}") + + return calendarRepo.save(Calendar(response.entries)) + .also { log.info("Saved calendar: ${it.id}") } + .entries.sorted() + } + + private fun List.sorted() = sortedBy { + (it.date.split(".").reversed() + (it.time ?: "")).joinToString("") + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/entities/Calendar.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/entities/Calendar.kt new file mode 100644 index 0000000..f93037c --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/entities/Calendar.kt @@ -0,0 +1,11 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.calendar.entities + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntity +import jakarta.persistence.ElementCollection +import jakarta.persistence.Entity + +@Entity +class Calendar( + @ElementCollection + val entries: List +) : AppEntity() diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/entities/CalendarEntry.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/entities/CalendarEntry.kt new file mode 100644 index 0000000..f7bd1ec --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/entities/CalendarEntry.kt @@ -0,0 +1,12 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.calendar.entities + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +data class CalendarEntry( + @Column(length = 1023) + val title: String, + val date: String, + val time: String?, +) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/entities/CalendarRepo.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/entities/CalendarRepo.kt new file mode 100644 index 0000000..3d0fdfa --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/calendar/entities/CalendarRepo.kt @@ -0,0 +1,7 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.calendar.entities + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntityRepo + +interface CalendarRepo : AppEntityRepo { + fun findFirstByOrderByCreatedDateDesc(): Calendar? +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/controller/ChatController.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/controller/ChatController.kt new file mode 100644 index 0000000..970278d --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/controller/ChatController.kt @@ -0,0 +1,50 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.controller + +import de.niklaskerkhoff.tutorassistantappservice.lib.app_components.AppController +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.ChatBaseData +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.ChatMainData +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.ChatService +import org.springframework.http.CacheControl +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken +import org.springframework.web.bind.annotation.* +import reactor.core.publisher.Flux +import java.util.* + +@RestController +@RequestMapping("chats") +class ChatController( + private val chatService: ChatService +) : AppController() { + @GetMapping("{chatId}") + fun getChatById(@PathVariable chatId: UUID, jwt: JwtAuthenticationToken): ChatMainData = + chatService.getChatById(chatId, jwt.name) + + @GetMapping + fun getChats(jwt: JwtAuthenticationToken): List = chatService.getChats(jwt.name) + + @PostMapping + fun createChat(jwt: JwtAuthenticationToken): ChatBaseData = chatService.createChat(jwt.name) + + @DeleteMapping("{chatId}") + fun deleteChat(@PathVariable chatId: UUID, jwt: JwtAuthenticationToken): Unit = + chatService.deleteChat(chatId, jwt.name) + + @PostMapping("{chatId}/messages", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) + fun sendMessageToExistingChat( + @PathVariable chatId: UUID, + @RequestBody message: String, + jwt: JwtAuthenticationToken + ): ResponseEntity> { + val flux = chatService.sendMessage(chatId, message, jwt.name) + val headers = HttpHeaders().apply { + contentType = MediaType.TEXT_EVENT_STREAM + cacheControl = CacheControl.noCache().headerValue + this["X-Accel-Buffering"] = "no" + } + + return ResponseEntity.ok().headers(headers).body(flux) + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/controller/MessageController.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/controller/MessageController.kt new file mode 100644 index 0000000..9839962 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/controller/MessageController.kt @@ -0,0 +1,37 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.controller + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.findByIdOrThrow +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages.MessageFeedback +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages.MessageRepo +import org.springframework.web.bind.annotation.* +import java.util.* + +@RestController +@RequestMapping("chats/messages") +class MessageController( + private val messageRepo: MessageRepo +) { + @GetMapping("{id}") + fun getMessageFeedback(@PathVariable id: UUID) = messageRepo.findByIdOrThrow(id).feedback + + + data class RatingPatch(val rating: Int) + + @PatchMapping("{id}/feedback-rating") + fun setMessageRating(@PathVariable id: UUID, @RequestBody patch: RatingPatch): MessageFeedback { + val message = messageRepo.findByIdOrThrow(id) + message.feedback = message.feedback.copy(rating = patch.rating) + messageRepo.save(message) + return message.feedback + } + + data class ContentPatch(val content: String) + + @PatchMapping("{id}/feedback-content") + fun setMessageRating(@PathVariable id: UUID, @RequestBody patch: ContentPatch): MessageFeedback { + val message = messageRepo.findByIdOrThrow(id) + message.feedback = message.feedback.copy(content = patch.content.trim()) + messageRepo.save(message) + return message.feedback + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/Chat.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/Chat.kt new file mode 100644 index 0000000..7ccbca2 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/Chat.kt @@ -0,0 +1,24 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.model + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntity +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages.Message +import jakarta.persistence.* +import java.util.* + +@Entity +class Chat( + val userId: String, + @Embedded + var summary: ChatSummary? = null, +) : AppEntity() { + + @OneToMany(orphanRemoval = true) + @OrderBy("createdDate ASC") + private val _messages = mutableListOf() + + @get:OneToMany + val messages: List get() = _messages.toList() + + fun addMessage(message: Message) = _messages.add(message) + fun removeMessage(messageId: UUID) = _messages.removeIf { it.id == messageId } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatData.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatData.kt new file mode 100644 index 0000000..4a35851 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatData.kt @@ -0,0 +1,36 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.model + +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages.Message +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages.MessageContext +import java.util.* + +data class ChatMainData( + val id: UUID?, + val summary: ChatSummary?, + val messages: List +) { + constructor(chat: Chat) : this( + chat.id, + chat.summary, + chat.messages.mapNotNull { + if (it.role == "system") null + else MessageMainData(it) + } + ) +} + +data class ChatBaseData( + val id: UUID?, + val summary: ChatSummary?, +) { + constructor(chat: Chat) : this(chat.id, chat.summary) +} + +data class MessageMainData( + val id: UUID?, + val role: String, + val content: String, + val contexts: List? +) { + constructor(message: Message) : this(message.id, message.role, message.content, message.contexts) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatRepo.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatRepo.kt new file mode 100644 index 0000000..4557d46 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatRepo.kt @@ -0,0 +1,13 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.model + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntityRepo +import org.springframework.data.jpa.repository.Query + +interface ChatRepo : AppEntityRepo { + fun findByUserIdOrderByCreatedDateDesc(userId: String): List + + fun findBySummaryIsNull(): List + + @Query("select c from Chat c where size(c._messages) <= 1") + fun findEmptyChats(): List +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatService.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatService.kt new file mode 100644 index 0000000..888fff8 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatService.kt @@ -0,0 +1,189 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.model + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import de.niklaskerkhoff.tutorassistantappservice.lib.app_components.AppService +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.findByIdOrThrow +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages.Message +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages.MessageContext +import de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages.MessageRepo +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.jackson.* +import kotlinx.coroutines.runBlocking +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.server.ResponseStatusException +import reactor.core.publisher.Flux +import java.util.* + +@Suppress("BlockingMethodInNonBlockingContext") +@Service +class ChatService( + private val messageRepo: MessageRepo, + private val chatRepo: ChatRepo, + private val webClient: WebClient, + private val objectMapper: ObjectMapper, +) : AppService() { + private companion object { + const val EVENT_STREAM_END = "=====END=====" + const val MESSAGE_END = "=====MESSAGE_END=====" + const val CONTEXT_TOKEN_START = "##########context:" + } + + @Value("\${app.tutor-assistant.base-url}") + private lateinit var baseUrl: String + + fun getChats(userId: String) = chatRepo.findByUserIdOrderByCreatedDateDesc(userId).map { ChatBaseData(it) } + + fun getChatById(chatId: UUID, userId: String): ChatMainData { + val chat = chatRepo.findByIdOrThrow(chatId).requireUser(userId) + return ChatMainData(chat) + } + + fun createChat(userId: String): ChatBaseData { + val chat = chatRepo.save(Chat(userId)).also { + log.info("Created chat: ${it.id}") + } + return ChatBaseData(chat) + } + + fun deleteChat(chatId: UUID, userId: String) { + val chat = chatRepo.findByIdOrThrow(chatId).requireUser(userId) + chatRepo.delete(chat).also { + log.info("Deleted chat: $chatId") + } + } + + @Transactional + fun sendMessage(chatId: UUID, message: String, userId: String): Flux { + val chat = chatRepo.findByIdOrThrow(chatId).requireUser(userId) + return handleMessageSending(chat, message) + } + + private fun handleMessageSending(chat: Chat, message: String): Flux { + val userMessage = Message("user", message) + chat.addMessage(userMessage) + + /*mono(Dispatchers.IO) { + messageRepo.save(userMessage) + chatRepo.save(chat) + }*/ + + messageRepo.save(userMessage).also { + log.info("Created user-message: ${it.id} for chat: ${chat.id}") + } + + chatRepo.save(chat).also { + log.info("Updated chat: ${it.id} with user-message: ${userMessage.id}") + } + + return loadAiMessage(chat, message) + .concatWith(Flux.just(MESSAGE_END)) + .concatWith(loadChatSummary(chat)) + .concatWith(Flux.just(EVENT_STREAM_END)) + } + + private fun loadAiMessage(chat: Chat, message: String): Flux { + val dataToPost = mapOf( + "message" to message, + "history" to chat.messages.map { + mapOf( + "role" to it.role, + "content" to it.content, + ) + } + ) + + val contexts = mutableListOf() + var answer = "" + + return webClient + .post() + .uri("$baseUrl/chats/message") + .bodyValue(dataToPost) + .retrieve() + .bodyToFlux(String::class.java) + .map { + val unquotedToken = it.drop(1).dropLast(1) + + if (unquotedToken.startsWith(CONTEXT_TOKEN_START)) { + val cleanedToken = unquotedToken.drop(CONTEXT_TOKEN_START.length) + contexts.add(getContextFromJson(cleanedToken)) + "" + } else { + val cleanedToken = unquotedToken.replace("\\n", "\n") + answer += cleanedToken + "\"$cleanedToken\"" + } + + } + .doOnSubscribe { + log.info("Started loading ai-message in chat: ${chat.id}") + } + .doOnComplete { + val aiMessage = Message("ai", answer, contexts) + chat.addMessage(aiMessage) + messageRepo.save(aiMessage).also { + log.info("Created aiMessage: ${it.id} for chat: ${chat.id}") + } + chatRepo.save(chat).also { + log.info("Updated chat: ${it.id} with ai-message: ${aiMessage.id}") + } + } + } + + private fun loadChatSummary(chat: Chat): Flux { + return Flux.defer { + log.info("Started loading chat summary in chat: ${chat.id}") + runBlocking { + val client = HttpClient { + install(ContentNegotiation) { + jackson { + registerModule(JavaTimeModule()) + } + } + } + + val response = client.use { + client.post("$baseUrl/chats/summarize") { + contentType(ContentType.Application.Json) + setBody(mapOf("history" to chat.messages)) + } + } + + chat.summary = response.body() + chatRepo.save(chat).also { + log.info("Updated chat: ${it.id} with summary") + } + } + Flux.empty() + } + } + + private fun getContextFromJson(json: String): MessageContext { + val root = objectMapper.readTree(json) + return MessageContext( + root.getOrNull("kwargs").getOrNull("metadata").getOrNull("source")?.asText(), + root.getOrNull("kwargs").getOrNull("metadata").getOrNull("page")?.asInt(), + root.getOrNull("kwargs").getOrNull("page_content")?.asText(), + root.getOrNull("kwargs").getOrNull("metadata").getOrNull("originalKey")?.asText() + ) + } + + private fun JsonNode?.getOrNull(key: String): JsonNode? { + return if (this?.has(key) == true) this[key] else null + } + + private fun Chat.requireUser(userId: String): Chat { + if (this.userId != userId) throw ResponseStatusException(HttpStatus.NOT_FOUND) + return this + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatSummary.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatSummary.kt new file mode 100644 index 0000000..61da65b --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatSummary.kt @@ -0,0 +1,13 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.model + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + + +@Embeddable +data class ChatSummary( + val title: String, + val subtitle: String, + @Column(columnDefinition = "text") + val content: String, +) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/Message.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/Message.kt new file mode 100644 index 0000000..ae4f6e9 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/Message.kt @@ -0,0 +1,16 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntity +import jakarta.persistence.Column +import jakarta.persistence.ElementCollection +import jakarta.persistence.Entity + +@Entity +class Message( + val role: String, + @Column(columnDefinition = "text") + val content: String, + @ElementCollection + val contexts: List? = null, + var feedback: MessageFeedback = MessageFeedback(0, "") +) : AppEntity() diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageContext.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageContext.kt new file mode 100644 index 0000000..22e1f9d --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageContext.kt @@ -0,0 +1,13 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +data class MessageContext( + val source: String?, + val page: Int?, + @Column(columnDefinition = "text") + val content: String?, + val originalKey: String? +) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageFeedback.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageFeedback.kt new file mode 100644 index 0000000..da2c42d --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageFeedback.kt @@ -0,0 +1,11 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +data class MessageFeedback( + val rating: Int, + @Column(name = "feedback_content", columnDefinition = "text") + val content: String +) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageRepo.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageRepo.kt new file mode 100644 index 0000000..88d3c63 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageRepo.kt @@ -0,0 +1,5 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.chat.model.messages + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntityRepo + +interface MessageRepo : AppEntityRepo diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/TutorAssistantDocumentService.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/TutorAssistantDocumentService.kt new file mode 100644 index 0000000..fb5eb81 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/TutorAssistantDocumentService.kt @@ -0,0 +1,46 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents + +import de.niklaskerkhoff.tutorassistantappservice.lib.app_components.AppService +import de.niklaskerkhoff.tutorassistantappservice.lib.webclient.EmptyResponseBodyException +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.ParameterizedTypeReference +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient + +@Service +class TutorAssistantDocumentService( + private val webClient: WebClient +) : AppService() { + @Value("\${app.tutor-assistant.base-url}") + private lateinit var baseUrl: String + + fun addDocument( + title: String, + originalKey: String, + loaderType: String, + loaderParams: Map + ): List { + val requestBody = mapOf( + "title" to title, + "originalKey" to originalKey, + "loaderType" to loaderType, + "loaderParams" to loaderParams, + ) + + return webClient.post() + .uri("$baseUrl/documents/add") + .bodyValue(requestBody) + .retrieve() + .bodyToMono(object : ParameterizedTypeReference>() {}) + .block() ?: throw EmptyResponseBodyException() + } + + fun deleteDocument(tutorAssistantIds: List): Boolean { + return webClient.post() + .uri("$baseUrl/documents/delete") + .bodyValue(tutorAssistantIds) + .retrieve() + .bodyToMono(Boolean::class.java) + .block() ?: throw EmptyResponseBodyException() + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplicationController.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplicationController.kt new file mode 100644 index 0000000..b81dabc --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplicationController.kt @@ -0,0 +1,32 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications + +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities.toDto +import org.springframework.web.bind.annotation.* +import java.util.* + +@RestController +@RequestMapping("documents/applications") +class ApplicationController( + private val applicationService: ApplicationService +) { + @GetMapping("files") + fun getFileDocuments() = applicationService.getFileDocuments().map { it.toDto() } + + @GetMapping("websites") + fun getWebsiteDocuments() = applicationService.getWebsiteDocuments().map { it.toDto() } + + @PostMapping("index") + fun index(): Unit = applicationService.index() + + @PostMapping("files/{id}/reindex") + fun reindexFile(@PathVariable id: UUID): Unit = applicationService.reindexFileDocument(id) + + @PostMapping("websites/{id}/reindex") + fun reindexWebsite(@PathVariable id: UUID): Unit = applicationService.reindexWebsiteDocument(id) + + @DeleteMapping("files/{id}") + fun deleteFile(@PathVariable id: UUID): Unit = applicationService.deleteFileDocument(id) + + @DeleteMapping("websites/{id}") + fun deleteWebsite(@PathVariable id: UUID): Unit = applicationService.deleteWebsiteDocument(id) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplicationDocumentLoader.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplicationDocumentLoader.kt new file mode 100644 index 0000000..3f484e5 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplicationDocumentLoader.kt @@ -0,0 +1,7 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications + +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities.Document + +interface ApplicationDocumentLoader { + fun loadDocuments(): List +} \ No newline at end of file diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplicationService.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplicationService.kt new file mode 100644 index 0000000..566cc51 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplicationService.kt @@ -0,0 +1,61 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications + +import de.niklaskerkhoff.tutorassistantappservice.lib.app_components.AppService +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.findByIdOrThrow +import de.niklaskerkhoff.tutorassistantappservice.lib.exceptions.BadRequestException +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.TutorAssistantDocumentService +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities.* +import org.springframework.stereotype.Service +import java.util.* + +@Service +class ApplicationService( + private val fileDocumentRepo: FileDocumentRepo, + private val websiteDocumentRepo: WebsiteDocumentRepo, + private val applicationDocumentLoader: ApplicationDocumentLoader, + private val applierVisitor: ApplierVisitor, + private val tutorAssistantDocumentService: TutorAssistantDocumentService +) : AppService() { + + fun getFileDocuments(): List = fileDocumentRepo.findAll() + + fun getWebsiteDocuments(): List = websiteDocumentRepo.findAll() + + fun index() { + val documents = applicationDocumentLoader.loadDocuments() + documents.forEach { it.accept(applierVisitor) } + } + + fun reindexFileDocument(id: UUID) = reindex(id, fileDocumentRepo) + + fun reindexWebsiteDocument(id: UUID) = reindex(id, websiteDocumentRepo) + + fun deleteFileDocument(id: UUID) = delete(id, fileDocumentRepo) + + fun deleteWebsiteDocument(id: UUID) = delete(id, websiteDocumentRepo) + + private fun reindex(id: UUID, documentRepo: DocumentRepo) { + val existingDocument = documentRepo.findByIdOrThrow(id) + val title = existingDocument.title + val allDocuments = applicationDocumentLoader.loadDocuments() + val documentToReindex = allDocuments.find { it.title == title } + ?: throw BadRequestException("Document $title not specified in main settings") + + delete(existingDocument, documentRepo) + documentToReindex.accept(applierVisitor) + } + + private fun delete(id: UUID, documentRepo: DocumentRepo) { + val document = documentRepo.findByIdOrThrow(id) + delete(document, documentRepo) + } + + private fun delete(document: T, documentRepo: DocumentRepo) { + tutorAssistantDocumentService.deleteDocument(document.tutorAssistantIds).also { + log.info("Deleted ${document.tutorAssistantIds} from Tutor-Assistant") + } + documentRepo.delete(document).also { + log.info("Deleted document with id ${document.id}") + } + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplierVisitor.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplierVisitor.kt new file mode 100644 index 0000000..7c8b579 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/ApplierVisitor.kt @@ -0,0 +1,92 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications + +import de.niklaskerkhoff.tutorassistantappservice.lib.logging.Logger +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.TutorAssistantDocumentService +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities.* +import org.springframework.stereotype.Component + +@Component +class ApplierVisitor( + private val fileDocumentRepo: FileDocumentRepo, + private val websiteDocumentRepo: WebsiteDocumentRepo, + private val tutorAssistantDocumentService: TutorAssistantDocumentService +) : DocumentVisitor, Logger { + + override fun visit(fileDocument: FileDocument) { + log.info("Visiting fileDocument with title ${fileDocument.title}") + + val existing = fileDocumentRepo.findByTitle(fileDocument.title) + if (existing != null) { + logStopping(fileDocument.title) + return + } + + logContinuing(fileDocument.title) + + val loaderParams = mapOf("url" to fileDocument.fileStoreUrl) + + val tutorAssistantIds = tutorAssistantDocumentService.addDocument( + fileDocument.title, + fileDocument.fileStoreId.toString(), + fileDocument.loaderType, + loaderParams + ).also { + logAddedToTutorAssistant(fileDocument.title, it) + } + + fileDocument.tutorAssistantIds = tutorAssistantIds + + fileDocumentRepo.save(fileDocument).also { + logSaved(it.title) + } + } + + override fun visit(websiteDocument: WebsiteDocument) { + log.info("Visiting websiteDocument with title ${websiteDocument.title}") + + val existing = websiteDocumentRepo.findByTitle(websiteDocument.title) + if (existing != null) { + logStopping(websiteDocument.title) + return + } + + logContinuing(websiteDocument.title) + + val loaderParams = mapOf( + "url" to websiteDocument.loaderParams.url, + "htmlSelector" to websiteDocument.loaderParams.htmlSelector, + "htmlSelectionIndex" to websiteDocument.loaderParams.htmlSelectionIndex, + ) + + val tutorAssistantIds = tutorAssistantDocumentService.addDocument( + websiteDocument.title, + websiteDocument.loaderParams.url, + websiteDocument.loaderType, + loaderParams + ).also { + logAddedToTutorAssistant(websiteDocument.title, it) + } + + websiteDocument.tutorAssistantIds = tutorAssistantIds + + websiteDocumentRepo.save(websiteDocument).also { + logSaved(websiteDocument.title) + } + } + + private fun logContinuing(title: String) { + log.info("$title does not exist, continuing") + } + + private fun logStopping(title: String) { + log.info("$title already exists, stopping") + } + + private fun logAddedToTutorAssistant(title: String, tutorAssistantIds: List) { + log.info("Added $title to Tutor-Assistant, got ${tutorAssistantIds.size} ids") + } + + private fun logSaved(title: String) { + log.info("Saved $title") + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/Document.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/Document.kt new file mode 100644 index 0000000..a3c4d0d --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/Document.kt @@ -0,0 +1,20 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntity +import jakarta.persistence.ElementCollection +import jakarta.persistence.Entity +import jakarta.persistence.Inheritance +import jakarta.persistence.InheritanceType + +@Entity +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +abstract class Document( + open val title: String, + open val loaderType: String, + open val collection: String? +) : AppEntity() { + @ElementCollection + open var tutorAssistantIds: List = emptyList() + + abstract fun accept(visitor: DocumentVisitor) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/DocumentRepo.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/DocumentRepo.kt new file mode 100644 index 0000000..9bd5307 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/DocumentRepo.kt @@ -0,0 +1,9 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntityRepo +import org.springframework.data.repository.NoRepositoryBean + +@NoRepositoryBean +interface DocumentRepo : AppEntityRepo { + fun findByTitle(title: String): T? +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/DocumentVisitor.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/DocumentVisitor.kt new file mode 100644 index 0000000..85f1cb9 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/DocumentVisitor.kt @@ -0,0 +1,6 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities + +interface DocumentVisitor { + fun visit(fileDocument: FileDocument) + fun visit(websiteDocument: WebsiteDocument) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocument.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocument.kt new file mode 100644 index 0000000..b6bf659 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocument.kt @@ -0,0 +1,17 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities + +import jakarta.persistence.Entity +import java.util.UUID + +@Entity +class FileDocument( + title: String, + loaderType: String, + collection: String?, + val fileStoreId: UUID, + val fileStoreUrl: String, +) : Document(title, loaderType, collection) { + override fun accept(visitor: DocumentVisitor) { + visitor.visit(this) + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentDtos.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentDtos.kt new file mode 100644 index 0000000..43aa96b --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentDtos.kt @@ -0,0 +1,19 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities + +import java.util.UUID + +data class FileDocumentDto( + val id: UUID?, + val title: String, + val loaderType: String, + val collection: String?, +) { + constructor(fileDocument: FileDocument) : this( + id = fileDocument.id, + title = fileDocument.title, + loaderType = fileDocument.loaderType, + collection = fileDocument.collection + ) +} + +fun FileDocument.toDto() = FileDocumentDto(this) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentRepo.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentRepo.kt new file mode 100644 index 0000000..829b8f2 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentRepo.kt @@ -0,0 +1,3 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities + +interface FileDocumentRepo : DocumentRepo diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/WebsiteDocument.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/WebsiteDocument.kt new file mode 100644 index 0000000..549a5be --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/WebsiteDocument.kt @@ -0,0 +1,23 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities + +import jakarta.persistence.Embeddable +import jakarta.persistence.Entity + +@Entity +class WebsiteDocument( + title: String, + loaderType: String, + collection: String?, + val loaderParams: LoaderParams, +) : Document(title, loaderType, collection) { + override fun accept(visitor: DocumentVisitor) { + visitor.visit(this) + } + + @Embeddable + data class LoaderParams( + val url: String, + val htmlSelector: String, + val htmlSelectionIndex: Int + ) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/WebsiteDocumentDtos.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/WebsiteDocumentDtos.kt new file mode 100644 index 0000000..7c02927 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/WebsiteDocumentDtos.kt @@ -0,0 +1,21 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities + +import java.util.* + +data class WebsiteDocumentDto( + val id: UUID?, + val title: String, + val loaderType: String, + val collection: String?, + val url: String, +) { + constructor(websiteDocument: WebsiteDocument) : this( + id = websiteDocument.id, + title = websiteDocument.title, + loaderType = websiteDocument.loaderType, + collection = websiteDocument.collection, + url = websiteDocument.loaderParams.url + ) +} + +fun WebsiteDocument.toDto() = WebsiteDocumentDto(this) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/WebsiteDocumentRepo.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/WebsiteDocumentRepo.kt new file mode 100644 index 0000000..5255ca6 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/WebsiteDocumentRepo.kt @@ -0,0 +1,3 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities + +interface WebsiteDocumentRepo : DocumentRepo diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/resources/ResourceController.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/resources/ResourceController.kt new file mode 100644 index 0000000..2081675 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/resources/ResourceController.kt @@ -0,0 +1,55 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.resources + +import de.niklaskerkhoff.tutorassistantappservice.lib.filestore.FileStoreFileReferenceDto +import de.niklaskerkhoff.tutorassistantappservice.lib.filestore.FileStoreService +import de.niklaskerkhoff.tutorassistantappservice.lib.filestore.toDto +import org.springframework.core.io.InputStreamResource +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile +import java.util.* + +@RestController +@RequestMapping("documents/resources") +class ResourceController( + private val resourceService: ResourceService, + private val fileStoreService: FileStoreService +) { + companion object { + private val CONTENT_TYPES = mapOf( + "pdf" to MediaType.APPLICATION_PDF, + ) + private val DEFAULT_CONTENT_TYPE = MediaType.TEXT_PLAIN + } + + @GetMapping + fun listFiles(): List = fileStoreService.listFiles().map { it.toDto() } + + @GetMapping("{id}") + fun getFile(@PathVariable id: UUID): ResponseEntity { + val fileData = fileStoreService.getFileById(id) + val fileType = fileData.first.displayName.split(".").last() + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"${fileData.first.displayName}\"") + .contentType(CONTENT_TYPES[fileType] ?: DEFAULT_CONTENT_TYPE) + .body(fileData.second) + } + + @PostMapping + @PreAuthorize("hasRole('document-manager')") + fun addFile( + @RequestPart("file") file: MultipartFile, + ): FileStoreFileReferenceDto { + return fileStoreService.assignAndUpload(file, resourceService.getUniqueFilename(file)).toDto() + } + + @DeleteMapping("{id}") + @PreAuthorize("hasRole('document-manager')") + fun deleteFile(@PathVariable id: UUID) { + fileStoreService.deleteById(id) + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/resources/ResourceService.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/resources/ResourceService.kt new file mode 100644 index 0000000..0983669 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/resources/ResourceService.kt @@ -0,0 +1,41 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.resources + +import de.niklaskerkhoff.tutorassistantappservice.lib.exceptions.BadRequestException +import de.niklaskerkhoff.tutorassistantappservice.lib.filestore.FileStoreFileReferenceDefaultRepo +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile + +@Service +class ResourceService( + private val fileReferenceDefaultRepo: FileStoreFileReferenceDefaultRepo +) { + companion object { + private const val UNIQUE_START_N = 2 + } + + fun getUniqueFilename(file: MultipartFile): String { + val filename = file.originalFilename ?: throw BadRequestException("Filename must not be null") + return getUniqueFilename(filename) + } + + private fun getUniqueFilename(filename: String, n: Int? = null): String { + val fileReferences = fileReferenceDefaultRepo.findAllByDisplayName(filename) + if (fileReferences.size > 1) throw UnknownError("Filenames must be unique") + if (fileReferences.isNotEmpty()) { + return if (n == null) getUniqueFilename(filename, UNIQUE_START_N) else getUniqueFilename(filename, n + 1) + } + + return if (n == null) filename else addNumberToFilename(filename, n) + } + + private fun addNumberToFilename(filename: String, number: Int): String { + val dotIndex = filename.lastIndexOf('.') + return if (dotIndex != -1) { + val name = filename.substring(0, dotIndex) + val extension = filename.substring(dotIndex) + "$name.$number$extension" + } else { + "$filename.$number" + } + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingController.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingController.kt new file mode 100644 index 0000000..78d7062 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingController.kt @@ -0,0 +1,23 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings + +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings.entities.SettingDto +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings.entities.toDto +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile +import java.util.* + +@RestController +@RequestMapping("documents/settings") +class SettingController( + private val settingService: SettingService +) { + + @GetMapping + fun getSettings(): List = settingService.getSettings().map { it.toDto() } + + @PostMapping + fun addSetting(@RequestPart("file") file: MultipartFile): SettingDto = settingService.addSettings(file).toDto() + + @DeleteMapping("{id}") + fun deleteSetting(@PathVariable id: UUID): Unit = settingService.deleteSetting(id) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingService.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingService.kt new file mode 100644 index 0000000..d11c3e2 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingService.kt @@ -0,0 +1,75 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings + +import com.fasterxml.jackson.databind.ObjectMapper +import de.niklaskerkhoff.tutorassistantappservice.lib.app_components.AppService +import de.niklaskerkhoff.tutorassistantappservice.lib.exceptions.BadRequestException +import de.niklaskerkhoff.tutorassistantappservice.lib.filestore.FileStoreService +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.ApplicationDocumentLoader +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities.Document +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings.entities.Setting +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings.entities.SettingRepo +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings.entities.SettingType +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import java.util.* + +@Service +class SettingService( + private val settingRepo: SettingRepo, + private val fileStoreService: FileStoreService, + private val objectMapper: ObjectMapper +) : AppService(), ApplicationDocumentLoader { + + companion object { + private val VALUE_STRATEGIES = mapOf String>( + "plain" to { it }, + "underscored" to { it.replace(" ", "_") } + ) + } + + override fun loadDocuments(): List { + val settings = settingRepo.findAll() + val (tempMainSettings, tempValueSettings) = settings.partition { it.type == SettingType.MAIN } + if (tempMainSettings.isEmpty()) throw BadRequestException("Main setting does not exist") + + val mainJson = tempMainSettings.first().content + val values = tempValueSettings.associate { it.name to it.content.trim().split('\n') } + val fileStoreIdsAndUrls = fileStoreService.listFiles().associate { it.displayName to Pair(it.id, it.storeUrl) } + + return SettingsParser(objectMapper, mainJson, values, fileStoreIdsAndUrls, VALUE_STRATEGIES).parse().also { + log.info("Parsed ${it.size} documents") + } + } + + fun getSettings(): List = settingRepo.findAll() + + @Transactional + fun addSettings(file: MultipartFile): Setting { + val name = file.originalFilename ?: throw BadRequestException("File name must not be null") + val fileEnding = name.split(".").last() + val value = file.inputStream.bufferedReader().use { it.readText() } + val type = if (fileEnding == "json") SettingType.MAIN else SettingType.VALUES + + settingRepo.deleteAllByName(name).also { + log.info("Deleted existing settings with name '$name'") + } + + if (type == SettingType.MAIN) { + settingRepo.deleteAllByType(type).also { + log.info("Deleted main setting if existed") + } + } + + val setting = Setting(name, value, type) + return settingRepo.save(setting).also { + log.info("Saved new setting with name '$name'") + } + } + + fun deleteSetting(id: UUID) { + settingRepo.deleteById(id).also { + log.info("Deleted setting with id $id") + } + } +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingsExceptions.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingsExceptions.kt new file mode 100644 index 0000000..52e22ea --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingsExceptions.kt @@ -0,0 +1,3 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings + +class SettingsParserException(message: String) : Exception(message) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingsParser.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingsParser.kt new file mode 100644 index 0000000..b353f64 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/SettingsParser.kt @@ -0,0 +1,171 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities.Document +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities.FileDocument +import de.niklaskerkhoff.tutorassistantappservice.modules.documents.applications.entities.WebsiteDocument +import java.util.* + +class SettingsParser( + private val objectMapper: ObjectMapper, + private val mainJson: String, + private val allValues: Map>, + private val fileStoreIdsAndUrls: Map>, + private val valueStrategies: Map String> +) { + companion object { + private const val WEBSITE_TYPE = "Website" + private const val FILE_TYPE = "File" + private val VALUE_REGEX = "\\$\\{(\\w+)}".toRegex() + } + + fun parse(): List { + val json = objectMapper.readTree(mainJson) + + return parseRoot(json) + } + + private fun parseRoot(json: JsonNode): List { + json.requireArray() + + return json.elements().asSequence().map { parseCollectionOrDocument(it) }.flatten().toList() + } + + private fun parseCollectionOrDocument(json: JsonNode): List { + json.requireObject() + + return when { + json.has("title") -> listOf(parseDocument(json, null, null)) + json.has("collection") -> parseCollection(json) + else -> throw SettingsParserException("Failed parsing collection or document") + } + } + + private fun parseCollection(json: JsonNode): List { + json.requireObjectKeys("collection") + + val collection = json["collection"].stringOrThrow() + + return when { + json.has("elements") -> parseElements(json["elements"], collection) + json.has("elementsBuilder") -> parseElementsBuilder(json["elementsBuilder"], collection, getValues(json)) + else -> throw SettingsParserException("Failed parsing collection") + } + } + + private fun parseElements(json: JsonNode, collection: String?): List { + json.requireArray() + + return json.elements().asSequence().map { parseDocument(it, collection, null) }.toList() + } + + private fun parseElementsBuilder( + json: JsonNode, + collection: String?, + values: List + ): List { + json.requireObject() + + return values.map { parseDocument(json, collection, it) } + } + + private fun parseDocument(json: JsonNode, collection: String?, value: String?): Document { + json.requireObjectKeys("type") + + val type = json["type"].stringOrThrow() + + return when (type) { + WEBSITE_TYPE -> parseWebsite(json, collection, value) + FILE_TYPE -> parseFile(json, collection, value) + else -> throw SettingsParserException("Failed parsing document") + } + } + + private fun parseFile(json: JsonNode, collection: String?, value: String?): Document { + json.requireObjectKeys("title", "loaderType", "filename") + + val (fileStoreId, fileStoreUrl) = json.getUrlFromFilename(value) + + return FileDocument( + json["title"].stringWithValueOrThrow(value), + json["loaderType"].stringWithValueOrThrow(value), + collection, + fileStoreId, + fileStoreUrl + ) + } + + private fun parseWebsite(json: JsonNode, collection: String?, value: String?): Document { + json.requireObjectKeys("title", "loaderType", "loaderParams") + + return WebsiteDocument( + json["title"].stringWithValueOrThrow(value), + json["loaderType"].stringWithValueOrThrow(value), + collection, + parseWebsiteLoaderParams(json["loaderParams"], value) + ) + } + + private fun parseWebsiteLoaderParams(json: JsonNode, value: String?): WebsiteDocument.LoaderParams { + json.requireObjectKeys("url", "htmlSelector", "htmlSelectionIndex") + + return WebsiteDocument.LoaderParams( + json["url"].stringWithValueOrThrow(value), + json["htmlSelector"].stringWithValueOrThrow(value), + json["htmlSelectionIndex"].intOrThrow() + ) + } + + private fun JsonNode.requireObject() { + if (!isObject) throw SettingsParserException("Not an object") + } + + private fun JsonNode.requireArray() { + if (!isArray) throw SettingsParserException("Not an array") + } + + private fun JsonNode.requireObjectKeys(vararg keys: String) { + keys.forEach { if (!has(it)) throw SettingsParserException("Key $it not found") } + } + + private fun JsonNode.stringOrThrow(): String { + if (!isTextual) throw SettingsParserException("Not a string") + return asText() + } + + private fun JsonNode.intOrThrow(): Int { + if (!isInt) throw SettingsParserException("Not an int") + return asInt() + } + + private fun getValues(json: JsonNode): List { + json.requireObjectKeys("values") + + val key = json["values"].stringOrThrow() + + return allValues[key] ?: throw SettingsParserException("Values for key $key not found") + } + + private fun JsonNode.getUrlFromFilename(value: String?): Pair { + val filename = this["filename"].stringWithValueOrThrow(value) + val idAndUrl = fileStoreIdsAndUrls[filename] ?: throw SettingsParserException("File $filename does not exist") + val id = idAndUrl.first ?: throw SettingsParserException("File store id must not be null") + return Pair(id, idAndUrl.second) + } + + private fun JsonNode.stringWithValueOrThrow(value: String?): String { + val string = stringOrThrow() + if (value == null) return string + + return VALUE_REGEX.replace(string) { matchResult -> + val strategyName = matchResult.groups[1]?.value + ?: throw SettingsParserException("Unknown error reading strategy name") + val strategy = valueStrategies[strategyName] + ?: throw SettingsParserException("Value strategy $strategyName does not exist") + + strategy(value) + } + } +} + diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/Setting.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/Setting.kt new file mode 100644 index 0000000..be5e37e --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/Setting.kt @@ -0,0 +1,13 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings.entities + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity + +@Entity +class Setting( + val name: String, + @Column(columnDefinition = "text") + val content: String, + val type: SettingType +) : AppEntity() diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/SettingDtos.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/SettingDtos.kt new file mode 100644 index 0000000..3df8324 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/SettingDtos.kt @@ -0,0 +1,19 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings.entities + +import java.util.* + +data class SettingDto( + val id: UUID?, + val name: String, + val content: String, + val type: SettingType, +) { + constructor(setting: Setting) : this( + id = setting.id, + name = setting.name, + content = setting.content, + type = setting.type + ) +} + +fun Setting.toDto() = SettingDto(this) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/SettingRepo.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/SettingRepo.kt new file mode 100644 index 0000000..2913b61 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/SettingRepo.kt @@ -0,0 +1,9 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings.entities + +import de.niklaskerkhoff.tutorassistantappservice.lib.entities.AppEntityRepo + +interface SettingRepo : AppEntityRepo { + fun deleteAllByType(type: SettingType) + + fun deleteAllByName(name: String) +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/SettingType.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/SettingType.kt new file mode 100644 index 0000000..8958647 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/settings/entities/SettingType.kt @@ -0,0 +1,5 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.documents.settings.entities + +enum class SettingType { + MAIN, VALUES +} diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/helper/HelperController.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/helper/HelperController.kt new file mode 100644 index 0000000..e7c7cf8 --- /dev/null +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/helper/HelperController.kt @@ -0,0 +1,56 @@ +package de.niklaskerkhoff.tutorassistantappservice.modules.helper + +import de.niklaskerkhoff.tutorassistantappservice.modules.calendar.entities.CalendarRepo +import org.apache.commons.text.similarity.LevenshteinDistance +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("helper") +@PreAuthorize("hasRole('document-manager')") +class HelperController( + val calendarRepo: CalendarRepo +) { + + @GetMapping("info-leven") + fun getLeven() { + /*val calendars = calendarRepo.findAll().sortedBy { it.createdDate }.subList(20, 30) + val permutations = calendars.map { calendar1 -> calendars.map { calendar2 -> Pair(calendar1, calendar2) } } + + val sum = permutations.sumOf { row -> + row.sumOf { normalizedLevenshtein(it.first.entries.toString(), it.second.entries.toString()) } + } + + println("---------------------------------------------------") + + println(sum / 100.0) + + println("---------------------------------------------------") + permutations.forEach { row -> + println( + row.joinToString(" ") { + String.format( + "%.5f", + normalizedLevenshtein(it.first.entries.toString(), it.second.entries.toString()) + ) + } + ) + } + println("---------------------------------------------------")*/ + + } + + + private fun normalizedLevenshtein(str1: String, str2: String): Double { + val levenshteinDistance = LevenshteinDistance().apply(str1, str2) + val maxLength = maxOf(str1.length, str2.length) + + return if (maxLength == 0) { + 0.0 + } else { + levenshteinDistance.toDouble() / maxLength + } + } +} diff --git a/tutor-assistant-app-service/src/main/resources/application-prod.yml b/tutor-assistant-app-service/src/main/resources/application-prod.yml new file mode 100644 index 0000000..e69de29 diff --git a/tutor-assistant-app-service/src/main/resources/application.yml b/tutor-assistant-app-service/src/main/resources/application.yml new file mode 100644 index 0000000..fd595eb --- /dev/null +++ b/tutor-assistant-app-service/src/main/resources/application.yml @@ -0,0 +1,40 @@ +server: + servlet: + context-path: /api + +spring: + application: + name: tutor-assistant-app-service + + datasource: + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + url: jdbc:postgresql://${SPRING_DATASOURCE_HOST}/tutor-assistant-app-service + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + show-sql: false + + security: + oauth2: + resourceserver: + jwt: + issuer-uri: ${KEYCLOAK_ISSUER_URI} + +logging: + file: + name: ${LOGS_PATH} + logback: + rollingpolicy: + max-file-size: 10MB + max-history: 50 + + +app: + tutor-assistant: + base-url: ${TUTOR_ASSISTANT_BASE_URL} + seaweedfs: + master-url: ${SEAWEEDFS_MASTER_URL} diff --git a/tutor-assistant-app-service/src/main/resources/banner.txt b/tutor-assistant-app-service/src/main/resources/banner.txt new file mode 100644 index 0000000..4e382cd --- /dev/null +++ b/tutor-assistant-app-service/src/main/resources/banner.txt @@ -0,0 +1,26 @@ + _____ _____ _____ _____ + /\ \ /\ \ /\ \ /\ \ + /::\ \ /::\ \ /::\ \ /::\ \ + \:::\ \ /::::\ \ /::::\ \ /::::\ \ + \:::\ \ /::::::\ \ /::::::\ \ /::::::\ \ + \:::\ \ /:::/\:::\ \ /:::/\:::\ \ /:::/\:::\ \ + \:::\ \ /:::/__\:::\ \ /:::/__\:::\ \ /:::/__\:::\ \ + /::::\ \ /::::\ \:::\ \ /::::\ \:::\ \ \:::\ \:::\ \ + /::::::\ \ /::::::\ \:::\ \ /::::::\ \:::\ \ ___\:::\ \:::\ \ + /:::/\:::\ \ /:::/\:::\ \:::\ \ /:::/\:::\ \:::\ \ /\ \:::\ \:::\ \ + /:::/ \:::\____\/:::/ \:::\ \:::\____\/:::/ \:::\ \:::\____\/::\ \:::\ \:::\____\ + /:::/ \::/ /\::/ \:::\ /:::/ /\::/ \:::\ /:::/ /\:::\ \:::\ \::/ / + /:::/ / \/____/ \/____/ \:::\/:::/ / \/____/ \:::\/:::/ / \:::\ \:::\ \/____/ + /:::/ / \::::::/ / \::::::/ / \:::\ \:::\ \ +/:::/ / \::::/ / \::::/ / \:::\ \:::\____\ +\::/ / /:::/ / /:::/ / \:::\ /:::/ / + \/____/ /:::/ / /:::/ / \:::\/:::/ / + /:::/ / /:::/ / \::::::/ / + /:::/ / /:::/ / \::::/ / + \::/ / \::/ / \::/ / + \/____/ \/____/ \/____/ + +Tutor Assistant App Service + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} diff --git a/tutor-assistant-app-service/src/test/kotlin/de/niklaskerkhoff/tutorassistantappservice/TutorAssistantAppServiceApplicationTests.kt b/tutor-assistant-app-service/src/test/kotlin/de/niklaskerkhoff/tutorassistantappservice/TutorAssistantAppServiceApplicationTests.kt new file mode 100644 index 0000000..6ccfadf --- /dev/null +++ b/tutor-assistant-app-service/src/test/kotlin/de/niklaskerkhoff/tutorassistantappservice/TutorAssistantAppServiceApplicationTests.kt @@ -0,0 +1,13 @@ +package de.niklaskerkhoff.tutorassistantappservice + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class TutorAssistantAppServiceApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/tutor-assistant-app-service/src/test/resources/application-test.yml b/tutor-assistant-app-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..e69de29 diff --git a/tutor-assistant/Dockerfile b/tutor-assistant/Dockerfile new file mode 100644 index 0000000..9333de0 --- /dev/null +++ b/tutor-assistant/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . +EXPOSE 8500 + +CMD ["python", "main.py"] diff --git a/tutor-assistant/main.py b/tutor-assistant/main.py new file mode 100644 index 0000000..53e3805 --- /dev/null +++ b/tutor-assistant/main.py @@ -0,0 +1,29 @@ +import os + +import uvicorn +from fastapi import FastAPI + +from tutor_assistant.controller.api import chats_controller, documents_controller, calendar_controller, demo_controller +from tutor_assistant.controller.config.domain_config import config + +_app = FastAPI( + title='Tutor Assistant', + version='1.0.0', + description='' +) + + +def _main(): + config.vector_store_manager.create_if_not_exists() + + # app.logger = config.logger + _app.include_router(calendar_controller.router, tags=["calendar"]) + _app.include_router(chats_controller.router, tags=["chats"]) + _app.include_router(demo_controller.router, tags=["demo"]) + _app.include_router(documents_controller.router, tags=["documents"]) + + uvicorn.run(_app, host=os.getenv('HOST'), port=8500) + + +if __name__ == '__main__': + _main() diff --git a/tutor-assistant/requirements.txt b/tutor-assistant/requirements.txt new file mode 100644 index 0000000..fa6e997 --- /dev/null +++ b/tutor-assistant/requirements.txt @@ -0,0 +1,16 @@ +beautifulsoup4 +faiss-cpu +fastapi +langchain +langchain-chroma +langchain-community +langchain-core +langchain-ollama +langchain-openai +langchain-text-splitters +lark +numpy +pypdf +requests +starlette +uvicorn diff --git a/tutor-assistant/resources/prompt_templates/base_template.txt b/tutor-assistant/resources/prompt_templates/base_template.txt new file mode 100644 index 0000000..ad5b1cd --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/base_template.txt @@ -0,0 +1,10 @@ +Du bist ein hilfreicher Assistent. + +Hier ein paar allgemeine Informationen, die zum Beantworten der Fragen des Benutzers relevant sein könnten: +- Wir haben heute folgendes Datum: 06.11.2024 +- Die Benutzer sind studentische Tutoren für eine Programmieren-Vorlesung. Sie bringen anderen Studenten programmieren bei. +- Es geht um die Programmiersprache Java. +- Wenn nicht anders gefordert, schreibe angefragten Code in Java. Wenn nicht anders gefragt, schreibe deine Antworten auf {language}. +- Schreibe Code immer auf Englisch. Kommentare im Code dürfen auf der Standardsprache sein. +- Wenn du den Eindruck hast, dass eine Frage völlig am Thema vorbeigeht, beantworte sie dennoch so gut es geht, weise aber darauf hin, dass du dafür nicht entwickelt wurdest. +- Sprich den Benutzer mit "Du" an diff --git a/tutor-assistant/resources/prompt_templates/calendar_chat_model_improvement.txt b/tutor-assistant/resources/prompt_templates/calendar_chat_model_improvement.txt new file mode 100644 index 0000000..d0ddc5e --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/calendar_chat_model_improvement.txt @@ -0,0 +1,7 @@ +Ich habe folgende Anfrage an dich gesendet: +{prompt} + +Du hast folgendes geantwortet: +{first_answer} + +Sag mir, warum die Antwort schlecht war, gib die Antwort nochmal unverändert zurück und dann noch eine verbesserte Antwort. diff --git a/tutor-assistant/resources/prompt_templates/calendar_chat_model_json.txt b/tutor-assistant/resources/prompt_templates/calendar_chat_model_json.txt new file mode 100644 index 0000000..a024ae6 --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/calendar_chat_model_json.txt @@ -0,0 +1,42 @@ +Ich möchte, dass du alle Termine und Fristen im folgenden Kontext als JSON darstellst. +Verwende die keys `title`, `date`, `time`. Wenn du keine Uhrzeit findest, lasse `time` einfach weg. +Verwende keine Zeitspannen, sondern lege für Start und Ende separate Ereignisse an. Mache deutlich, welches der Start und welches das Ende ist. +Zeige alle Ereignisse seit 01.01.2023 + +Beispielausgabe: +{{ + "entries": [ + {{ + "title": "Vorstellungsgespräch", + "date": "22.10.2024", + "time": "12:00" + }}, + {{ + "title": "Start Aufgaben 1", + "date": "28.10.2024", + }}, + {{ + "title": "Beginn Abgabe der Aufgaben", + "date": "06.11.2024", + "time": "13:45" + }}, + {{ + "title": "Sport", + "date": "06.11.2024", + "time": "13:45" + }}, + {{ + "title": "Ende Abgabe der Aufgaben", + "date": "02.01.2024", + }}, + ] +}} + +Falls keine Ereignisse gefunden wurden, gib einfach folgendes aus: +{{ + "entries": [ + ] +}} + +Der Kontext: +{context} diff --git a/tutor-assistant/resources/prompt_templates/calendar_chat_model_markdown.txt b/tutor-assistant/resources/prompt_templates/calendar_chat_model_markdown.txt new file mode 100644 index 0000000..fd7afb0 --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/calendar_chat_model_markdown.txt @@ -0,0 +1,24 @@ +Erstelle eine Tabelle in Markdown-Format, die alle Termine und Fristen des folgenden Kontexts auflistet. + +- Die Tabelle soll zwei Spalten haben: "Datum" und "Ereignis". +- Füge jedes Ereignis in einer separaten Zeile ein, und falls mehrere Ereignisse an einem Datum stattfinden, mache für jedes eine eigene Zeile. +- Sortiere die Zeilen nach dem Datum. +- Verwende keine Zeitspannen, sondern lege gegebenenfalls für Start, Ende, ... eines Ereignisses mehrere Zeilen an. Achte dabei besonders auf die Sortierung nach Datum. +- Behandle den heutigen Tag als Ereignis: Setze in der Zeile das Wort "Heute" in die Ereignis-Spalte. Achte dabei besonders auf die Sortierung nach Datum. +- Mache alle Zeilen des heutigen Tages fett. +- Zeige Ereignisse vor und nach dem heutigen Datum. +- Gib ausschließlich die Tabelle in Markdown aus, ohne weitere Kommentare oder Überschriften. + +Die Tabelle soll beispielsweise so aussehen: + +| Datum | Ereignis | +|---------------------|---------------------------------------------| +| Mi, 22.10. | Vorstellungsgespräch | +| Mo, 28.10. | Start Aufgaben 1 | +| **Mi, 06.11.** | **Heute** | +| **Mi, 06.11.** | **Beginn Abgabe der Aufgaben um 12:00 Uhr** | +| **Mi, 06.11.** | **Sport** | +| Fr, 02.01. | Ende Abgabe der Aufgaben um 12:00 Uhr | + +Kontext: +{context} diff --git a/tutor-assistant/resources/prompt_templates/calendar_vector_store.txt b/tutor-assistant/resources/prompt_templates/calendar_vector_store.txt new file mode 100644 index 0000000..f74b51d --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/calendar_vector_store.txt @@ -0,0 +1 @@ +Wichtige Termine und Fristen mit Zeit und Ort: Übungsblätter, Prüfungen, Veranstaltungen diff --git a/tutor-assistant/resources/prompt_templates/chat_message.txt b/tutor-assistant/resources/prompt_templates/chat_message.txt new file mode 100644 index 0000000..ff7b8f9 --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/chat_message.txt @@ -0,0 +1,10 @@ +Wenn du dir bei einer Antwort unsicher bist, teile sie mit, aber schreibe dazu, dass du dir unsicher bist. +Wenn du eine Antwort gar nicht weißt, teile dies dem Benutzer mit statt irgendetwas Falsches zu antworten. + +Verwende folgenden Kontext und die bisherige Konversation, um eine Antwort zu generieren, die dem Benutzer bestmöglich hilft. + +Bevor du deine Antwort gibst, erkläre genau, warum du diese Antwort gibst! +Nutze Markdown, um deine Antworten übersichtlich zu gestalten. + +Der Kontext: +{context} diff --git a/tutor-assistant/resources/prompt_templates/chat_summary.txt b/tutor-assistant/resources/prompt_templates/chat_summary.txt new file mode 100644 index 0000000..9e67f72 --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/chat_summary.txt @@ -0,0 +1,10 @@ +Fasse den Chat zusammen. Gib ihm einen kurzen prägnanten Titel, einen Untertitel, der das Anliegen des Benutzers ausdrückt und eine Zusammenfassung der Ergebnisse. +Es soll nicht im Stil sein "Der Benutzer möchte dies und jenes" sondern für einen allgemeinen Benutzer formuliert sein. + +Die Zusammenfassung soll nur 1 bis 2 Sätze sein oder stichpunktartig sein, wenn sinnvoll als Markdown-Stichpunkte. +Die Zusammenfassung soll die Ergebnisse wiederspiegeln und nicht zwangsläufig den Verlauf. + +Gib deine Antwort als JSON-Objekt mit den Keys `title`, `subtitle` und `content`. `content` ist dabei die eigentliche Zusammenfassung. +Die Werte der Keys `title`, `subtitle` und `content` müssen unter allen Umständen einfache Strings sein, sonst geht die Applikation kaputt! +Übernehme für die Werte die Sprache des Chats. +Gib keine weitere Beschreibung aus. diff --git a/tutor-assistant/resources/prompt_templates/document_summary_1.txt b/tutor-assistant/resources/prompt_templates/document_summary_1.txt new file mode 100644 index 0000000..f9bc08b --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/document_summary_1.txt @@ -0,0 +1,11 @@ +Ich habe ein Dokument bestehend aus Meta-Daten und Inhalt. +Gib mir Schlagwörter oder kurze Phrasen, die bei einer Similarity-Search helfen können, dieses Dokument zu finden. +Trenne die Schlagwörter und kurzen Phrasen mit Semikolon. +Sehr wichtig: Wenn du Ereignisse findest, wo du das genaue Datum erkennen kannst, dann füge auf jeden Fall das Wort "Calendar" hinzu und liste anschließend alle Datums auf. +Gib mir zudem eine Zusammenfassung. +Trenne die Schlagwörter / kurzen Phrasen und die Zusammenfassung mit zwei Leerzeilen +Beziehe die Meta-Daten und den Inhalt mit ein. + +Meta-Daten: {metadata} + +Inhalt: {content} diff --git a/tutor-assistant/resources/prompt_templates/document_summary_2.txt b/tutor-assistant/resources/prompt_templates/document_summary_2.txt new file mode 100644 index 0000000..fb14f33 --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/document_summary_2.txt @@ -0,0 +1,12 @@ +Ich habe ein Dokument bestehend aus Meta-Daten und Inhalt. +Wenn du Ereignisse findest, wo du das genaue Datum erkennen kannst, gib "CalendarEntries:" zurück, gefolgt von einer Liste der erkannten Datums. +Wenn du keine Ereignisse findest, gib nichts aus. + +Beispiel: +CalendarEntries: 27.10.2024, 01.03.2025 + +Beziehe die Meta-Daten und den Inhalt mit ein. + +Meta-Daten: {metadata} + +Inhalt: {content} diff --git a/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries.txt b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries.txt new file mode 100644 index 0000000..1631f6f --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries.txt @@ -0,0 +1,5 @@ +Dies ist ein Chatverlauf. +Untersuche, welche groben Themen in der letzten Nachricht des Benutzers vorkommen. Benenne sie jeweils mit einem Begriff. +Gib möglichst wenig Begriffe aus. Sie sollen nur die Hauptthemen der Anfragen abdecken. Häufig wird nur ein Begriff notwendig sein. +Verwende die anderen Nachrichten nur, um den Kontext der letzten Nachricht zu verstehen. +Ganz wichtig: Beantworte nicht die Frage. Gib nur die Begriffe zu den groben Themen aus. Trenne sie mit Semikolons. diff --git a/tutor-assistant/tutor_assistant/controller/api/calendar_controller.py b/tutor-assistant/tutor_assistant/controller/api/calendar_controller.py new file mode 100644 index 0000000..655ce81 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/api/calendar_controller.py @@ -0,0 +1,36 @@ +import json + +import numpy as np +from fastapi import APIRouter +from langchain_core.documents import Document + +from tutor_assistant.controller.config.domain_config import config +from tutor_assistant.controller.utils.data_transfer_utils import json_output +from tutor_assistant.domain.calendar.calendar_chain_service import CalendarChainService +from tutor_assistant.utils.string_utils import shorten_middle + +router = APIRouter() + + +@router.post('/calendar') +async def _calendar(): + config.logger.info('POST /calendar') + + chain = CalendarChainService(config).create() + + result = chain.invoke({}) + context = result['context'] + answer = result['answer'] + + entry: Document + json_context = json.dumps([json.loads(json.dumps(entry.to_json(), cls=NumpyEncoder)) for entry in context]) + config.logger.info(f'Result: {shorten_middle(answer, 30)}') + + return json_output(answer) + + +class NumpyEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.float32): + return float(obj) + return super().default(obj) diff --git a/tutor-assistant/tutor_assistant/controller/api/chats_controller.py b/tutor-assistant/tutor_assistant/controller/api/chats_controller.py new file mode 100644 index 0000000..82e1d24 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/api/chats_controller.py @@ -0,0 +1,47 @@ +import json + +from fastapi import Request, APIRouter +from starlette.responses import StreamingResponse + +from tutor_assistant.controller.config.domain_config import config +from tutor_assistant.controller.utils.api_utils import check_request_body +from tutor_assistant.controller.utils.data_transfer_utils import json_output +from tutor_assistant.controller.utils.langchain_utils import stream_chain +from tutor_assistant.domain.chats.message_chain_service import MessageChainService +from tutor_assistant.domain.chats.summary_chain_service import SummaryChainService +from tutor_assistant.utils.string_utils import shorten_middle + +router = APIRouter() + + +@router.post('/chats/message') +async def _message(request: Request): + body = await request.json() + check_request_body(body, ['message']) + user_message_content = body['message'] + history = body.get('history', []) + + config.logger.info(f'POST /chats/message: len(message):{len(user_message_content)};len(history):{len(history)}') + + chain = MessageChainService(config).create(user_message_content, history) + + config.logger.info('Starting event-stream') + + return StreamingResponse( + stream_chain(chain), media_type="text/event-stream" + ) + + +@router.post("/chats/summarize") +async def _summary(request: Request): + body = await request.json() + history = body.get('history', []) + + config.logger.info(f'POST /chats/summarize: len(history):{len(history)}') + + chain = SummaryChainService(config).create(history) + result = chain.invoke({}) + + config.logger.info(f'Result: {shorten_middle(result, 30)}') + + return json_output(result) diff --git a/tutor-assistant/tutor_assistant/controller/api/demo_controller.py b/tutor-assistant/tutor_assistant/controller/api/demo_controller.py new file mode 100644 index 0000000..abd6d05 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/api/demo_controller.py @@ -0,0 +1,69 @@ +from fastapi import APIRouter +from langchain_community.vectorstores import FAISS +from langchain_core.documents import Document +from langchain_core.messages import ChatMessage +from langchain_core.prompts import ChatPromptTemplate + +from tutor_assistant.controller.config.domain_config import config + +router = APIRouter() + + +@router.get("/demo/messages") +async def _get_demo_messages(): + template = ChatPromptTemplate.from_messages( + [ + ('user', 'Hallo?'), + ('ai', 'Wie kann ich dir helfen?'), + ('user', 'Mir ist langweilig') + ] + ) + + + +@router.post('/demo/meta-docs') +async def _meta_docs(): + documents = [ + Document(page_content="Übungsblatt 0, Aufgabe A"), + Document(page_content="Übungsblatt 0, Aufgabe B"), + Document(page_content="Übungsblatt 0, Aufgabe C"), + Document(page_content="Übungsblatt 1, Aufgabe A"), + Document(page_content="Übungsblatt 1, Aufgabe B"), + Document(page_content="Übungsblatt 1, Aufgabe C"), + Document(page_content="Übungsblatt 2, Aufgabe A"), + Document(page_content="Übungsblatt 2, Aufgabe B"), + Document(page_content="Übungsblatt 2, Aufgabe C"), + Document(page_content="Übungsblatt 3, Aufgabe A"), + Document(page_content="Übungsblatt 3, Aufgabe B"), + Document(page_content="Übungsblatt 5, Aufgabe A"), + ] + + queries = [ + 'Übungsblatt 2 Aufgabe A', + + 'Worum geht es in Aufgabe A auf Übungsblatt 2?', + 'Worum geht es in Aufgabe A auf Übungsblatt zwei?', + 'Worum geht es in Aufgabe A auf dem 2. Übungsblatt?', + 'Worum geht es in Aufgabe A auf dem zweiten Übungsblatt?', + + 'Worum geht es in Aufgabe 1 auf Übungsblatt 2?', + 'Worum geht es in Aufgabe 1 auf Übungsblatt zwei?', + 'Worum geht es in Aufgabe 1 auf dem 2. Übungsblatt?', + 'Worum geht es in Aufgabe 1 auf dem zweiten Übungsblatt?', + + 'Worum geht es in Aufgabe eins auf Übungsblatt 2?', + 'Worum geht es in Aufgabe eins auf Übungsblatt zwei?', + 'Worum geht es in Aufgabe eins auf dem 2. Übungsblatt?', + 'Worum geht es in Aufgabe eins auf dem zweiten Übungsblatt?', + + 'Worum geht es in der ersten Aufgabe auf Übungsblatt 2?', + 'Worum geht es in der ersten Aufgabe auf Übungsblatt zwei?', + 'Worum geht es in der ersten Aufgabe auf dem 2. Übungsblatt?', + 'Worum geht es in der ersten Aufgabe auf dem zweiten Übungsblatt?', + ] + + vectorstore = FAISS.from_documents(documents, config.embeddings) + + for query in queries: + result = vectorstore.similarity_search_with_score(query, k=1) + print(result[0][0], result[0][1]) diff --git a/tutor-assistant/tutor_assistant/controller/api/documents_controller.py b/tutor-assistant/tutor_assistant/controller/api/documents_controller.py new file mode 100644 index 0000000..47152a2 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/api/documents_controller.py @@ -0,0 +1,45 @@ +from fastapi import Request, APIRouter + +from tutor_assistant.controller.config.domain_config import config +from tutor_assistant.controller.config.loaders_config import loader_creators +from tutor_assistant.controller.utils.api_utils import check_request_body +from tutor_assistant.controller.utils.loaders_utils import get_loader +from tutor_assistant.domain.documents.document_service import DocumentService + +router = APIRouter() + + +@router.post('/documents/add') +async def _add_document(request: Request): + body: dict = await request.json() + check_request_body(body, ['title', 'originalKey', 'loaderType', 'loaderParams']) + title: str = body['title'] + original_key: str = body['originalKey'] + loader_type: str = body['loaderType'] + loader_params: dict = body['loaderParams'] + summarize_documents_count = body.get('summarizeDocumentsCount', -1) + + config.logger.info( + f'POST /documents/add: loader_type:{loader_type};loader_params:{loader_params.keys()};summarize_documents_count:{summarize_documents_count}') + + loader = get_loader(loader_creators, title, loader_type, loader_params) + + ids = DocumentService(config).add(loader, original_key, summarize_documents_count) + + config.logger.info(f'Result: {ids}') + + return ids + + +@router.post('/documents/delete') +async def _delete_document(request: Request): + body = await request.json() + ids: list[str] = body + + config.logger.info(f'POST /documents/delete: ids:{ids}') + + result = DocumentService(config).delete(ids) + + config.logger.info(f'Result: {result}') + + return True if result is None else result diff --git a/tutor-assistant/tutor_assistant/controller/config/_logging_config.py b/tutor-assistant/tutor_assistant/controller/config/_logging_config.py new file mode 100644 index 0000000..2891145 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/config/_logging_config.py @@ -0,0 +1,25 @@ +import logging +import os +from logging.handlers import RotatingFileHandler + + +def get_logger() -> logging.Logger: + logger = logging.getLogger('tutor-assistant') + logger.setLevel(logging.DEBUG) + + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + console_handler.setFormatter(formatter) + + file_handler = RotatingFileHandler( + f"{os.getenv('DATA_DIR')}/app.log", maxBytes=5 * 1024 * 1024, backupCount=10 + ) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(formatter) + + logger.addHandler(console_handler) + logger.addHandler(file_handler) + + return logger diff --git a/tutor-assistant/tutor_assistant/controller/config/domain_config.py b/tutor-assistant/tutor_assistant/controller/config/domain_config.py new file mode 100644 index 0000000..ac3be29 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/config/domain_config.py @@ -0,0 +1,23 @@ +import os + +from langchain_openai import ChatOpenAI, OpenAIEmbeddings + +from tutor_assistant.controller.config._logging_config import get_logger +from tutor_assistant.controller.utils.model_utils import get_remote_ollama_chat_model +from tutor_assistant.controller.utils.resource_utils import load_resources +from tutor_assistant.domain.domain_config import DomainConfig +from tutor_assistant.domain.vector_stores.chroma_repo import ChromaRepo +from tutor_assistant.domain.vector_stores.faiss_repo import FaissRepo + +_embeddings = OpenAIEmbeddings(model='text-embedding-3-large') + +config = DomainConfig( + ChatOpenAI(model='gpt-4o', temperature=0), + # get_remote_ollama_chat_model('llama3.1:8b'), + _embeddings, + # FaissRepo(f"{os.getenv('DATA_DIR')}/faiss_index", _embeddings), + ChromaRepo(f"{os.getenv('DATA_DIR')}/chroma_index", _embeddings), + load_resources(f'{os.getcwd()}/resources'), + get_logger(), + "Deutsch" +) diff --git a/tutor-assistant/tutor_assistant/controller/config/loaders_config.py b/tutor-assistant/tutor_assistant/controller/config/loaders_config.py new file mode 100644 index 0000000..073af88 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/config/loaders_config.py @@ -0,0 +1,13 @@ +from tutor_assistant.controller.utils.loaders_utils import create_pypdf_loader, create_mediawiki_website_loader, \ + create_assignment_pdf_loader, create_markdown_url_loader, create_web_base_loader, create_mediawiki_url_loader, \ + create_web_headings_loader + +loader_creators = { + 'assignment-pdf-url': create_assignment_pdf_loader, + 'markdown-url': create_markdown_url_loader, + 'mediawiki-url': create_mediawiki_url_loader, + 'mediawiki-web': create_mediawiki_website_loader, + 'pypdf-url': create_pypdf_loader, + 'web-base-web': create_web_base_loader, + 'web-headings-web': create_web_headings_loader, +} diff --git a/tutor-assistant/tutor_assistant/controller/utils/api_utils.py b/tutor-assistant/tutor_assistant/controller/utils/api_utils.py new file mode 100644 index 0000000..41815fa --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/utils/api_utils.py @@ -0,0 +1,7 @@ +from fastapi import HTTPException + + +def check_request_body(d: dict, keys: list): + for key in keys: + if key not in d: + raise HTTPException(status_code=400, detail='Bad request body') \ No newline at end of file diff --git a/tutor-assistant/tutor_assistant/controller/utils/data_transfer_utils.py b/tutor-assistant/tutor_assistant/controller/utils/data_transfer_utils.py new file mode 100644 index 0000000..a2ed52e --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/utils/data_transfer_utils.py @@ -0,0 +1,33 @@ +import json +import re +from typing import Any + +from tutor_assistant.controller.utils.langchain_utils import escape_prompt + + +def messages_from_history(history: list[dict[str, str]]) -> list[tuple[str, str]]: + messages = [] + + for message in history: + content = escape_prompt(message['content']) + messages.append((message['role'], content)) + + return messages + + +def json_output(text: str) -> dict[str, Any]: + start_index = text.find('{') + end_index = text.rfind('}') + + if start_index == -1 or end_index == -1 or start_index > end_index: + return {} + + return json.loads(text[start_index:end_index + 1]) + + +def clean_markdown_output(text: str) -> str: + # Entfernt den ersten ```markdown und alles davor + text = re.sub(r".*?```markdown\s*", "", text, count=1) + # Entfernt das letzte ``` und alles danach + text = re.sub(r"```.*$", "", text, count=1) + return text diff --git a/tutor-assistant/tutor_assistant/controller/utils/event_stream_utils.py b/tutor-assistant/tutor_assistant/controller/utils/event_stream_utils.py new file mode 100644 index 0000000..f4cbaee --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/utils/event_stream_utils.py @@ -0,0 +1,6 @@ +def event_output(token: str): + return f'event: data\ndata: "{token}"\n\n' + + +def event_end(): + return f'event: end\n\n' diff --git a/tutor-assistant/tutor_assistant/controller/utils/langchain_utils.py b/tutor-assistant/tutor_assistant/controller/utils/langchain_utils.py new file mode 100644 index 0000000..cfe0265 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/utils/langchain_utils.py @@ -0,0 +1,36 @@ +import json + +from langchain_core.documents import Document +from langchain_core.runnables import Runnable + +from tutor_assistant.controller.utils.event_stream_utils import event_end, event_output + + +def stream_chain(chain: Runnable, answer_key='answer', context_key='context'): + for item in chain.stream({}): + if context_key in item: + yield from _handle_context(item[context_key], context_key) + elif answer_key in item: + yield from _handle_answer(item[answer_key]) + else: + yield from _handle_answer(item) + + yield event_end() + + +def _handle_context(tokens, context_key: str): + if isinstance(tokens, list): + token: Document + for token in tokens: + encoded_token = json.dumps(token.to_json()) + yield event_output(f'##########{context_key}:{encoded_token}') + + +def _handle_answer(token): + if isinstance(token, str): + token = token.replace('\n', '\\n') + yield event_output(token) + + +def escape_prompt(content: str) -> str: + return content.replace('{', '{{').replace('}', '}}') diff --git a/tutor-assistant/tutor_assistant/controller/utils/loaders_utils.py b/tutor-assistant/tutor_assistant/controller/utils/loaders_utils.py new file mode 100644 index 0000000..691159b --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/utils/loaders_utils.py @@ -0,0 +1,65 @@ +from typing import Any, Callable + +from fastapi import HTTPException +from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader +from langchain_core.document_loaders import BaseLoader + +from tutor_assistant.controller.utils.api_utils import check_request_body +from tutor_assistant.domain.documents.loaders.assignment_pdf_loader import AssignmentPdfUrlLoader +from tutor_assistant.domain.documents.loaders.markdown_loaders import MarkdownUrlSplitByHeadingsLoader +from tutor_assistant.domain.documents.loaders.media_wiki_loaders import MediaWikiWebsiteSplitByHeadingsLoader, \ + MediaWikiUrlSplitByHeadingsLoader +from tutor_assistant.domain.documents.loaders.website_loaders import WebsiteSplitByHeadingsLoader + + +def get_loader(loader_creators: dict[str, Callable], title: str, loader_type: str, + loader_params: dict[str, Any]) -> BaseLoader: + if loader_type not in loader_creators: + raise HTTPException(status_code=400, detail=f'Unsupported loader type: {loader_type}') + + return loader_creators[loader_type](title, loader_params) + + +def create_pypdf_loader(_: str, loader_params: dict[str, Any]) -> BaseLoader: + check_request_body(loader_params, ['url']) + return PyPDFLoader(loader_params['url']) + + +def create_web_base_loader(_: str, loader_params: dict[str, Any]) -> BaseLoader: + check_request_body(loader_params, ['url']) + return WebBaseLoader(web_paths=[loader_params['url']]) + + +def create_mediawiki_website_loader(title: str, loader_params: dict[str, Any]) -> BaseLoader: + check_request_body(loader_params, ['url', 'htmlSelector', 'htmlSelectionIndex']) + return MediaWikiWebsiteSplitByHeadingsLoader( + title, + loader_params['url'], + loader_params['htmlSelector'], + loader_params['htmlSelectionIndex'] + ) + + +def create_mediawiki_url_loader(title: str, loader_params: dict[str, Any]) -> BaseLoader: + check_request_body(loader_params, ['url']) + return MediaWikiUrlSplitByHeadingsLoader(title, loader_params['url']) + + +def create_assignment_pdf_loader(title: str, loader_params: dict[str, Any]) -> BaseLoader: + check_request_body(loader_params, ['url']) + return AssignmentPdfUrlLoader(title, loader_params['url']) + + +def create_markdown_url_loader(title: str, loader_params: dict[str, Any]) -> BaseLoader: + check_request_body(loader_params, ['url']) + return MarkdownUrlSplitByHeadingsLoader(title, loader_params['url']) + + +def create_web_headings_loader(title: str, loader_params: dict[str, Any]) -> BaseLoader: + check_request_body(loader_params, ['url', 'htmlSelector', 'htmlSelectionIndex']) + return WebsiteSplitByHeadingsLoader( + title, + loader_params['url'], + loader_params['htmlSelector'], + loader_params['htmlSelectionIndex'] + ) diff --git a/tutor-assistant/tutor_assistant/controller/utils/model_utils.py b/tutor-assistant/tutor_assistant/controller/utils/model_utils.py new file mode 100644 index 0000000..87c28a0 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/utils/model_utils.py @@ -0,0 +1,17 @@ +import os +from base64 import b64encode + +from langchain_ollama import ChatOllama + + +def get_remote_ollama_chat_model(model_name: str) -> ChatOllama: + host = os.environ.get("OLLAMA_HOST") + username = os.environ.get("OLLAMA_USER") + password = os.environ.get("OLLAMA_PASSWORD") + + if host is None or username is None or password is None: + raise ValueError("OLLAMA_USER and OLLAMA_PASSWORD must be set in .env file") + + headers = {'Authorization': "Basic " + b64encode(f"{username}:{password}".encode('utf-8')).decode("ascii")} + + return ChatOllama(base_url=host, model=model_name, temperature=0, client_kwargs={'headers': headers}) diff --git a/tutor-assistant/tutor_assistant/controller/utils/resource_utils.py b/tutor-assistant/tutor_assistant/controller/utils/resource_utils.py new file mode 100644 index 0000000..cabd6a1 --- /dev/null +++ b/tutor-assistant/tutor_assistant/controller/utils/resource_utils.py @@ -0,0 +1,21 @@ +import os + + +def load_resources(path: str, file_types: list[str] = None): + if file_types is None: + file_types = ['txt', 'yml', 'yaml', 'json', 'xml'] + result = {} + for entry_name in os.listdir(path): + entry_path = os.path.join(path, entry_name) + + if os.path.isdir(entry_path): + result[entry_name] = load_resources(entry_path) + else: + file_type = entry_path.split('.')[-1] + if file_type not in file_types: + continue + with open(entry_path, 'r') as file: + content = file.read() + result[entry_name] = content + + return result diff --git a/tutor-assistant/tutor_assistant/domain/calendar/calendar_chain_service.py b/tutor-assistant/tutor_assistant/domain/calendar/calendar_chain_service.py new file mode 100644 index 0000000..73b2bba --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/calendar/calendar_chain_service.py @@ -0,0 +1,52 @@ +from langchain_core.documents import Document +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.runnables import RunnablePassthrough + +from tutor_assistant.domain.domain_config import DomainConfig +from tutor_assistant.domain.utils.templates import prepend_base_template + + +class CalendarChainService: + def __init__(self, config: DomainConfig): + self._config = config + + def create(self): + template = self._config.resources['prompt_templates']['calendar_chat_model_json.txt'] + complete_template = prepend_base_template(self._config, template) + prompt_template = ChatPromptTemplate.from_template(complete_template) + model_chain = self._get_model_chain(prompt_template) + + retriever_prompt = self._config.resources['prompt_templates']['calendar_vector_store.txt'] + retriever_chain = self._get_retriever_chain(retriever_prompt) + + return ( + RunnablePassthrough + .assign(context=retriever_chain) + # .assign(answer=lambda _: 'Hello World') + .assign(answer=model_chain) + ) + + def _get_model_chain(self, prompt): + model = self._config.chat_model + parser = StrOutputParser() + + return prompt | model | parser + + def _get_retriever_chain(self, query: str): + return lambda _: self._retriever(query) + + def _retriever(self, query: str) -> list[Document]: + vector_store = self._config.vector_store_manager.load() + try: + docs, scores = zip(*vector_store.similarity_search_with_score(query, k=100)) + except: + return [] + result = [] + doc: Document + for doc, score in zip(docs, scores): + if 'CalendarEntries' in doc.metadata['summary']: + doc.metadata["score"] = score + result.append(doc) + + return result diff --git a/tutor-assistant/tutor_assistant/domain/calendar/calendar_chain_service_with_improvement.py b/tutor-assistant/tutor_assistant/domain/calendar/calendar_chain_service_with_improvement.py new file mode 100644 index 0000000..4de4329 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/calendar/calendar_chain_service_with_improvement.py @@ -0,0 +1,52 @@ +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate + +from tutor_assistant.domain.domain_config import DomainConfig +from tutor_assistant.domain.utils.templates import prepend_base_template + + +class CalendarChainServiceWithImprovement: + def __init__(self, config: DomainConfig): + self._config = config + + def load(self): + retriever_chain = self._get_retriever_chain() + context = retriever_chain.invoke({}) + + self._config.logger.info('retrieved context') + + first_prompt_template = self._get_first_prompt_template() + first_model_chain = self._get_model_chain(first_prompt_template) + + self._config.logger.info('retrieved context') + + improvement_prompt_template = self._get_improvement_prompt_template() + improvement_model_chain = self._get_model_chain(improvement_prompt_template) + + first_answer = first_model_chain.invoke({'context': context}) + improved_answer = improvement_model_chain.invoke( + {'prompt': first_prompt_template, 'first_answer': first_answer} + ) + + return improved_answer + + def _get_model_chain(self, prompt): + model = self._config.chat_model + parser = StrOutputParser() + + return prompt | model | parser + + def _get_retriever_chain(self): + retriever_prompt = self._config.resources['prompt_templates']['calendar_vector_store.txt'] + retriever = self._config.vector_store_manager.load().as_retriever() + return (lambda _: retriever_prompt) | retriever + + def _get_first_prompt_template(self): + template = self._config.resources['prompt_templates']['calendar_chat_model_markdown.txt'] + complete_template = prepend_base_template(self._config, template) + return ChatPromptTemplate.from_template(complete_template) + + def _get_improvement_prompt_template(self): + template = self._config.resources['prompt_templates']['calendar_chat_model_improvement.txt'] + complete_template = prepend_base_template(self._config, template) + return ChatPromptTemplate.from_template(complete_template) diff --git a/tutor-assistant/tutor_assistant/domain/chats/message_chain_service.py b/tutor-assistant/tutor_assistant/domain/chats/message_chain_service.py new file mode 100644 index 0000000..8531240 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/chats/message_chain_service.py @@ -0,0 +1,81 @@ +from typing import Any + +from langchain.retrievers import SelfQueryRetriever +from langchain_core.documents import Document +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.runnables import RunnablePassthrough, RunnableSerializable + +from tutor_assistant.controller.utils.data_transfer_utils import messages_from_history +from tutor_assistant.controller.utils.langchain_utils import escape_prompt +from tutor_assistant.domain.documents.retrievers.hybrid_retriever import HybridRetriever +from tutor_assistant.domain.domain_config import DomainConfig +from tutor_assistant.domain.utils.templates import prepend_base_template + + +class MessageChainService: + def __init__(self, config: DomainConfig): + self._config = config + + def create(self, user_message_content: str, history: list[dict[str, str]]) -> RunnableSerializable: + messages = self._get_all_messages(user_message_content, history) + + chat_prompt = self._get_chat_prompt(messages) + + model_chain = self._get_model_chain(chat_prompt) + retriever = HybridRetriever(self._config) + + # retriever_chain = (lambda _: user_message_content) | self._get_self_query_retriever_chain(user_message_content) + # retriever_chain = (lambda _: user_message_content) | self._get_base_retriever_chain(user_message_content) + retriever_chain = (lambda _: messages) | retriever + + return ( + RunnablePassthrough + .assign(context=retriever_chain) + .assign(answer=model_chain) + ) + + @staticmethod + def _get_all_messages(user_message_content: str, history) -> list[tuple[str, str]]: + messages = [] + for msg in messages_from_history(history): + messages.append(msg) + messages.append(('user', escape_prompt(user_message_content))) + + return messages + + def _get_chat_prompt(self, messages: list[tuple[str, str]]) -> ChatPromptTemplate: + template = self._config.resources['prompt_templates']['chat_message.txt'] + complete_template = prepend_base_template(self._config, template) + + prompt_messages = [('system', complete_template)] + prompt_messages.extend(messages) + + prompt_template = ChatPromptTemplate.from_messages(prompt_messages) + + return prompt_template + + def _get_model_chain(self, prompt) -> RunnableSerializable[Any, str]: + model = self._config.chat_model + parser = StrOutputParser() + + return prompt | model | parser + + def _get_base_retriever_chain(self, query: str) -> RunnableSerializable[Any, list[Document]]: + retriever = self._config.vector_store_manager.load().as_retriever() + return (lambda _: query) | retriever + + def _get_self_query_retriever_chain(self, query: str) -> RunnableSerializable[Any, list[Document]]: + metadata_field_info = [] + document_content_description = "Informationen zu Programmieren an der Uni" + llm = self._config.chat_model + retriever = SelfQueryRetriever.from_llm( + llm, self._config.vector_store_manager.load(), document_content_description, metadata_field_info, + enable_limit=True, + verbose=True + ) + + # query += " !Gib mir maximal 20 Dokumente!" + print(retriever.invoke(query)) + + return (lambda _: query) | retriever diff --git a/tutor-assistant/tutor_assistant/domain/chats/summary_chain_service.py b/tutor-assistant/tutor_assistant/domain/chats/summary_chain_service.py new file mode 100644 index 0000000..0bf548c --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/chats/summary_chain_service.py @@ -0,0 +1,24 @@ +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate + +from tutor_assistant.domain.domain_config import DomainConfig +from tutor_assistant.controller.utils.data_transfer_utils import messages_from_history + + +class SummaryChainService: + def __init__(self, config: DomainConfig): + self._config = config + + def create(self, history): + messages = messages_from_history(history) + + system_prompt = self._config.resources['prompt_templates']['chat_summary.txt'] + + messages.append(('system', system_prompt)) + + prompt_template = ChatPromptTemplate.from_messages(messages) + + model = self._config.chat_model + parser = StrOutputParser() + + return prompt_template | model | parser diff --git a/tutor-assistant/tutor_assistant/domain/documents/document_service.py b/tutor-assistant/tutor_assistant/domain/documents/document_service.py new file mode 100644 index 0000000..9dcfcc8 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/documents/document_service.py @@ -0,0 +1,78 @@ +import uuid +from typing import Optional + +from langchain_core.document_loaders import BaseLoader +from langchain_core.documents import Document +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate + +from tutor_assistant.domain.domain_config import DomainConfig + + +class DocumentService: + def __init__(self, config: DomainConfig): + self._config = config + + def add(self, loader: BaseLoader, original_key: str, summarize_documents_count: int) -> list[str]: + documents = loader.load() + ids: list[str] = [] + for i, doc in enumerate(documents): + doc.id = str(uuid.uuid4()) + doc.metadata['id'] = doc.id + doc.metadata['originalKey'] = original_key + ids.append(doc.id) + + # if len(documents) <= summarize_documents_count: + # self._summarize_documents(documents) + + meta_docs = self._handle_meta_docs(documents) + + store = self._config.vector_store_manager.load() + store_ids = store.add_documents(documents) + + if store_ids != ids: + raise RuntimeError( + f'ids and store_ids should be equal, but got ids={ids} and store_ids={store_ids}') + + meta_doc_ids = store.add_documents(meta_docs) + store_ids.extend(meta_doc_ids) + + self._config.vector_store_manager.save(store) + + return store_ids + + def delete(self, ids: list[str]) -> Optional[bool]: + store = self._config.vector_store_manager.load() + success = store.delete(ids) + self._config.vector_store_manager.save(store) + + return success + + @staticmethod + def _handle_meta_docs(docs: list[Document]) -> list[Document]: + meta_docs = [] + for doc in docs: + if 'headings' in doc.metadata: + meta_doc = Document( + doc.metadata['headings'], + metadata={'references': doc.metadata['id']} + ) + meta_docs.append(meta_doc) + del doc.metadata['headings'] + + return meta_docs + + def _summarize_documents(self, documents: list[Document]): + template = self._config.resources['prompt_templates']['document_summary_2.txt'] + + prompt_template = ChatPromptTemplate.from_template(template) + chat_model = self._config.chat_model + chain = prompt_template | chat_model | StrOutputParser() + + for document in documents: + document_values = { + "metadata": document.metadata, + "content": document.page_content, + } + + document.metadata['summary'] = chain.invoke(document_values) diff --git a/tutor-assistant/tutor_assistant/domain/documents/loaders/assignment_pdf_loader.py b/tutor-assistant/tutor_assistant/domain/documents/loaders/assignment_pdf_loader.py new file mode 100644 index 0000000..6ce2ebd --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/documents/loaders/assignment_pdf_loader.py @@ -0,0 +1,33 @@ +import re +from typing import Iterator + +from langchain_community.document_loaders import PyPDFLoader +from langchain_core.document_loaders import BaseLoader +from langchain_core.documents import Document + + +class AssignmentPdfUrlLoader(BaseLoader): + _heading_regex = re.compile(r'Aufgabe\s[A-Z]:\s.+\(\d+\sPunkte\)') + + def __init__(self, title: str, url: str): + self._title = title + self._url = url + + def lazy_load(self) -> Iterator[Document]: + documents = PyPDFLoader(self._url).load() + + heading = None + + for document in documents: + matches = self._heading_regex.findall(document.page_content) + add_present_heading = True + if matches and len(matches) > 0: + heading = matches[0] + add_present_heading = False + + if heading is not None: + document.metadata['headings'] = f'{self._title}\n{heading}' + if add_present_heading: + document.page_content = f'{heading}\n\n{document.page_content}' + + yield document diff --git a/tutor-assistant/tutor_assistant/domain/documents/loaders/markdown_loaders.py b/tutor-assistant/tutor_assistant/domain/documents/loaders/markdown_loaders.py new file mode 100644 index 0000000..9a2e36b --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/documents/loaders/markdown_loaders.py @@ -0,0 +1,27 @@ +import re +from typing import Iterator + +from langchain_core.document_loaders import BaseLoader +from langchain_core.documents import Document + +from tutor_assistant.domain.documents.text_splitters.heading_splitter import split_by_headings +from tutor_assistant.domain.documents.utils.content_loader_utils import load_content_from_url + + +class MarkdownUrlSplitByHeadingsLoader(BaseLoader): + def __init__(self, title: str, url: str): + self._title = title + self._url = url + + def lazy_load(self) -> Iterator[Document]: + content = load_content_from_url(self._url) + yield from _process_markdown_by_headings(self._title, content) + + +def _process_markdown_by_headings(title: str, content: str) -> list[Document]: + cleaned = _remove_comments(content) + return split_by_headings(cleaned, re.compile(r'(?P#+)\s*(?P.+)'), title) + + +def _remove_comments(content: str) -> str: + return re.sub(r'', '', content, flags=re.DOTALL) diff --git a/tutor-assistant/tutor_assistant/domain/documents/loaders/media_wiki_loaders.py b/tutor-assistant/tutor_assistant/domain/documents/loaders/media_wiki_loaders.py new file mode 100644 index 0000000..8e80500 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/documents/loaders/media_wiki_loaders.py @@ -0,0 +1,40 @@ +import re +from typing import Iterator + +from langchain_core.document_loaders import BaseLoader +from langchain_core.documents import Document + +from tutor_assistant.domain.documents.text_splitters.heading_splitter import split_by_headings +from tutor_assistant.domain.documents.utils.content_loader_utils import load_content_from_url, \ + load_website_text + + +class MediaWikiUrlSplitByHeadingsLoader(BaseLoader): + def __init__(self, title: str, url: str): + self._title = title + self._url = url + + def lazy_load(self) -> Iterator[Document]: + content = load_content_from_url(self._url) + yield from _process_media_wiki_by_headings(self._title, content) + + +class MediaWikiWebsiteSplitByHeadingsLoader(BaseLoader): + def __init__(self, title: str, url: str, html_selector: str, html_selection_index: int): + self._title = title + self._url = url + self._html_selector = html_selector + self._html_selection_index = html_selection_index + + def lazy_load(self) -> Iterator[Document]: + content = load_website_text(self._url, self._html_selector, self._html_selection_index) + yield from _process_media_wiki_by_headings(self._title, content) + + +def _process_media_wiki_by_headings(title: str, content: str) -> list[Document]: + cleaned = _remove_comments(content) + return split_by_headings(cleaned, re.compile(r'(?P=+)\s*(?P[^\n=]+)\s*(?P=level)\s*'), title) + + +def _remove_comments(content: str) -> str: + return re.sub(r'', '', content, flags=re.DOTALL) diff --git a/tutor-assistant/tutor_assistant/domain/documents/loaders/website_loaders.py b/tutor-assistant/tutor_assistant/domain/documents/loaders/website_loaders.py new file mode 100644 index 0000000..f568a7b --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/documents/loaders/website_loaders.py @@ -0,0 +1,19 @@ +from typing import Iterator + +from langchain_core.document_loaders import BaseLoader +from langchain_core.documents import Document + +from tutor_assistant.domain.documents.text_splitters.heading_splitter import split_html_by_headings +from tutor_assistant.domain.documents.utils.content_loader_utils import load_website_html + + +class WebsiteSplitByHeadingsLoader(BaseLoader): + def __init__(self, title: str, url: str, html_selector: str, html_selection_index: int): + self._title = title + self._url = url + self._html_selector = html_selector + self._html_selection_index = html_selection_index + + def lazy_load(self) -> Iterator[Document]: + content = load_website_html(self._url, self._html_selector, self._html_selection_index) + yield from split_html_by_headings(content, self._title) diff --git a/tutor-assistant/tutor_assistant/domain/documents/retrievers/hybrid_retriever.py b/tutor-assistant/tutor_assistant/domain/documents/retrievers/hybrid_retriever.py new file mode 100644 index 0000000..eed74cf --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/documents/retrievers/hybrid_retriever.py @@ -0,0 +1,89 @@ +from typing import Any +from uuid import uuid4 + +from langchain_core.callbacks import CallbackManagerForRetrieverRun +from langchain_core.documents import Document +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.retrievers import BaseRetriever + +from tutor_assistant.domain.domain_config import DomainConfig +from tutor_assistant.utils.list_utils import distinct_by + + +class HybridRetriever(BaseRetriever): + def __init__(self, config: DomainConfig, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self._chat_model = config.chat_model + self._vector_store = config.vector_store_manager.load() + self._multiple_prompts = ( + config.resources)['prompt_templates']['hybrid_retriever']['multiple_retriever_queries.txt'] + + def _get_relevant_documents( + self, messages: list[tuple[str, str]], *, run_manager: CallbackManagerForRetrieverRun + ) -> list[Document]: + queries = self._get_queries(messages) + docs: list[Document] = [] + for query in queries: + queried_docs = self._search_with_score(query.strip()) + docs.extend(queried_docs) + referenced_docs = self._get_referenced_by(query, docs) + docs.extend(referenced_docs) + + distinct = distinct_by(self._id_or_random, docs) + filtered = list(filter(lambda x: 'id' in x.metadata, distinct)) + return distinct + + def _get_queries(self, messages: list[tuple[str, str]]) -> list[str]: + chain = self._get_chat_prompt(messages) | self._chat_model + content = chain.invoke({}).content + print('content', content) + queries = content.split(';') + + return queries + + def _get_chat_prompt(self, messages: list[tuple[str, str]]) -> ChatPromptTemplate: + prompt_messages = messages + [('system', self._multiple_prompts)] + return ChatPromptTemplate.from_messages(prompt_messages) + + def _search_with_score(self, query: str) -> list[Document]: + try: + docs, scores = zip( + *self._vector_store.similarity_search_with_score( + query, + k=5 + ) + ) + except Exception as e: + print('Exception:', e) + return [] + result = [] + doc: Document + for doc, np_score in zip(docs, scores): + score = float(np_score) + doc.metadata['score'] = score + if np_score < 5: + result.append(doc) + + return result + + def _get_referenced_by(self, query: str, docs: list[Document]) -> list[Document]: + referenced_docs: list[Document] = [] + for doc in docs: + if 'references' in doc.metadata: + ids: str = doc.metadata['references'] + queried_docs = self._vector_store.similarity_search( + query, + k=1000, + # filter=lambda metadata: (metadata['id'] in ids) if 'id' in metadata else False, + # filter={'id__in': ';'.join(ids)}, + ) + filtered_docs: list[Document] = list(filter( + lambda d: (d.metadata['id'] in ids) if 'id' in d.metadata else False, + queried_docs + )) + referenced_docs.extend(filtered_docs) + return referenced_docs + + @staticmethod + def _id_or_random(doc: Document) -> Any: + return doc.metadata['id'] if 'id' in doc.metadata else str(uuid4()) diff --git a/tutor-assistant/tutor_assistant/domain/documents/text_splitters/heading_splitter.py b/tutor-assistant/tutor_assistant/domain/documents/text_splitters/heading_splitter.py new file mode 100644 index 0000000..d42ddb3 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/documents/text_splitters/heading_splitter.py @@ -0,0 +1,95 @@ +import re + +from bs4 import BeautifulSoup +from langchain_core.documents import Document + + +def split_html_by_headings(html: str, title: str, max_level=3) -> list[Document]: + regex = re.compile(r'[1-6])>\s*(?P.*?)\s*') + soup = BeautifulSoup(html, 'html.parser') + + for level in range(1, max_level + 1): + for heading in soup.find_all(f'h{level}'): + heading_text = heading.text.strip().replace('\n', '') + heading.string = heading_text + heading.insert_before("\n") + heading.insert_after("\n") + + docs = split_by_headings(str(soup), regex, title, max_level) + + for doc in docs: + soup = BeautifulSoup(doc.page_content, 'html.parser') + doc.page_content = soup.text + return docs + + +def split_by_headings(text: str, regex: re.Pattern[str], title: str, max_level=3) -> list[Document]: + if max_level < 1: + max_level = 1 + + sections: list[Document] = [] + content = '' + headings = [] + headings[:] = [None] * max_level + heading_codes = {} + + def get_metadata(): + headings_dict = {} + for (i, heading) in enumerate(headings): + if heading is not None: + headings_dict[f'Heading {i + 1}'] = heading + return headings_dict + + def get_heading_str(): + result = f'{title}\n' + for heading in headings: + if heading is not None: + result += f'{heading_codes[heading]}\n' + + return result + + def get_content_with_headings(): + result = f'{title}\n' + for heading in headings: + if heading is not None: + result += f'{heading_codes[heading]}\n' + result += f'\n{content}' + return result.strip() + + def append_document(): + if len(content.strip()) > 0: + heading_str = get_heading_str() + doc = Document(f'{heading_str}\n{content}') + doc.metadata['headings'] = heading_str + sections.append(doc) + + def handle_match(): + level_group = match.group('level') + if level_group.isdigit(): + level = int(level_group) + else: + level = len(level_group) + + heading = match.group('heading').strip() + + if level <= max_level: + append_document() + + heading_index = level - 1 + headings[heading_index:] = [None] * (max_level - heading_index) + headings[heading_index] = heading + heading_codes[heading] = match.group() + + lines = text.split('\n') + for line in lines: + match = regex.match(line) + + if match: + handle_match() + content = '' + else: + content += line + '\n' + + append_document() + + return sections diff --git a/tutor-assistant/tutor_assistant/domain/documents/utils/content_loader_utils.py b/tutor-assistant/tutor_assistant/domain/documents/utils/content_loader_utils.py new file mode 100644 index 0000000..fa8801b --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/documents/utils/content_loader_utils.py @@ -0,0 +1,22 @@ +import requests +from bs4 import BeautifulSoup + + +def load_content_from_url(url: str) -> str: + response = requests.get(url) + response.raise_for_status() + return response.text + + +def load_website_text(url: str, html_selector: str, html_selection_index: int) -> str: + response = requests.get(url) + soup = BeautifulSoup(response.text, 'html.parser') + + return soup.select(html_selector)[html_selection_index].text + + +def load_website_html(url: str, html_selector: str, html_selection_index: int) -> str: + response = requests.get(url) + soup = BeautifulSoup(response.text, 'html.parser') + + return soup.select(html_selector)[html_selection_index].prettify() diff --git a/tutor-assistant/tutor_assistant/domain/domain_config.py b/tutor-assistant/tutor_assistant/domain/domain_config.py new file mode 100644 index 0000000..0939314 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/domain_config.py @@ -0,0 +1,26 @@ +import logging +from typing import Any + +from langchain_core.embeddings import Embeddings +from langchain_core.language_models import BaseChatModel + +from tutor_assistant.domain.vector_stores.vector_store_repo import VectorStoreRepo + + +class DomainConfig: + def __init__(self, + chat_model: BaseChatModel, + embeddings: Embeddings, + vector_store_manager: VectorStoreRepo, + resources: dict[str, Any], + logger: logging.Logger, + language: str, + ): + self.chat_model = chat_model + self.embeddings = embeddings + self.vector_store_manager = vector_store_manager + self.resources = resources + self.logger = logger + self.language = language + + logger.info('Application configuration complete.') diff --git a/tutor-assistant/tutor_assistant/domain/utils/templates.py b/tutor-assistant/tutor_assistant/domain/utils/templates.py new file mode 100644 index 0000000..8d16e18 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/utils/templates.py @@ -0,0 +1,13 @@ +from datetime import datetime + +from tutor_assistant.domain.domain_config import DomainConfig + + +def prepend_base_template(config: DomainConfig, template: str) -> str: + now = datetime.now() + formatted_date = now.strftime("%A, %d.%m.%Y") + + base_template: str = config.resources['prompt_templates']['base_template.txt'] + filled_base_template = base_template.replace('{date}', formatted_date).replace('{language}', config.language) + + return f"{filled_base_template}\n\n{template}" diff --git a/tutor-assistant/tutor_assistant/domain/vector_stores/chroma_repo.py b/tutor-assistant/tutor_assistant/domain/vector_stores/chroma_repo.py new file mode 100644 index 0000000..53d3883 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/vector_stores/chroma_repo.py @@ -0,0 +1,20 @@ +from langchain_chroma import Chroma +from langchain_core.embeddings import Embeddings + +from tutor_assistant.domain.vector_stores.vector_store_repo import VectorStoreRepo + + +class ChromaRepo(VectorStoreRepo): + + def __init__(self, store_path: str, embeddings: Embeddings): + self._store_path = store_path + self._embeddings = embeddings + + def create_if_not_exists(self) -> Chroma: + return self.load() + + def save(self, store) -> None: + pass + + def load(self) -> Chroma: + return Chroma(embedding_function=self._embeddings, persist_directory=self._store_path) diff --git a/tutor-assistant/tutor_assistant/domain/vector_stores/faiss_repo.py b/tutor-assistant/tutor_assistant/domain/vector_stores/faiss_repo.py new file mode 100644 index 0000000..2f14ed6 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/vector_stores/faiss_repo.py @@ -0,0 +1,38 @@ +import os + +import faiss +from langchain_community.docstore import InMemoryDocstore +from langchain_community.vectorstores import FAISS +from langchain_core.embeddings import Embeddings + +from tutor_assistant.domain.vector_stores.vector_store_repo import VectorStoreRepo + + +class FaissRepo(VectorStoreRepo): + + def __init__(self, store_path: str, embeddings: Embeddings): + self._store_path = store_path + self._embeddings = embeddings + + def create_if_not_exists(self) -> FAISS: + if os.path.exists(self._store_path): + return self.load() + + index = faiss.IndexFlatL2(len(self._embeddings.embed_query("hello world"))) + + store = FAISS( + embedding_function=self._embeddings, + index=index, + docstore=InMemoryDocstore(), + index_to_docstore_id={}, + ) + + self.save(store) + + return store + + def save(self, store: FAISS) -> None: + store.save_local(self._store_path) + + def load(self) -> FAISS: + return FAISS.load_local(self._store_path, self._embeddings, allow_dangerous_deserialization=True) diff --git a/tutor-assistant/tutor_assistant/domain/vector_stores/vector_store_repo.py b/tutor-assistant/tutor_assistant/domain/vector_stores/vector_store_repo.py new file mode 100644 index 0000000..ba6e7e3 --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/vector_stores/vector_store_repo.py @@ -0,0 +1,13 @@ +from langchain_core.vectorstores import VectorStore + + +class VectorStoreRepo: + + def create_if_not_exists(self) -> VectorStore: + raise NotImplementedError() + + def save(self, store) -> None: + raise NotImplementedError() + + def load(self) -> VectorStore: + raise NotImplementedError() diff --git a/tutor-assistant/tutor_assistant/utils/list_utils.py b/tutor-assistant/tutor_assistant/utils/list_utils.py new file mode 100644 index 0000000..981bece --- /dev/null +++ b/tutor-assistant/tutor_assistant/utils/list_utils.py @@ -0,0 +1,14 @@ +from typing import List, Callable, TypeVar + +T = TypeVar("T") + + +def distinct_by(get_distinct_key: Callable[[T], object], items: List[T]) -> List[T]: + seen = set() + result = [] + for item in items: + key = get_distinct_key(item) + if key not in seen: + seen.add(key) + result.append(item) + return result diff --git a/tutor-assistant/tutor_assistant/utils/string_utils.py b/tutor-assistant/tutor_assistant/utils/string_utils.py new file mode 100644 index 0000000..fcdc732 --- /dev/null +++ b/tutor-assistant/tutor_assistant/utils/string_utils.py @@ -0,0 +1,8 @@ +def shorten_middle(text: str, char_count: int, replace_new_lines=True, middle=' ... ') -> str: + if replace_new_lines: + text = text.replace('\n', '') + + if len(text) <= 2 * char_count: + return text + else: + return text[:char_count] + middle + text[-char_count:] diff --git a/tutor-assistent-web/Dockerfile b/tutor-assistent-web/Dockerfile new file mode 100644 index 0000000..7824f24 --- /dev/null +++ b/tutor-assistent-web/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20 AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine + +COPY --from=builder /app/dist /usr/share/nginx/html + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/tutor-assistent-web/ignore.eslint.config.js b/tutor-assistent-web/ignore.eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/tutor-assistent-web/ignore.eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/tutor-assistent-web/index.html b/tutor-assistent-web/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/tutor-assistent-web/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/tutor-assistent-web/package-lock.json b/tutor-assistent-web/package-lock.json new file mode 100644 index 0000000..7029ca1 --- /dev/null +++ b/tutor-assistent-web/package-lock.json @@ -0,0 +1,5680 @@ +{ + "name": "tutor-assistent", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tutor-assistent", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "@fontsource/inter": "^5.1.0", + "@mui/icons-material": "^6.1.3", + "@mui/joy": "^5.0.0-beta.48", + "@react-keycloak/web": "^3.4.0", + "axios": "^1.7.7", + "classnames": "^2.5.1", + "date-fns": "^4.1.0", + "i18next-browser-languagedetector": "^8.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^15.0.1", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.26.2", + "rehype-highlight": "^7.0.0", + "remark-gfm": "^4.0.0", + "sse.js": "^2.5.0", + "use-local-storage-state": "^19.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "5.4.6" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "dependencies": { + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", + "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.2.0", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/cache": { + "version": "11.13.1", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", + "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", + "integrity": "sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/react": { + "version": "11.13.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", + "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/cache": "^11.13.0", + "@emotion/serialize": "^1.3.1", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", + "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "node_modules/@emotion/styled": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", + "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", + "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", + "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", + "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz", + "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==", + "dependencies": { + "@floating-ui/utils": "^0.2.7" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.10", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz", + "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.7" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz", + "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==" + }, + "node_modules/@fontsource/inter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.1.0.tgz", + "integrity": "sha512-zKZR3kf1G0noIes1frLfOHP5EXVVm0M7sV/l9f/AaYf+M/DId35FO4LkigWjqWYjTJZGgplhdv4cB+ssvCqr5A==" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", + "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.3.tgz", + "integrity": "sha512-QBQCCIMSAv6IkArTg4Hg8q2sJRhHOci8oPAlkHWFlt2ghBdy3EqyLbIELLE/bhpqhX+E/ZkPYGIUQCd5/L0owA==", + "dependencies": { + "@babel/runtime": "^7.25.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^6.1.3", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/joy": { + "version": "5.0.0-beta.48", + "resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.48.tgz", + "integrity": "sha512-OhTvjuGl9I5IvpBr0BQyDehIW/xb2yteW6YglHJMdOb/279nItn76X1NBtPV9ImldNlBjReGwvpOXmBTTGER9w==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.16.1", + "@mui/system": "^5.16.1", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.1", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.3.tgz", + "integrity": "sha512-loV5MBoMKLrK80JeWINmQ1A4eWoLv51O2dBPLJ260IAhupkB3Wol8lEQTEvvR2vO3o6xRHuXe1WaQEP6N3riqg==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/core-downloads-tracker": "^6.1.3", + "@mui/system": "^6.1.3", + "@mui/types": "^7.2.18", + "@mui/utils": "^6.1.3", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.3.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^6.1.3", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/@mui/core-downloads-tracker": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.3.tgz", + "integrity": "sha512-ajMUgdfhTb++rwqj134Cq9f4SRN8oXUqMRnY72YBnXiXai3olJLLqETheRlq3MM8wCKrbq7g6j7iWL1VvP44VQ==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material/node_modules/@mui/private-theming": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.3.tgz", + "integrity": "sha512-XK5OYCM0x7gxWb/WBEySstBmn+dE3YKX7U7jeBRLm6vHU5fGUd7GiJWRirpivHjOK9mRH6E1MPIVd+ze5vguKQ==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/utils": "^6.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/@mui/styled-engine": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.3.tgz", + "integrity": "sha512-i4yh9m+eMZE3cNERpDhVr6Wn73Yz6C7MH0eE2zZvw8d7EFkIJlCQNZd1xxGZqarD2DDq2qWHcjIOucWGhxACtA==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.25.6", + "@emotion/cache": "^11.13.1", + "@emotion/serialize": "^1.3.2", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/@mui/system": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.3.tgz", + "integrity": "sha512-ILaD9UsLTBLjMcep3OumJMXh1PYr7aqnkHm/L47bH46+YmSL1zWAX6tWG8swEQROzW2GvYluEMp5FreoxOOC6w==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/private-theming": "^6.1.3", + "@mui/styled-engine": "^6.1.3", + "@mui/types": "^7.2.18", + "@mui/utils": "^6.1.3", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/@mui/utils": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.3.tgz", + "integrity": "sha512-4JBpLkjprlKjN10DGb1aiy/ii9TKbQ601uSHtAmYFAS879QZgAD7vRnv/YBE4iBbc7NXzFgbQMCOFrupXWekIA==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/types": "^7.2.18", + "@types/prop-types": "^15.7.13", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", + "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.16.6", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", + "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", + "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.16.6", + "@mui/styled-engine": "^5.16.6", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.18", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz", + "integrity": "sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-keycloak/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@react-keycloak/core/-/core-3.2.0.tgz", + "integrity": "sha512-1yzU7gQzs+6E1v6hGqxy0Q+kpMHg9sEcke2yxZR29WoU8KNE8E50xS6UbI8N7rWsgyYw8r9W1cUPCOF48MYjzw==", + "dependencies": { + "react-fast-compare": "^3.2.0" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/reactkeycloak" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@react-keycloak/web": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@react-keycloak/web/-/web-3.4.0.tgz", + "integrity": "sha512-yKKSCyqBtn7dt+VckYOW1IM5NW999pPkxDZOXqJ6dfXPXstYhOQCkTZqh8l7UL14PkpsoaHDh7hSJH8whah01g==", + "dependencies": { + "@babel/runtime": "^7.9.0", + "@react-keycloak/core": "^3.2.0", + "hoist-non-react-statics": "^3.3.2" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/reactkeycloak" + }, + "peerDependencies": { + "keycloak-js": ">=9.0.2", + "react": ">=16.8", + "react-dom": ">=16.8", + "typescript": ">=3.8" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", + "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.2.tgz", + "integrity": "sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.2.tgz", + "integrity": "sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.2.tgz", + "integrity": "sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.2.tgz", + "integrity": "sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.2.tgz", + "integrity": "sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.2.tgz", + "integrity": "sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.2.tgz", + "integrity": "sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.2.tgz", + "integrity": "sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.2.tgz", + "integrity": "sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.2.tgz", + "integrity": "sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.2.tgz", + "integrity": "sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.2.tgz", + "integrity": "sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.2.tgz", + "integrity": "sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.2.tgz", + "integrity": "sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.2.tgz", + "integrity": "sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.2.tgz", + "integrity": "sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.2.tgz", + "integrity": "sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.2.tgz", + "integrity": "sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" + }, + "node_modules/@types/react": { + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", + "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", + "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "peer": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", + "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/type-utils": "8.5.0", + "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", + "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", + "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", + "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/utils": "8.5.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", + "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", + "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", + "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.22", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.22.tgz", + "integrity": "sha512-tKYm5YHPU1djz0O+CGJ+oJIvimtsCcwR2Z9w7Skh08lUdyzXY5djods3q+z2JkWdb7tCcmM//eVavSRAiaPRNg==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", + "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.10.0", + "@eslint/plugin-kit": "^0.1.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.1.0-rc-fb9a90fa48-20240614", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0-rc-fb9a90fa48-20240614.tgz", + "integrity": "sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.11.tgz", + "integrity": "sha512-wrAKxMbVr8qhXTtIKfXqAn5SAtRZt0aXxe5P23Fh4pUAdC6XEsybGLB8P0PI4j1yYqOgUEUlzKAGDfo7rJOjcw==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", + "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", + "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/i18next": { + "version": "23.15.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.15.1.tgz", + "integrity": "sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "peer": true, + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-sha256": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz", + "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==", + "peer": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/keycloak-js": { + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.5.tgz", + "integrity": "sha512-/lrUWHpDoqt4XrY07Wtfo4S2/KPJ3pvE4y1xtus65pAlgqtiEONtlYOdAdJv6iAWl7RE4Jx7eaN5P1A5yO/NLg==", + "peer": true, + "dependencies": { + "js-sha256": "^0.11.0", + "jwt-decode": "^4.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.1.0.tgz", + "integrity": "sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.9.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.3.tgz", + "integrity": "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", + "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.45", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", + "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "node_modules/react-i18next": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.1.tgz", + "integrity": "sha512-NwxLqNM6CLbeGA9xPsjits0EnXdKgCRSS6cgkgOdNcPXqL+1fYNl8fBg1wmnnHvFy812Bt4IWTPE9zjoPmFj3w==", + "dependencies": { + "@babel/runtime": "^7.24.8", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/react-markdown": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", + "integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", + "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "dependencies": { + "@remix-run/router": "1.19.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", + "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "dependencies": { + "@remix-run/router": "1.19.2", + "react-router": "6.26.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/rehype-highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.0.tgz", + "integrity": "sha512-QtobgRgYoQaK6p1eSr2SD1i61f7bjF2kZHAQHxeCHAuJf7ZUDMvQ7owDq9YTkmar5m5TSUol+2D3bp3KfJf/oA==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-text": "^4.0.0", + "lowlight": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.2.tgz", + "integrity": "sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.2", + "@rollup/rollup-android-arm64": "4.24.2", + "@rollup/rollup-darwin-arm64": "4.24.2", + "@rollup/rollup-darwin-x64": "4.24.2", + "@rollup/rollup-freebsd-arm64": "4.24.2", + "@rollup/rollup-freebsd-x64": "4.24.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.2", + "@rollup/rollup-linux-arm-musleabihf": "4.24.2", + "@rollup/rollup-linux-arm64-gnu": "4.24.2", + "@rollup/rollup-linux-arm64-musl": "4.24.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.2", + "@rollup/rollup-linux-riscv64-gnu": "4.24.2", + "@rollup/rollup-linux-s390x-gnu": "4.24.2", + "@rollup/rollup-linux-x64-gnu": "4.24.2", + "@rollup/rollup-linux-x64-musl": "4.24.2", + "@rollup/rollup-win32-arm64-msvc": "4.24.2", + "@rollup/rollup-win32-ia32-msvc": "4.24.2", + "@rollup/rollup-win32-x64-msvc": "4.24.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sse.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/sse.js/-/sse.js-2.5.0.tgz", + "integrity": "sha512-I7zYndqOOkNpz9KIdFZ8c8A7zs1YazNewBr8Nsi/tqThfJkVPuP1q7UE2h4B0RwoWZxbBYpd06uoW3NI3SaZXg==" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.5.0.tgz", + "integrity": "sha512-uD+XxEoSIvqtm4KE97etm32Tn5MfaZWgWfMMREStLxR6JzvHkc2Tkj7zhTEK5XmtpTmKHNnG8Sot6qDfhHtR1Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.5.0", + "@typescript-eslint/parser": "8.5.0", + "@typescript-eslint/utils": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-local-storage-state": { + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-19.4.0.tgz", + "integrity": "sha512-Ixs/kA2B6mbUv9B78MPNoZ8DGYJ7U407QPKBQrNZQbyYSvgPfBKPMscFTPy56Q+zmcmU9m0SGnF6qs1H2vXv6w==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/astoilkov" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/tutor-assistent-web/package.json b/tutor-assistent-web/package.json new file mode 100644 index 0000000..c09d1ed --- /dev/null +++ b/tutor-assistent-web/package.json @@ -0,0 +1,46 @@ +{ + "name": "tutor-assistent", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "@fontsource/inter": "^5.1.0", + "@mui/icons-material": "^6.1.3", + "@mui/joy": "^5.0.0-beta.48", + "@react-keycloak/web": "^3.4.0", + "axios": "^1.7.7", + "classnames": "^2.5.1", + "date-fns": "^4.1.0", + "i18next-browser-languagedetector": "^8.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^15.0.1", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.26.2", + "rehype-highlight": "^7.0.0", + "remark-gfm": "^4.0.0", + "sse.js": "^2.5.0", + "use-local-storage-state": "^19.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "5.4.6" + } +} diff --git a/tutor-assistent-web/public/vite.svg b/tutor-assistent-web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/tutor-assistent-web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tutor-assistent-web/src/app/App.tsx b/tutor-assistent-web/src/app/App.tsx new file mode 100644 index 0000000..020d526 --- /dev/null +++ b/tutor-assistent-web/src/app/App.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { Routing } from './routing/Routing' +import { Auth } from './auth/Auth.tsx' +import texts from '../texts/texts.json' +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 { CalendarBar } from '../modules/calendar/components/CalendarBar.tsx' + +configureI18n(texts) + + +const App = () => { + return ( + + +
+ + + + + + + + +
+
+
+ ) +} + +export default App diff --git a/tutor-assistent-web/src/app/JoyTheme.tsx b/tutor-assistent-web/src/app/JoyTheme.tsx new file mode 100644 index 0000000..8561605 --- /dev/null +++ b/tutor-assistent-web/src/app/JoyTheme.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' +import { CssVarsProvider } from '@mui/joy/styles' +import { ChildrenProps } from '../lib/types.ts' + +export function JoyTheme({ children }: ChildrenProps) { + return ( + + {children} + + ) +} diff --git a/tutor-assistent-web/src/app/MainStyle.tsx b/tutor-assistent-web/src/app/MainStyle.tsx new file mode 100644 index 0000000..6ea0785 --- /dev/null +++ b/tutor-assistent-web/src/app/MainStyle.tsx @@ -0,0 +1,60 @@ +import { Box, styled } from '@mui/joy' + +export const Main = styled(Box)` + width: 100%; + height: 100%; + + table.noTableMargin { + margin-bottom: 0; + } + + &.noTableMargin > table { + margin-bottom: 0; + } + + table { + border-spacing: 0; + border-collapse: collapse; + display: block; + margin-top: 0; + margin-bottom: 16px; + width: max-content; + max-width: 100%; + border-color: ${props => props.theme.palette.divider}; + background: ${props => props.theme.palette.background.surface}; + } + + thead { + border-bottom: 2px solid ${props => props.theme.palette.divider}; + } + + td, th { + padding: 6px 13px; + border: 1px solid ${props => props.theme.palette.divider}; + } + + th { + font-weight: bold; + } + + table tr:first-of-type th { + border-top: none; + } + + table tr *:first-of-type { + border-left: none; + } + + table tr *:last-child { + border-right: none; + } + + table tr:last-child td { + border-bottom: none; + + } + + table img { + background-color: transparent; + } +` diff --git a/tutor-assistent-web/src/app/auth/Auth.tsx b/tutor-assistent-web/src/app/auth/Auth.tsx new file mode 100644 index 0000000..1a7fc4a --- /dev/null +++ b/tutor-assistent-web/src/app/auth/Auth.tsx @@ -0,0 +1,64 @@ +import { createContext } from 'react' +import axios, { AxiosInstance } from 'axios' +import { chill, isNotPresent } from '../../lib/utils/utils.ts' +import { ChildrenProps } from '../../lib/types.ts' +import { useKeycloak } from '@react-keycloak/web' + +type AuthContextType = { + getAuthHttp: () => AxiosInstance, + isLoggedIn: () => boolean, + openLogin: () => void, + logout: () => void, + getRoles: () => string[] +} + +export const AuthContext = createContext({ + getAuthHttp: () => axios, + isLoggedIn: () => false, + openLogin: chill, + logout: chill, + getRoles: () => [], +}) + + +export function Auth({ children }: ChildrenProps) { + + const { keycloak, initialized } = useKeycloak() + + + function isLoggedIn() { + return keycloak.authenticated ?? false + } + + if (!initialized) return <> + + function getAuthHttp() { + if (!keycloak.authenticated || isNotPresent(keycloak.token)) { + keycloak.login() + return axios + } + + return axios.create({ + headers: { + Authorization: `Bearer ${keycloak.token}`, + }, + }) + } + + return ( + keycloak.tokenParsed?.realm_access?.roles ?? [], + }} + > + {children} + + ) +} + + + diff --git a/tutor-assistent-web/src/app/auth/Authenticated.tsx b/tutor-assistent-web/src/app/auth/Authenticated.tsx new file mode 100644 index 0000000..675e3db --- /dev/null +++ b/tutor-assistent-web/src/app/auth/Authenticated.tsx @@ -0,0 +1,24 @@ +import { ChildrenProps } from '../../lib/types.ts' +import { useAuth } from './useAuth.ts' +import { isPresent } from '../../lib/utils/utils.ts' +import { haveCommonElements } from '../../lib/utils/array-utils.ts' + +interface Props extends ChildrenProps { + roles?: string[] +} + +export function Authenticated({ children, roles }: Props) { + const { isLoggedIn, openLogin, getRoles } = useAuth() + + + if (!isLoggedIn()) { + openLogin() + return <> + } + + if (isPresent(roles) && !haveCommonElements(roles, getRoles())) { + return <> + } + + return children +} diff --git a/tutor-assistent-web/src/app/auth/useAuth.ts b/tutor-assistent-web/src/app/auth/useAuth.ts new file mode 100644 index 0000000..e4193bb --- /dev/null +++ b/tutor-assistent-web/src/app/auth/useAuth.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react' +import { AuthContext } from './Auth.tsx' + +export function useAuth() { + return useContext(AuthContext) +} diff --git a/tutor-assistent-web/src/app/base.ts b/tutor-assistent-web/src/app/base.ts new file mode 100644 index 0000000..12510d5 --- /dev/null +++ b/tutor-assistent-web/src/app/base.ts @@ -0,0 +1,9 @@ +import { getCurrentBaseUrl } from '../lib/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() + +export const apiBaseUrl = envApiBaseUrl !== 'default' ? envApiBaseUrl : `${currentBaseUrl}/api` +export const keycloakBaseUrl = envKeycloakBaseUrl !== 'default' ? envKeycloakBaseUrl : `${currentBaseUrl}/auth` diff --git a/tutor-assistent-web/src/app/config/i18n-config.ts b/tutor-assistent-web/src/app/config/i18n-config.ts new file mode 100644 index 0000000..d2ed291 --- /dev/null +++ b/tutor-assistent-web/src/app/config/i18n-config.ts @@ -0,0 +1,32 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import LanguageDetector from 'i18next-browser-languagedetector' + +export function configureI18n(texts: 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] + }) + }) + + 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 + }, + }) +} diff --git a/tutor-assistent-web/src/app/config/keycloak-config.ts b/tutor-assistent-web/src/app/config/keycloak-config.ts new file mode 100644 index 0000000..3f97d2d --- /dev/null +++ b/tutor-assistent-web/src/app/config/keycloak-config.ts @@ -0,0 +1,8 @@ +import Keycloak from 'keycloak-js' +import { keycloakBaseUrl } from '../base.ts' + +export const keycloak = new Keycloak({ + url: keycloakBaseUrl, + realm: 'tutor-assistant', + clientId: 'tutor-assistant-web', +}) diff --git a/tutor-assistent-web/src/app/routing/Routing.tsx b/tutor-assistent-web/src/app/routing/Routing.tsx new file mode 100644 index 0000000..9a84be4 --- /dev/null +++ b/tutor-assistent-web/src/app/routing/Routing.tsx @@ -0,0 +1,44 @@ +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' + +interface Props { + +} + +export function Routing({}: Props) { + + return ( + + + } /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + ) +} diff --git a/tutor-assistent-web/src/common/components/Bar.tsx b/tutor-assistent-web/src/common/components/Bar.tsx new file mode 100644 index 0000000..3c5bb89 --- /dev/null +++ b/tutor-assistent-web/src/common/components/Bar.tsx @@ -0,0 +1,16 @@ +import { styled } from '@mui/joy' +import { VStack } from '../../lib/components/flex-layout.tsx' + +const barWidth = '420px' +export const Bar = styled(VStack)` + min-width: ${barWidth}; + width: ${barWidth}; + max-width: ${barWidth}; + background: ${props => props.theme.palette.background.surface}; + border-right: 1px solid ${props => props.theme.palette.divider}; + + &.right { + border-right: none; + border-left: 1px solid ${props => props.theme.palette.divider}; + } +` diff --git a/tutor-assistent-web/src/common/components/Header.tsx b/tutor-assistent-web/src/common/components/Header.tsx new file mode 100644 index 0000000..4c8ed25 --- /dev/null +++ b/tutor-assistent-web/src/common/components/Header.tsx @@ -0,0 +1,23 @@ +import { Row, Spacer } from '../../lib/components/flex-layout.tsx' +import { Divider, Typography } from '@mui/joy' +import React, { ReactNode } from 'react' +import { isPresent } from '../../lib/utils/utils.ts' + +interface Props { + title: string + leftNode?: ReactNode + rightNode?: ReactNode +} + +export function Header({ title, leftNode, rightNode }: Props) { + return ( + <> + + {isPresent(leftNode) && leftNode} + {title} + {isPresent(rightNode) && rightNode} + + + + ) +} diff --git a/tutor-assistent-web/src/common/components/StyledDivider.tsx b/tutor-assistent-web/src/common/components/StyledDivider.tsx new file mode 100644 index 0000000..18587d8 --- /dev/null +++ b/tutor-assistent-web/src/common/components/StyledDivider.tsx @@ -0,0 +1,5 @@ +import { Divider, styled } from '@mui/joy' + +export const StyledDivider = styled(Divider)` + margin-top: 0 !important; +` diff --git a/tutor-assistent-web/src/common/components/StyledMarkdown.tsx b/tutor-assistent-web/src/common/components/StyledMarkdown.tsx new file mode 100644 index 0000000..3a94dd8 --- /dev/null +++ b/tutor-assistent-web/src/common/components/StyledMarkdown.tsx @@ -0,0 +1,8 @@ +import { styled } from '@mui/joy' +import Markdown from 'react-markdown' + +export const StyledMarkdown = styled(Markdown)` + code.hljs { + background: ${props => props.theme.palette.background.surface}; + } +` diff --git a/tutor-assistent-web/src/index.css b/tutor-assistent-web/src/index.css new file mode 100644 index 0000000..356388f --- /dev/null +++ b/tutor-assistent-web/src/index.css @@ -0,0 +1,38 @@ +@import 'highlight.js/styles/stackoverflow-light.min.css' screen; +@import 'highlight.js/styles/stackoverflow-dark.min.css' screen and (prefers-color-scheme: dark); + +* { + box-sizing: border-box; +} + +:root { + /*noinspection CssUnresolvedCustomProperty,CssNoGenericFontName*/ + font-family: var(--joy-fontFamily-body); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + color-scheme: light dark; +} + +body { + width: 100vw; + height: 100vh; + margin: 0; + padding: 0; + overflow-y: hidden; +} + +div#root { + width: 100%; + height: 100%; + overflow-y: hidden; +} + +/* +display: flex; +place-items: center; +*/ + + diff --git a/tutor-assistent-web/src/lib/components/BaseLayout.tsx b/tutor-assistent-web/src/lib/components/BaseLayout.tsx new file mode 100644 index 0000000..59dcf9b --- /dev/null +++ b/tutor-assistent-web/src/lib/components/BaseLayout.tsx @@ -0,0 +1,13 @@ +import {VStack} from "./flex-layout.tsx"; + +interface Props { + +} + +export function BaseLayout({}: Props) { + return ( + + + + ) +} diff --git a/tutor-assistent-web/src/lib/components/ColumnLayout.tsx b/tutor-assistent-web/src/lib/components/ColumnLayout.tsx new file mode 100644 index 0000000..8fd37e6 --- /dev/null +++ b/tutor-assistent-web/src/lib/components/ColumnLayout.tsx @@ -0,0 +1,131 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +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' + +type WidthColumnCount = { [width: number]: number } + +interface Props { + columnCounts: WidthColumnCount | number + values: T[] + render: (value: T) => React.ReactNode + fill: 'vertical' | 'horizontal' + spacing?: number +} + +export function ColumnLayout({ columnCounts, fill, values, render, spacing }: Props) { + + if (isNotPresent(spacing)) spacing = 0 + + const wrapperRef = useRef(null) + const [wrapperWidth, setWrapperWidth] = useState(0) + + const items = useMemo(() => { + + if (fill === 'vertical') { + return getFilledVertical() + } else { + return getFilledHorizontal() + } + + }, [getCount(), values]) + + + function getKey() { + const widthColumnCounts = columnCounts as WidthColumnCount + + const keys = Object.keys(widthColumnCounts).map(it => stringToNumber(it)).sort(byNumberReverse) + + for (const key of keys) { + if (key < wrapperWidth) return key + } + + return last(keys) + } + + function getCount() { + if (typeof columnCounts === 'number') return columnCounts + return columnCounts[getKey()!] + } + + function getFilledVertical() { + const count = getCount() + const result: T[][] = [] + + for (let i = 0; i < count; i++) { + result.push([]) + } + + let currentColumn = 0 + const itemsPerColumn = Math.ceil(values.length / count) + + for (let i = 0; i < values.length; i++) { + result[currentColumn].push(values[i]) + + if (result[currentColumn].length >= itemsPerColumn) { + currentColumn++ + } + } + + return result + } + + function getFilledHorizontal() { + const count = getCount() + const result: T[][] = [] + + for (let i = 0; i < count; i++) { + result.push([]) + } + + for (let i = 0; i < values.length; i++) { + result[i % count].push(values[i]) + } + + return result + } + + + useEffect(() => { + const resizeHandler = () => { + setWrapperWidth(wrapperRef.current?.offsetWidth ?? 0) + } + + window.addEventListener('resize', resizeHandler) + + return () => { + window.removeEventListener('resize', resizeHandler) + } + }, []) + + useEffect(() => { + setWrapperWidth(wrapperRef.current?.offsetWidth ?? 0) + }, [wrapperRef.current]) + + + return ( + + + { + items.map((item, index) => ( + + + + { + item.map((value, index) => ( + + {render(value)} + + )) + } + + + + + )) + } + + + ) +} diff --git a/tutor-assistent-web/src/lib/components/Scroller.tsx b/tutor-assistent-web/src/lib/components/Scroller.tsx new file mode 100644 index 0000000..876ba63 --- /dev/null +++ b/tutor-assistent-web/src/lib/components/Scroller.tsx @@ -0,0 +1,37 @@ +import { Box } from '@mui/joy' +import { ReactNode, useEffect, useRef } from 'react' +import { isNotPresent } from '../utils/utils.ts' +import { empty } from '../utils/array-utils.ts' + +interface Props { + children: ReactNode + padding?: number + scrollToBottomOnChange?: unknown[] +} + +export function Scroller({ children, padding, scrollToBottomOnChange }: Props) { + if (isNotPresent(scrollToBottomOnChange)) scrollToBottomOnChange = [] + + const scrollerRef = useRef(null) + + useEffect(() => { + if (isNotPresent(scrollToBottomOnChange) || empty(scrollToBottomOnChange)) return + + const scroller = scrollerRef.current + if (isNotPresent(scroller)) return + scroller.scrollTop = scroller.scrollHeight + }, scrollToBottomOnChange) + + + return ( + + + {children} + + + ) +} diff --git a/tutor-assistent-web/src/lib/components/StarRater.tsx b/tutor-assistent-web/src/lib/components/StarRater.tsx new file mode 100644 index 0000000..00fc67f --- /dev/null +++ b/tutor-assistent-web/src/lib/components/StarRater.tsx @@ -0,0 +1,52 @@ +import { Row } from './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' + +interface Props { + max: number + rating: number + onSelect: (rating: number) => void +} + +export function StarRater({ max, rating, onSelect }: Props) { + const [preview, setPreview] = useState() + + function isFilled(index: number) { + return index + 1 <= (isPresent(preview) ? preview : rating) + } + + function handleSelection(index: number) { + onSelect(index + 1) + } + + return ( + + {range(0, max).map((_, index) => ( + handleSelection(index)} + onMouseOver={() => setPreview(index + 1)} + onMouseOut={() => setPreview(undefined)} + /> + ))} + + ) +} + +interface RatingStarProps { + isFilled: boolean + onClick: () => void + onMouseOver: () => void + onMouseOut: () => void +} + +function RatingStar({ isFilled, onClick, onMouseOver, onMouseOut }: RatingStarProps) { + return ( + isFilled + ? () + : () + ) +} \ No newline at end of file diff --git a/tutor-assistent-web/src/lib/components/flex-layout.tsx b/tutor-assistent-web/src/lib/components/flex-layout.tsx new file mode 100644 index 0000000..ecb48b4 --- /dev/null +++ b/tutor-assistent-web/src/lib/components/flex-layout.tsx @@ -0,0 +1,42 @@ +import { Box, Stack, styled } from '@mui/joy' + +export const VStack = styled(Stack)` + margin: 0 !important; + flex-direction: column; + width: 100%; + height: 100%; + overflow-y: hidden; +` + +export const HStack = styled(Stack)` + margin: 0 !important; + flex-direction: row; + width: 100%; + height: 100%; + overflow-y: hidden; +` + +export const Column = styled(Stack)` + margin: 0 !important; + flex-direction: column; + height: 100%; + overflow-y: hidden; +` + +export const Row = styled(Stack)` + margin: 0 !important; + flex-direction: row; + width: 100%; + align-items: center; +` + +export const MainContent = styled(Box)` + width: 100%; + height: 100%; + flex: 1; + overflow: hidden; +` + +export const Spacer = styled('span')` + flex: 1; +` diff --git a/tutor-assistent-web/src/lib/hooks/useCallOnce.ts b/tutor-assistent-web/src/lib/hooks/useCallOnce.ts new file mode 100644 index 0000000..72e1a51 --- /dev/null +++ b/tutor-assistent-web/src/lib/hooks/useCallOnce.ts @@ -0,0 +1,10 @@ +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-assistent-web/src/lib/hooks/useInstantState.ts b/tutor-assistent-web/src/lib/hooks/useInstantState.ts new file mode 100644 index 0000000..e52946a --- /dev/null +++ b/tutor-assistent-web/src/lib/hooks/useInstantState.ts @@ -0,0 +1,18 @@ +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-assistent-web/src/lib/hooks/useOnChange.ts b/tutor-assistent-web/src/lib/hooks/useOnChange.ts new file mode 100644 index 0000000..8805db2 --- /dev/null +++ b/tutor-assistent-web/src/lib/hooks/useOnChange.ts @@ -0,0 +1,26 @@ +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-assistent-web/src/lib/types.ts b/tutor-assistent-web/src/lib/types.ts new file mode 100644 index 0000000..d988669 --- /dev/null +++ b/tutor-assistent-web/src/lib/types.ts @@ -0,0 +1,12 @@ +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-assistent-web/src/lib/utils/array-utils.ts b/tutor-assistent-web/src/lib/utils/array-utils.ts new file mode 100644 index 0000000..44dc08e --- /dev/null +++ b/tutor-assistent-web/src/lib/utils/array-utils.ts @@ -0,0 +1,144 @@ +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-assistent-web/src/lib/utils/math-utils.ts b/tutor-assistent-web/src/lib/utils/math-utils.ts new file mode 100644 index 0000000..81c164b --- /dev/null +++ b/tutor-assistent-web/src/lib/utils/math-utils.ts @@ -0,0 +1,10 @@ +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 +} diff --git a/tutor-assistent-web/src/lib/utils/scroll-utils.ts b/tutor-assistent-web/src/lib/utils/scroll-utils.ts new file mode 100644 index 0000000..d47ac42 --- /dev/null +++ b/tutor-assistent-web/src/lib/utils/scroll-utils.ts @@ -0,0 +1,22 @@ +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-assistent-web/src/lib/utils/ui-utils.ts b/tutor-assistent-web/src/lib/utils/ui-utils.ts new file mode 100644 index 0000000..e96f06c --- /dev/null +++ b/tutor-assistent-web/src/lib/utils/ui-utils.ts @@ -0,0 +1,34 @@ +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-assistent-web/src/lib/utils/utils.ts b/tutor-assistent-web/src/lib/utils/utils.ts new file mode 100644 index 0000000..afc0b39 --- /dev/null +++ b/tutor-assistent-web/src/lib/utils/utils.ts @@ -0,0 +1,47 @@ +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-assistent-web/src/main.tsx b/tutor-assistent-web/src/main.tsx new file mode 100644 index 0000000..15a0d50 --- /dev/null +++ b/tutor-assistent-web/src/main.tsx @@ -0,0 +1,34 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +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' + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/tutor-assistent-web/src/modules/calendar/calendar-model.ts b/tutor-assistent-web/src/modules/calendar/calendar-model.ts new file mode 100644 index 0000000..41c2f93 --- /dev/null +++ b/tutor-assistent-web/src/modules/calendar/calendar-model.ts @@ -0,0 +1,6 @@ +export interface CalendarEntry { + title: string + date: string + time?: string + isCurrentDate: boolean +} diff --git a/tutor-assistent-web/src/modules/calendar/components/CalendarBar.tsx b/tutor-assistent-web/src/modules/calendar/components/CalendarBar.tsx new file mode 100644 index 0000000..ff33462 --- /dev/null +++ b/tutor-assistent-web/src/modules/calendar/components/CalendarBar.tsx @@ -0,0 +1,59 @@ +import { Header } from '../../../common/components/Header.tsx' +import { MainContent, Row } from '../../../lib/components/flex-layout.tsx' +import { Button, IconButton } from '@mui/joy' +import { Cached, Logout } from '@mui/icons-material' +import { Bar } from '../../../common/components/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 { CalendarTable } from './CalendarTable.tsx' + +interface Props { + +} + +export function CalendarBar({}: Props) { + 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-assistent-web/src/modules/calendar/components/CalendarTable.tsx b/tutor-assistent-web/src/modules/calendar/components/CalendarTable.tsx new file mode 100644 index 0000000..1815776 --- /dev/null +++ b/tutor-assistent-web/src/modules/calendar/components/CalendarTable.tsx @@ -0,0 +1,38 @@ +import { isPresent } from '../../../lib/utils/utils.ts' +import React from 'react' +import { CalendarEntry } from '../calendar-model.ts' +import { styled } from '@mui/joy' +import classNames from 'classnames' + +interface Props { + calendarEntries: CalendarEntry[] +} + +export function CalendarTable({ calendarEntries }: Props) { + return ( + + + + + + + + + {calendarEntries.map((entry, index) => ( + + + + + ))} + +
DatumEreignis
+ {entry.date} {isPresent(entry.time) && <>
({entry.time} Uhr)} +
{entry.title}
+ ) +} + +const TableRow = styled('tr')` + &.current { + font-weight: bold; + } +` diff --git a/tutor-assistent-web/src/modules/calendar/useCalendar.ts b/tutor-assistent-web/src/modules/calendar/useCalendar.ts new file mode 100644 index 0000000..0a573d8 --- /dev/null +++ b/tutor-assistent-web/src/modules/calendar/useCalendar.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react' +import { useAuth } from '../../app/auth/useAuth.ts' +import { apiBaseUrl } from '../../app/base.ts' +import { CalendarEntry } from './calendar-model.ts' +import { format } from 'date-fns' +import { useTranslation } from 'react-i18next' + +export function useCalendar() { + const { t } = useTranslation() + const { getAuthHttp } = useAuth() + + const [calendarEntries, setCalendarEntries] = useState([]) + + + useEffect(() => { + loadCalendarEntries() + }, []) + + async function loadCalendarEntries() { + const currentDate = format(new Date(), 'dd.MM.yyyy') + const url = `${apiBaseUrl}/calendar?currentDate=${currentDate}¤tTitle=${t('Today')}` + const response = await getAuthHttp().get(url) + setCalendarEntries(response.data) + } + + async function loadNewCalendar() { + await getAuthHttp().post(`${apiBaseUrl}/calendar`) + loadCalendarEntries() + console.log('New Info loaded') + } + + return { + calendarEntries, + loadNewCalendar, + } +} diff --git a/tutor-assistent-web/src/modules/chat/ChatPage.tsx b/tutor-assistent-web/src/modules/chat/ChatPage.tsx new file mode 100644 index 0000000..9965a68 --- /dev/null +++ b/tutor-assistent-web/src/modules/chat/ChatPage.tsx @@ -0,0 +1,100 @@ +import React, { FormEvent, useEffect, useRef } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { isNotPresent, isPresent } from '../../lib/utils/utils.ts' +import { useChatManager } from './hooks/useChatManager.ts' +import { useTranslation } from 'react-i18next' +import { useChat } from './hooks/useChat.ts' +import { useAsyncActionTrigger } from './hooks/useAsyncActionTrigger.ts' +import { MainContent, Row, VStack } from '../../lib/components/flex-layout.tsx' +import { Divider, Textarea } from '@mui/joy' +import { ChatDetails } from './components/details/ChatDetails.tsx' +import { ChatOverview } from './components/overview/ChatOverview.tsx' + + +export function ChatPage() { + const { t } = useTranslation() + const navigate = useNavigate() + + const inputRef = useRef(null) + + const chatId = useParams().chatId + const { createChat } = useChatManager() + const { chat, sendMessage, isLoading } = useChat(chatId) + const [isSending, sendMessageAction] = useAsyncActionTrigger( + handleSend, + () => isPresent(chat) && chat.id === chatId, + [chat?.id], + ) + + + useEffect(() => { + inputRef.current?.focus() + }, [isSending, isLoading, inputRef.current, chatId]) + + async function handleFormSubmit(e?: FormEvent) { + e?.preventDefault() + + const input = inputRef.current + if (isNotPresent(input) || input.value === '') return + + await handleCreateChat() + sendMessageAction() + } + + async function handleCreateChat() { + if (isNotPresent(chatId)) { + const chat = await createChat() + navigate(`/chats/${chat.id}`) + } + } + + async function handleSend() { + const input = inputRef.current + if (isNotPresent(input) || input.value === '') return + + const message = input.value + if (message !== '') { + await sendMessage(message) + input.value = '' + } + } + + async function handleInput(e: React.KeyboardEvent) { + if (e.ctrlKey && e.key === 'Enter') { + e.preventDefault() + handleFormSubmit() + } + } + + return ( + + + { + isPresent(chatId) + ? + : + } + + + + + +
+