diff --git a/.dockerignore b/.dockerignore index 6e9e9362..3dc17691 100644 --- a/.dockerignore +++ b/.dockerignore @@ -332,3 +332,6 @@ # performance testing sandbox **/sandbox + +# Vite +# **/build/ huh, also ignored when building inside dockerfile, thought it was only during COPY... \ No newline at end of file diff --git a/Common.Web/Common.Web.csproj b/Common.Web/Common.Web.csproj index 5292d0b3..2cc37ac1 100644 --- a/Common.Web/Common.Web.csproj +++ b/Common.Web/Common.Web.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable @@ -9,8 +9,7 @@ - - + diff --git a/Common.Web/Extensions.cs b/Common.Web/Extensions.cs new file mode 100644 index 00000000..b690fad5 --- /dev/null +++ b/Common.Web/Extensions.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Hosting; + +namespace Common.Web +{ + public static class Extensions + { + public static bool HasEnvironmentPart(this IHostEnvironment hostEnvironment, string environmentPart) + { + ArgumentNullException.ThrowIfNull(hostEnvironment); + return hostEnvironment.EnvironmentName.Split('.') + .Any(part => string.Equals(environmentPart, part, StringComparison.OrdinalIgnoreCase)); + } + public static bool HasDevelopmentEnvironment(this IHostEnvironment hostEnvironment) + => hostEnvironment.HasEnvironmentPart(Environments.Development); + } +} diff --git a/Common.Web/ServiceConfiguration.cs b/Common.Web/ServiceConfiguration.cs index 332e0570..e8822438 100644 --- a/Common.Web/ServiceConfiguration.cs +++ b/Common.Web/ServiceConfiguration.cs @@ -10,15 +10,15 @@ namespace Common.Web { public static class ServiceConfiguration { - public static void ConfigureProcessingPipelineServices(IServiceCollection services, IEnumerable pluginModules) + public static void ConfigureProcessingPipelineServices(IServiceCollection services, IConfiguration config, IEnumerable pluginModules) { services.AddSingleton(); //(sp => new TableClientFactory("vektor") - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); foreach (var plugin in pluginModules) - plugin.ConfigureServices(services); + plugin.ConfigureServices(services, config); } public static void ConfigurePlugins(IApplicationBuilder app, IEnumerable pluginModules) @@ -36,7 +36,8 @@ public static void ConfigureApplicationInsights(IApplicationBuilder app, IConfig if (aiConn == "SECRET" || aiConn == string.Empty) { if (isDevelopment == false) - throw new ArgumentException("InstrumentationKey not set"); + Console.WriteLine($"Warning: InstrumentationKey not set ({aiConn})"); + //throw new ArgumentException("InstrumentationKey not set"); } else { diff --git a/Common.Web/TypedConfiguration.cs b/Common.Web/TypedConfiguration.cs index 96a932b8..3954851a 100644 --- a/Common.Web/TypedConfiguration.cs +++ b/Common.Web/TypedConfiguration.cs @@ -21,55 +21,101 @@ public class TypedConfiguration // https://referbruv.com/blog/posts/working-with-options-pattern-in-aspnet-core-the-complete-guide var appSettings = new T(); - config.GetSection(sectionKey).Bind(appSettings); - services.AddSingleton(appSettings.GetType(), appSettings!); + //config.GetSection(sectionKey).Bind(appSettings); + //services.AddSingleton(appSettings.GetType(), appSettings!); - RecurseBind(appSettings, services, config); + //RecurseBind(appSettings, services, config); + XBind(appSettings, services, config.GetSection(typeof(T).Name)); + // https://kaylumah.nl/2021/11/29/validated-strongly-typed-ioptions.html + // If we want to inject IOptions instead of just Type, this is needed: https://stackoverflow.com/a/61157181 services.ConfigureOptions(instance) + //services.Configure(config.GetSection("AceKnowledge")); - // https://kaylumah.nl/2021/11/29/validated-strongly-typed-ioptions.html - // If we want to inject IOptions instead of just Type, this is needed: https://stackoverflow.com/a/61157181 services.ConfigureOptions(instance) - //services.Configure(config.GetSection("AceKnowledge")); - - return appSettings; + return appSettings; } - private static void RecurseBind(object appSettings, IServiceCollection services, IConfiguration config) + private static void XBind(object setting, IServiceCollection services, IConfiguration config) + { + config.Bind(setting); + Rec(setting); + void Rec(object s) + { + services.AddSingleton(s.GetType(), s); + var props = s.GetType() + .GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); + foreach (var item in props) + { + if (item.PropertyType == typeof(string) || item.PropertyType.IsPrimitive) + { } + else if (item.PropertyType.IsAssignableTo(typeof(System.Collections.IEnumerable)) + && item.PropertyType.IsGenericType) + { } + else + { + var v = item.GetValue(s); + if (v != null) + Rec(v); + } + } + } + } + + private static void RecurseBind(object appSettings, IServiceCollection services, IConfiguration config) { - var props = appSettings.GetType() - .GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public) - .Where(o => !o.PropertyType.IsSealed); // TODO: (low) better check than IsSealed (also unit test) + var props = appSettings.GetType() + .GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public) + ; //.Where(o => !o.PropertyType.IsSealed); // TODO: (low) better check than IsSealed (also unit test) - foreach (var prop in props) - { - var instance = prop.GetValue(appSettings); - //config.GetSection(prop.Name).Bind(instance); - services.AddSingleton(instance!.GetType(), instance!); + foreach (var prop in props) + { + var instance = prop.GetValue(appSettings); + if (instance == null) + { + var isNullable = Nullable.GetUnderlyingType(prop.PropertyType) != null; + if (isNullable) + continue; + if (prop.GetCustomAttributes(typeof(System.Runtime.CompilerServices.NullableAttribute), true).Any()) + continue; + throw new Exception($"Null value for non-nullable property '{prop.Name}'"); + } + //config.GetSection(prop.Name).Bind(instance); - RecurseBind(instance, services, config); + if (instance is System.Collections.IList lst) + { + foreach (var item in lst) + { + services.AddSingleton(item.GetType(), item); + //RecurseBind(item, services, config); + } + } + else + { + services.AddSingleton(instance!.GetType(), instance!); + RecurseBind(instance, services, config); + } - //var asOptions = Microsoft.Extensions.Options.Options.Create(instance); - //services.ConfigureOptions(instance); + //var asOptions = Microsoft.Extensions.Options.Options.Create(instance); + //services.ConfigureOptions(instance); - // Execute validation (if available) - var validatorType = instance.GetType().Assembly.GetTypes() - .Where(t => - { - var validatorInterface = t.GetInterfaces().SingleOrDefault(o => - o.IsGenericType && o.GetGenericTypeDefinition() == typeof(Microsoft.Extensions.Options.IValidateOptions<>)); - return validatorInterface != null && validatorInterface.GenericTypeArguments.Single() == instance.GetType(); - } - ).FirstOrDefault(); - if (validatorType != null) - { - var validator = Activator.CreateInstance(validatorType); - var m = validatorType.GetMethod("Validate"); - var result = (Microsoft.Extensions.Options.ValidateOptionsResult?)m?.Invoke(validator, new object[] { "", instance }); - if (result!.Failed) - { - throw new Exception($"{validatorType.Name}: {result.FailureMessage}"); - } - } - } - } + // Execute validation (if available) + var validatorType = instance.GetType().Assembly.GetTypes() + .Where(t => + { + var validatorInterface = t.GetInterfaces().SingleOrDefault(o => + o.IsGenericType && o.GetGenericTypeDefinition() == typeof(Microsoft.Extensions.Options.IValidateOptions<>)); + return validatorInterface != null && validatorInterface.GenericTypeArguments.Single() == instance.GetType(); + } + ).FirstOrDefault(); + if (validatorType != null) + { + var validator = Activator.CreateInstance(validatorType); + var m = validatorType.GetMethod("Validate"); + var result = (Microsoft.Extensions.Options.ValidateOptionsResult?)m?.Invoke(validator, new object[] { "", instance }); + if (result!.Failed) + { + throw new Exception($"{validatorType.Name}: {result.FailureMessage}"); + } + } + } + } } } diff --git a/Common/Class1.cs b/Common/Class1.cs index 351f9573..aa8f95d0 100644 --- a/Common/Class1.cs +++ b/Common/Class1.cs @@ -2,6 +2,69 @@ namespace Common.LLM { + public interface ILlmServiceFactory + { + ILlmService? Get(string? preferredModelName = null, string? preferredService = null); + + public ILlmService GetOrDefault(ILlmService defaultValue, string? preferredModelName = null, string? preferredService = null) + => Get(preferredModelName, preferredService) ?? defaultValue; + + public ILlmService GetOrThrow(string? preferredModelName = null, string? preferredService = null) + { + var result = Get(preferredModelName, preferredService); + if (result == null) + throw new Exception($"Model='{preferredModelName}' service='{preferredService}' not registered"); + return result; + } + } + + public class LlmServiceFactory : ILlmServiceFactory + { + private readonly List models; + private readonly List serviceConfigs; + private readonly Func httpClientFactory; + + public LlmServiceFactory(IEnumerable models, IEnumerable serviceConfigs, Func httpClientFactory) + { + this.models = models.ToList(); + this.serviceConfigs = serviceConfigs.ToList(); + this.httpClientFactory = httpClientFactory; + } + + public ILlmService? Get(string? preferredModelName = null, string? preferredService = null) + { + LlmModelSpecification? model = null; + LlmServiceSpecification? service = null; + + if (preferredModelName != null) + model = models.FirstOrDefault(o => o.Name.Equals(preferredModelName ?? "", StringComparison.OrdinalIgnoreCase)); + else if (preferredService != null) + service = serviceConfigs.First(o => o.Name == preferredService); + + if (model == null && service == null) + { + model = models.FirstOrDefault(); + if (model == null) + return null; + } + + if (model == null) + model = models.FirstOrDefault(o => o.AvailableOnServices.Contains(service.Name)); + else if (service == null) + service = serviceConfigs.FirstOrDefault(o => model.AvailableOnServices.Contains(o.Name)); + + if (model == null || service == null || model.AvailableOnServices.Contains(service.Name) == false) + return null; + + return service.Name switch + { + "Azure" => new AzureOpenAIRESTService(model, service), + //"AzureREST" => new AzureRESTLlmService(model, service, httpClientFactory), + //"Berget" => new BergetLlmService(model, service, httpClientFactory), + _ => throw new NotImplementedException($"Service {service.Name}") + }; + } + } // Temp until common nuget public interface ILlmService { @@ -12,7 +75,6 @@ public interface ILlmService } public record LlmContentReceivedData(string ContentSinceLast, int TotalLength); - public class LlmModelSpecification { public List AvailableOnServices { get; set; } = new(); @@ -45,6 +107,17 @@ public class LlmResult public string Model { get; set; } = ""; } + public class NullLlmService : ILlmService + { + public LlmModelSpecification ModelSpecification => new LlmModelSpecification(); + + public int CountTokens(string text) => 0; + + public Task Invoke(string prompt, LlmCallOptions? options = null) + => Task.FromResult((LlmResult?)new LlmResult { Completion = "" }); + } + + public class AzureOpenAIRESTService : ILlmService { private readonly Config config; diff --git a/Common/Common.csproj b/Common/Common.csproj index d8110dae..80381397 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -1,6 +1,6 @@  - net9.0 + net10.0 enable enable diff --git a/Directory.Packages.props b/Directory.Packages.props index bf88e4b1..7baf90f4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,64 +5,73 @@ $(NoWarn);NU1507 - + - - - - - - + + + + + + - + + + - - - - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - + - - - + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a2094c76..251ef8de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ # Build the runtime image +# podman build -t trainingapi . -f Dockerfile FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base WORKDIR /app # Expose the port the app runs on @@ -33,7 +34,7 @@ WORKDIR /app COPY --from=publish /app/publish . ENV ASPNETCORE_HTTP_PORTS=80 # Define the startup command -ENTRYPOINT ["dotnet", "TrainingApi.dll"] +ENTRYPOINT ["dotnet", "TrainingApi.dll", "--environment=Docker"] # FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build diff --git a/Dockerfile.web b/Dockerfile.web index dfda06a2..1e8e734a 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -1,26 +1,50 @@ -FROM node:24-alpine +FROM node:24-alpine as build + +# podman build -t adminapp . -f Dockerfile.web +# podman start adminapp -p 5171:5171 --name adminapp-1 # WORKDIR ProblemSource/AdminApp -WORKDIR /app - -# nothing copied? COPY ProblemSource/AdminApp/* / -# works, but too much copied? -COPY ProblemSource/AdminApp/** / -# COPY ./ProblemSource/AdminApp/** ./ -# COPY ./ProblemSource/AdminApp/package.json ./ -# COPY ./ProblemSource/AdminApp/src/** ./src/ -# COPY package*.json ./ - -# RUN find -maxdepth 2 -ls -# no output.. +WORKDIR /usr/src/app + +COPY ProblemSource/AdminApp/package.json . + +RUN echo "First copy result" RUN ls -ltR RUN npm install +# apk add curl + +COPY ProblemSource/AdminApp/ . + +RUN echo "Second copy result" +RUN ls -ltR -COPY . . +ENV NODE_ENV=docker RUN npm run build -EXPOSE 5173 +# COPY ProblemSource/AdminApp/mime.types /usr/src/app/build/ +COPY ProblemSource/AdminApp/nginx-site.conf /usr/src/app/build/ + + +# Stage 2 +# Need to > podman pull docker.io/library/nginx:latest +FROM nginx:latest +# COPY --from=build /usr/src/app/build /usr/share/nginx/html +COPY --from=build /usr/src/app/build /usr/share/nginx/html/admin +COPY --from=build /usr/src/app/build/nginx-site.conf /etc/nginx/conf.d/ + +#FROM node:24-alpine +#COPY --from=build /usr/src/app/build /usr/share/html +#RUN npm install -g http-server + +EXPOSE 5171 +ENV PORT=5171 + +# CMD http-server /usr/share/html --proxy http://localhost:5171/index.html? --mimetypes /usr/share/html/mime.types +# https://stackoverflow.com/questions/61106423/how-to-put-a-svelte-app-in-a-docker-container +# CMD [ "http-server", "build", "--proxy", "http://localhost:5171?"] +# --mimetypes mime.types -CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file +# cd ~/source/repos/JWMB/WebProcessor +# podman build -t adminapp . -f Dockerfile.web \ No newline at end of file diff --git a/Dockerfile.web-old b/Dockerfile.web-old new file mode 100644 index 00000000..bebce751 --- /dev/null +++ b/Dockerfile.web-old @@ -0,0 +1,60 @@ +FROM node:24-alpine + +# podman build -t adminapp . -f Dockerfile.web +# podman start adminapp -p 5171:5171 --name adminapp-1 + +# WORKDIR ProblemSource/AdminApp +WORKDIR /app + +# nothing copied? COPY ProblemSource/AdminApp/* / +# works, but too much copied? +# COPY ProblemSource/AdminApp/** / +# RUN rm -rf ./ProblemSource/AdminApp/build/_app/ + +COPY ProblemSource/AdminApp/package.json . + +# COPY ./ProblemSource/AdminApp/** ./ +# COPY ./ProblemSource/AdminApp/package.json ./ +# COPY ./ProblemSource/AdminApp/src/** ./src/ +# COPY package*.json ./ + +# RUN find -maxdepth 2 -ls +# no output.. +RUN echo "First copy result" +RUN ls -ltR + +RUN npm install +RUN npm install -g http-server +# apk add curl + +# RUN rm -rf ./ProblemSource/AdminApp/build/_app/ +# COPY . . +# COPY ProblemSource/AdminApp/ . +# COPY ProblemSource/AdminApp/ ProblemSource/AdminApp/ +# RUN rm -rf ./ProblemSource/AdminApp/build/_app/ + +COPY ProblemSource/AdminApp/ . + +RUN echo "Second copy result" +RUN ls -ltR + +EXPOSE 5171 +ENV PORT=5171 +# ENV PUBLIC_LOCAL_SERVER_PATH=https://kistudysync.azurewebsites.net + +# EXPOSE 80 +# ENV PORT=80 + +RUN npm run build + + +# https://stackoverflow.com/questions/61106423/how-to-put-a-svelte-app-in-a-docker-container +CMD [ "http-server", "build", "--proxy", "http://localhost:5171?"] +# CMD [ "http-server", "ProblemSource/AdminApp/build", "--proxy", "http://localhost:5171?"] +# CMD [ "http-server", "ProblemSource/AdminApp/build", "--proxy", "http://localhost:5171/index.html?"] +# CMD [ "http-server", "ProblemSource/AdminApp/build" ] + +# CMD ["npm", "run", "start"] +# CMD ["npm", "run", "dev", "--", "--host"] + +# podman build -t adminapp . -f Dockerfile.web \ No newline at end of file diff --git a/EmailServices/EmailServices.csproj b/EmailServices/EmailServices.csproj index c37e7ac5..6a3c0401 100644 --- a/EmailServices/EmailServices.csproj +++ b/EmailServices/EmailServices.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable diff --git a/ML.AzureFunction/ML.AzureFunction.csproj b/ML.AzureFunction/ML.AzureFunction.csproj index 287e855c..32549c50 100644 --- a/ML.AzureFunction/ML.AzureFunction.csproj +++ b/ML.AzureFunction/ML.AzureFunction.csproj @@ -20,7 +20,7 @@ - + diff --git a/ML.Helpers/ML.Helpers.csproj b/ML.Helpers/ML.Helpers.csproj index fa7a2386..0fa851b0 100644 --- a/ML.Helpers/ML.Helpers.csproj +++ b/ML.Helpers/ML.Helpers.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable diff --git a/MLTools/ML.Dynamic.csproj b/MLTools/ML.Dynamic.csproj index 9d558492..46153d1c 100644 --- a/MLTools/ML.Dynamic.csproj +++ b/MLTools/ML.Dynamic.csproj @@ -1,11 +1,12 @@  - net9.0 + net10.0 enable enable + diff --git a/MLTools/MLDynamicPredict.cs b/MLTools/MLDynamicPredict.cs index 8e1a24c6..4b3cd8ca 100644 --- a/MLTools/MLDynamicPredict.cs +++ b/MLTools/MLDynamicPredict.cs @@ -26,7 +26,7 @@ public MLDynamicPredict(DataViewSchema schema, ITransformer model, ColumnInfo co var ctx = new MLContext(seed: 0); var model = ctx.Model.Load(modelPath, out DataViewSchema schema); - var type = MLDynamicPredict.CreateType(schema); + var type = CreateType(schema); var instance = DynamicTypeFactory.CreateInstance(type, values); var predictor = new MLDynamicPredict(schema, model, colInfo); diff --git a/Organization.Tests/Organization.Tests.csproj b/Organization.Tests/Organization.Tests.csproj index c8bf9b41..9fda1c88 100644 --- a/Organization.Tests/Organization.Tests.csproj +++ b/Organization.Tests/Organization.Tests.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable false diff --git a/Organization/Organization.csproj b/Organization/Organization.csproj index bcbf88ca..b46c4338 100644 --- a/Organization/Organization.csproj +++ b/Organization/Organization.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable diff --git a/PluginModuleBase/IDataSink.cs b/PluginModuleBase/IDataSink.cs index bce47d27..3b0afcdc 100644 --- a/PluginModuleBase/IDataSink.cs +++ b/PluginModuleBase/IDataSink.cs @@ -3,5 +3,10 @@ public interface IDataSink { Task Log(string uuid, object data); - } + } + + public class NullDataSink : IDataSink + { + public Task Log(string uuid, object data) => Task.CompletedTask; + } } diff --git a/PluginModuleBase/IPluginModule.cs b/PluginModuleBase/IPluginModule.cs index 50de9654..92d0c01a 100644 --- a/PluginModuleBase/IPluginModule.cs +++ b/PluginModuleBase/IPluginModule.cs @@ -1,11 +1,12 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace PluginModuleBase { public interface IPluginModule { - void ConfigureServices(IServiceCollection services); - void Configure(IApplicationBuilder app); + void ConfigureServices(IServiceCollection services, IConfiguration config); + void Configure(IApplicationBuilder app); } } diff --git a/PluginModuleBase/PluginModuleBase.csproj b/PluginModuleBase/PluginModuleBase.csproj index 0c569973..4a238f73 100644 --- a/PluginModuleBase/PluginModuleBase.csproj +++ b/PluginModuleBase/PluginModuleBase.csproj @@ -1,12 +1,12 @@  - net9.0 + net10.0 enable enable + - \ No newline at end of file diff --git a/ProblemSource/AdminApp/.env b/ProblemSource/AdminApp/.env index 556a9d7c..a3429e9b 100644 --- a/ProblemSource/AdminApp/.env +++ b/ProblemSource/AdminApp/.env @@ -1,3 +1,8 @@ VITE_HTTPS=true # Public -PUBLIC_LOCAL_SERVER_PATH="https://kistudysync.azurewebsites.net" +#PUBLIC_LOCAL_SERVER_PATH=http://localhost:9090 +PUBLIC_LOCAL_SERVER_PATH=https://curricullm.net +#PUBLIC_LOCAL_SERVER_PATH="https://kistudysync.azurewebsites.net" +PUBLIC_HTTPS=true +PUBLIC_PORT=5171 +PUBLIC_VITE_TEST1=abc diff --git a/ProblemSource/AdminApp/mime.types b/ProblemSource/AdminApp/mime.types new file mode 100644 index 00000000..933cb5d7 --- /dev/null +++ b/ProblemSource/AdminApp/mime.types @@ -0,0 +1,3 @@ +# This file is an example of the Apache .types file format for describing mime-types. +# Other example: http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types +text/javascript js mjs \ No newline at end of file diff --git a/ProblemSource/AdminApp/nginx-site.conf b/ProblemSource/AdminApp/nginx-site.conf new file mode 100644 index 00000000..d0bca32c --- /dev/null +++ b/ProblemSource/AdminApp/nginx-site.conf @@ -0,0 +1,78 @@ +# user nginx; +# worker_processes auto; + +# error_log /var/log/nginx/error.log notice; +# pid /run/nginx.pid; + + +# events { +# worker_connections 1024; +# } + + +#http { +# include /etc/nginx/mime.types; +# default_type application/octet-stream; + +# log_format main '$remote_addr - $remote_user [$time_local] "$request" ' +# '$status $body_bytes_sent "$http_referer" ' +# '"$http_user_agent" "$http_x_forwarded_for"'; +# log_format dbg '$remote_addr - $remote_user [$time_local] "$request" ' +# '$status $body_bytes_sent "$http_referer" ' +# 'file_path: "$request_filename"'; + +# access_log /var/log/nginx/access.log main; + +# sendfile on; +# #tcp_nopush on; + +# keepalive_timeout 65; + +# #gzip on; + +# include /etc/nginx/conf.d/*.conf; +#} + +server { + listen 5171; + # listen 80; + # listen [::]:80; + server_name localhost; + + # access_log /var/log/nginx/host.access.log dbg; + + # location /admin/ { + # rewrite ^/admin(.*)$ $1 last; + # } + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri /admin/index.html; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + # deny access to .htaccess files, if Apache's document root concurs with nginx's one + #location ~ /\.ht { + # deny all; + #} +} + +# server { +# listen 80; + +# location / { +# proxy_pass http://myapp1; +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection 'upgrade'; +# proxy_set_header Host $host; +# proxy_cache_bypass $http_upgrade; +# } +# } \ No newline at end of file diff --git a/ProblemSource/AdminApp/package-lock.json b/ProblemSource/AdminApp/package-lock.json index b0bddf08..31a6e2ac 100644 --- a/ProblemSource/AdminApp/package-lock.json +++ b/ProblemSource/AdminApp/package-lock.json @@ -988,9 +988,9 @@ "license": "MIT" }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1082,9 +1082,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1327,6 +1327,16 @@ "node": ">=6.0.0" } }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -1769,9 +1779,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -1918,9 +1928,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -2011,13 +2021,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -2224,9 +2234,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2287,9 +2297,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -2428,9 +2438,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -2454,9 +2464,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -2475,7 +2485,7 @@ "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2619,12 +2629,13 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -2678,9 +2689,9 @@ } }, "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", + "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", "dev": true, "license": "MIT", "bin": { @@ -2759,9 +2770,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, "license": "ISC", "bin": { @@ -3393,9 +3404,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "license": "MIT", "engines": { "node": ">=8.3.0" diff --git a/ProblemSource/AdminApp/src/apiClient.ts b/ProblemSource/AdminApp/src/apiClient.ts index 9c7ce83b..71f50691 100644 --- a/ProblemSource/AdminApp/src/apiClient.ts +++ b/ProblemSource/AdminApp/src/apiClient.ts @@ -1,10 +1,9 @@ //---------------------- // -// Generated using the NSwag toolchain v13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// Generated using the NSwag toolchain v14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) // //---------------------- -/* tslint:disable */ /* eslint-disable */ // ReSharper disable InconsistentNaming @@ -15,13 +14,13 @@ export class AggregatesClient { constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { this.http = http ? http : window as any; - this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : ""; + this.baseUrl = baseUrl ?? ""; } trainingDayAccount(trainingId: number | undefined): Promise { let url_ = this.baseUrl + "/api/Aggregates/TrainingDayAccount?"; if (trainingId === null) - throw new Error("The parameter 'trainingId' cannot be null."); + throw new globalThis.Error("The parameter 'trainingId' cannot be null."); else if (trainingId !== undefined) url_ += "trainingId=" + encodeURIComponent("" + trainingId) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -58,7 +57,7 @@ export class AggregatesClient { phaseStatistics(trainingId: number | undefined): Promise { let url_ = this.baseUrl + "/api/Aggregates/PhaseStatistics?"; if (trainingId === null) - throw new Error("The parameter 'trainingId' cannot be null."); + throw new globalThis.Error("The parameter 'trainingId' cannot be null."); else if (trainingId !== undefined) url_ += "trainingId=" + encodeURIComponent("" + trainingId) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -100,13 +99,13 @@ export class RelayClient { constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { this.http = http ? http : window as any; - this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : ""; + this.baseUrl = baseUrl ?? ""; } getSyncUrls(uuid: string | undefined): Promise { let url_ = this.baseUrl + "/api/Relay/GetSyncUrls?"; if (uuid === null) - throw new Error("The parameter 'uuid' cannot be null."); + throw new globalThis.Error("The parameter 'uuid' cannot be null."); else if (uuid !== undefined) url_ += "uuid=" + encodeURIComponent("" + uuid) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -153,7 +152,7 @@ export class TestingClient { constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { this.http = http ? http : window as any; - this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : ""; + this.baseUrl = baseUrl ?? ""; } throwException(): Promise { @@ -189,7 +188,7 @@ export class TestingClient { log(level: LogLevel | undefined): Promise { let url_ = this.baseUrl + "/api/Testing/log?"; if (level === null) - throw new Error("The parameter 'level' cannot be null."); + throw new globalThis.Error("The parameter 'level' cannot be null."); else if (level !== undefined) url_ += "level=" + encodeURIComponent("" + level) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -219,6 +218,44 @@ export class TestingClient { } return Promise.resolve(null as any); } + + callPredictor(): Promise { + let url_ = this.baseUrl + "/api/Testing/predict"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/octet-stream" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processCallPredictor(_response); + }); + } + + protected processCallPredictor(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } } export class TrainingsClient { @@ -228,7 +265,7 @@ export class TrainingsClient { constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { this.http = http ? http : window as any; - this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : ""; + this.baseUrl = baseUrl ?? ""; } post(dto: TrainingCreateDto): Promise { @@ -271,11 +308,11 @@ export class TrainingsClient { delete(id: number | undefined, deleteTrainingDataOnly: boolean | undefined): Promise { let url_ = this.baseUrl + "/api/Trainings?"; if (id === null) - throw new Error("The parameter 'id' cannot be null."); + throw new globalThis.Error("The parameter 'id' cannot be null."); else if (id !== undefined) url_ += "id=" + encodeURIComponent("" + id) + "&"; if (deleteTrainingDataOnly === null) - throw new Error("The parameter 'deleteTrainingDataOnly' cannot be null."); + throw new globalThis.Error("The parameter 'deleteTrainingDataOnly' cannot be null."); else if (deleteTrainingDataOnly !== undefined) url_ += "deleteTrainingDataOnly=" + encodeURIComponent("" + deleteTrainingDataOnly) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -375,11 +412,11 @@ export class TrainingsClient { postGroup(dto: TrainingCreateDto, groupName: string | undefined, numTrainings: number | undefined): Promise { let url_ = this.baseUrl + "/api/Trainings/createclass?"; if (groupName === null) - throw new Error("The parameter 'groupName' cannot be null."); + throw new globalThis.Error("The parameter 'groupName' cannot be null."); else if (groupName !== undefined) url_ += "groupName=" + encodeURIComponent("" + groupName) + "&"; if (numTrainings === null) - throw new Error("The parameter 'numTrainings' cannot be null."); + throw new globalThis.Error("The parameter 'numTrainings' cannot be null."); else if (numTrainings !== undefined) url_ += "numTrainings=" + encodeURIComponent("" + numTrainings) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -420,7 +457,7 @@ export class TrainingsClient { getById(id: number): Promise { let url_ = this.baseUrl + "/api/Trainings/{id}"; if (id === undefined || id === null) - throw new Error("The parameter 'id' must be defined."); + throw new globalThis.Error("The parameter 'id' must be defined."); url_ = url_.replace("{id}", encodeURIComponent("" + id)); url_ = url_.replace(/[?&]$/, ""); @@ -453,8 +490,12 @@ export class TrainingsClient { return Promise.resolve(null as any); } - getTemplates(): Promise { - let url_ = this.baseUrl + "/api/Trainings/templates"; + getTemplates(returnOnlyDefaultTemplate: boolean | undefined): Promise { + let url_ = this.baseUrl + "/api/Trainings/templates?"; + if (returnOnlyDefaultTemplate === null) + throw new globalThis.Error("The parameter 'returnOnlyDefaultTemplate' cannot be null."); + else if (returnOnlyDefaultTemplate !== undefined) + url_ += "returnOnlyDefaultTemplate=" + encodeURIComponent("" + returnOnlyDefaultTemplate) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { @@ -623,6 +664,50 @@ export class TrainingsClient { } return Promise.resolve(null as any); } + + getAiAnalysis(trainingId: number | undefined, templateSource: string | undefined, onlyPrompt: boolean | undefined): Promise { + let url_ = this.baseUrl + "/api/Trainings/analysis?"; + if (trainingId === null) + throw new globalThis.Error("The parameter 'trainingId' cannot be null."); + else if (trainingId !== undefined) + url_ += "trainingId=" + encodeURIComponent("" + trainingId) + "&"; + if (templateSource === null) + throw new globalThis.Error("The parameter 'templateSource' cannot be null."); + else if (templateSource !== undefined) + url_ += "templateSource=" + encodeURIComponent("" + templateSource) + "&"; + if (onlyPrompt) + url_ += "onlyPrompt=" + encodeURIComponent("" + onlyPrompt) + "&"; + + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetAiAnalysis(_response); + }); + } + + protected processGetAiAnalysis(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AnalysisDto; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } } export class UsersClient { @@ -632,7 +717,7 @@ export class UsersClient { constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { this.http = http ? http : window as any; - this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : ""; + this.baseUrl = baseUrl ?? ""; } getAll(): Promise { @@ -705,7 +790,7 @@ export class UsersClient { get(id: string | undefined): Promise { let url_ = this.baseUrl + "/api/Users/GetOne?"; if (id === null) - throw new Error("The parameter 'id' cannot be null."); + throw new globalThis.Error("The parameter 'id' cannot be null."); else if (id !== undefined) url_ += "id=" + encodeURIComponent("" + id) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -742,10 +827,10 @@ export class UsersClient { get2(idQuery: string | undefined, idPath: string): Promise { let url_ = this.baseUrl + "/api/Users/{id}?"; if (idPath === undefined || idPath === null) - throw new Error("The parameter 'idPath' must be defined."); + throw new globalThis.Error("The parameter 'idPath' must be defined."); url_ = url_.replace("{id}", encodeURIComponent("" + idPath)); if (idQuery === null) - throw new Error("The parameter 'idQuery' cannot be null."); + throw new globalThis.Error("The parameter 'idQuery' cannot be null."); else if (idQuery !== undefined) url_ += "id=" + encodeURIComponent("" + idQuery) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -779,10 +864,88 @@ export class UsersClient { return Promise.resolve(null as any); } + getTrainingUsername(id: number): Promise { + let url_ = this.baseUrl + "/api/Users/trainingUsername/{id}"; + if (id === undefined || id === null) + throw new globalThis.Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/octet-stream" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetTrainingUsername(_response); + }); + } + + protected processGetTrainingUsername(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + getOrCreateFromApp(username: string | undefined): Promise { + let url_ = this.baseUrl + "/api/Users/getOrCreate?"; + if (username === null) + throw new globalThis.Error("The parameter 'username' cannot be null."); + else if (username !== undefined) + url_ += "username=" + encodeURIComponent("" + username) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "POST", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetOrCreateFromApp(_response); + }); + } + + protected processGetOrCreateFromApp(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as GetUserDto; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + patch(id: string | undefined, dto: PatchUserDto): Promise { let url_ = this.baseUrl + "/api/Users/id?"; if (id === null) - throw new Error("The parameter 'id' cannot be null."); + throw new globalThis.Error("The parameter 'id' cannot be null."); else if (id !== undefined) url_ += "id=" + encodeURIComponent("" + id) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -967,7 +1130,7 @@ export class HealthClient { constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { this.http = http ? http : window as any; - this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : ""; + this.baseUrl = baseUrl ?? ""; } heartbeat(): Promise { @@ -1016,7 +1179,7 @@ export class SyncClient { constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { this.http = http ? http : window as any; - this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : ""; + this.baseUrl = baseUrl ?? ""; } syncUnauthorized(): Promise { @@ -1112,7 +1275,7 @@ export class SyncClient { deleteData(uuid: string | undefined): Promise { let url_ = this.baseUrl + "/api/Sync/DeleteData?"; if (uuid === null) - throw new Error("The parameter 'uuid' cannot be null."); + throw new globalThis.Error("The parameter 'uuid' cannot be null."); else if (uuid !== undefined) url_ += "uuid=" + encodeURIComponent("" + uuid) + "&"; url_ = url_.replace(/[?&]$/, ""); @@ -1212,6 +1375,7 @@ export interface TrainingSettings { syncSettings?: TrainingSyncSettings | undefined; alarmClockInvisible?: boolean | undefined; analyzers?: string[] | undefined; + redirectToClient?: string | undefined; } export interface CustomData { @@ -1310,6 +1474,11 @@ export interface TrainingSummaryWithDaysDto extends TrainingSummaryDto { days: TrainingDayAccount[]; } +export interface AnalysisDto { + prompt: string; + completion: string; +} + export interface GetUserDto { username: string; role: string; diff --git a/ProblemSource/AdminApp/src/apiFacade.ts b/ProblemSource/AdminApp/src/apiFacade.ts index 8d478816..ca809561 100644 --- a/ProblemSource/AdminApp/src/apiFacade.ts +++ b/ProblemSource/AdminApp/src/apiFacade.ts @@ -10,7 +10,7 @@ export class ApiFacade { impersonateUser: string | null = null; constructor(baseUrl: string) { - // console.log("baseUrl", baseUrl); + // console.log("api baseUrl", baseUrl); const http = { fetch: (r: Request, init?: RequestInit) => { init = init || {}; diff --git a/ProblemSource/AdminApp/src/globalStore.ts b/ProblemSource/AdminApp/src/globalStore.ts index 6eb07bec..e56582b4 100644 --- a/ProblemSource/AdminApp/src/globalStore.ts +++ b/ProblemSource/AdminApp/src/globalStore.ts @@ -4,7 +4,7 @@ import type { LoginCredentials } from './apiClient'; import { ApiFacade } from './apiFacade'; import type { CurrentUserInfo } from './currentUserInfo'; import { Assistant } from './services/assistant'; -import { resolveLocalServerBaseUrl, Startup } from './startup'; +import { resolveLocalServerBaseUrl } from './startup'; import { SeverityLevel, type NotificationItem } from './types'; import type { TrainingUpdateMessage } from './types.js'; import { Realtime } from './services/realtime'; @@ -62,7 +62,7 @@ export const realtimeTrainingListener = (() => { signal.trigger(null); try { - await realtime.connect(Startup.resolveLocalServerBaseUrl(window.location)); + await realtime.connect(resolveLocalServerBaseUrl(window.location)); } catch (err) { console.error('error connecting', err); } diff --git a/ProblemSource/AdminApp/src/routes/teacher/+page.svelte b/ProblemSource/AdminApp/src/routes/teacher/+page.svelte index 7a754014..deff9340 100644 --- a/ProblemSource/AdminApp/src/routes/teacher/+page.svelte +++ b/ProblemSource/AdminApp/src/routes/teacher/+page.svelte @@ -19,6 +19,15 @@ // const showRealtimeButton = false; // For now, don't show it at all... const showRealtimeButton = $userStore?.role == "Admin"; + const showAIButton = $userStore?.role == "Admin"; + let aiDialogForId: number | null = null; + const promptSettings = { + template: "https://raw.githubusercontent.com/JWMB/WebProcessor/refs/heads/main/ProblemSource/ProblemSourceModule/Resources/AICoach/TeacherStudent.txt", + model: "qwen3.1", + prompt: "(Generated prompt goes here)", + completion: "(Generated completion goes here)" + }; + const rtlTools = new RealtimelineTools(2 * 60 * 1000); let realtimeConnected: boolean | null = rtlTools.isConnected; @@ -70,7 +79,10 @@ groups = Object.entries(groupsData).map((o) => ({ group: o[0], summaries: o[1] })); } + let lastClick = 0; async function onSelectGroup(groupId: string) { + if (Date.now() - lastClick < 50) return; + lastClick = Date.now(); detailedTrainingsData = await apiFacade.trainings.getSummaries(groupId); // RealtimelineTools.testData(detailedTrainingsData.map(o => o.id)).forEach(o => rtlTools.append(o));; getRealtimeData(); @@ -139,6 +151,17 @@ } }); } + + async function generatePrompt(id: number, templateSource: string, onlyPrompt: boolean) { + if (apiFacade == null) { + console.error('apiFacade null'); + return; + } + const analysis = await apiFacade.trainings.getAiAnalysis(id, templateSource, onlyPrompt); + console.log("asd", analysis); + promptSettings.completion = analysis.completion; + promptSettings.prompt = analysis.prompt; + }
@@ -147,10 +170,6 @@ {/if}
-

Vi söker engagerade och IT-kunniga lärare för ett nytt kollaborativt initiativ att ta fram gratis och öppna läromedel. - Läs mer på github.

-

Skicka ett mail om du eller en lärare du känner är intresserad av att hjälpa till!

-

Vi jobbar även på nya versioner av Vektor, med fler typer av övningar. Anmäl intresse för att prova nya versioner här.

{#if groups && groups.length > 0} (Total: {groups.map(o => o.summaries.length).reduce((p, c) => p + c)} created, {groups.map(o => o.summaries.filter(p => p.trainedDays > 0).length).reduce((p, c) => p + c)} started) @@ -159,7 +178,7 @@ tabs={groups.map((g) => { return { id: g.group }; })} - on:selected={(e) => onSelectGroup(e.detail)} + on:selected={(e) => { console.log(e); onSelectGroup(e.detail); }} >