Compare commits
3 Commits
5c84bbc35a
...
17817ea0a9
| Author | SHA1 | Date | |
|---|---|---|---|
| 17817ea0a9 | |||
| a0c232e052 | |||
| 3e7b04df1c |
46
.run/Run Backend and Frontend.run.xml
Normal file
46
.run/Run Backend and Frontend.run.xml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Run Backend and Frontend" type="com.intellij.execution.configurations.multilaunch" factoryName="MultiLaunchConfiguration">
|
||||||
|
<rows>
|
||||||
|
<ExecutableRowSnapshot>
|
||||||
|
<option name="condition">
|
||||||
|
<ConditionSnapshot>
|
||||||
|
<option name="type" value="immediately" />
|
||||||
|
</ConditionSnapshot>
|
||||||
|
</option>
|
||||||
|
<option name="executable">
|
||||||
|
<ExecutableSnapshot>
|
||||||
|
<option name="id" value="buildSolution:0942b9" />
|
||||||
|
</ExecutableSnapshot>
|
||||||
|
</option>
|
||||||
|
</ExecutableRowSnapshot>
|
||||||
|
<ExecutableRowSnapshot>
|
||||||
|
<option name="condition">
|
||||||
|
<ConditionSnapshot>
|
||||||
|
<option name="type" value="afterPreviousFinished" />
|
||||||
|
</ConditionSnapshot>
|
||||||
|
</option>
|
||||||
|
<option name="executable">
|
||||||
|
<ExecutableSnapshot>
|
||||||
|
<option name="id" value="runConfig:.NET Launch Settings Profile.USEntryCoach.Server: https" />
|
||||||
|
</ExecutableSnapshot>
|
||||||
|
</option>
|
||||||
|
</ExecutableRowSnapshot>
|
||||||
|
<ExecutableRowSnapshot>
|
||||||
|
<option name="condition">
|
||||||
|
<ConditionSnapshot>
|
||||||
|
<attributes>
|
||||||
|
<entry key="port" value="7085" />
|
||||||
|
</attributes>
|
||||||
|
<option name="type" value="waitPortOpened" />
|
||||||
|
</ConditionSnapshot>
|
||||||
|
</option>
|
||||||
|
<option name="executable">
|
||||||
|
<ExecutableSnapshot>
|
||||||
|
<option name="id" value="runConfig:npm.dev" />
|
||||||
|
</ExecutableSnapshot>
|
||||||
|
</option>
|
||||||
|
</ExecutableRowSnapshot>
|
||||||
|
</rows>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
7
USEntryCoach.Server/Data/LoginCredentials.cs
Normal file
7
USEntryCoach.Server/Data/LoginCredentials.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace USEntryCoach.Server.Data;
|
||||||
|
|
||||||
|
class LoginCredentials
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
9
USEntryCoach.Server/Data/User.cs
Normal file
9
USEntryCoach.Server/Data/User.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace USEntryCoach.Server.Data;
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public UserRole Role { get; set; }
|
||||||
|
}
|
||||||
7
USEntryCoach.Server/Data/UserRole.cs
Normal file
7
USEntryCoach.Server/Data/UserRole.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace USEntryCoach.Server.Data;
|
||||||
|
|
||||||
|
public enum UserRole
|
||||||
|
{
|
||||||
|
Developer,
|
||||||
|
User
|
||||||
|
}
|
||||||
@ -1,13 +1,68 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using USEntryCoach.Server.Data;
|
||||||
|
using USEntryCoach.Server.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
builder.Services.AddSingleton<TokenService>();
|
||||||
|
|
||||||
|
// Configure JWT token generation.
|
||||||
|
builder.Services.AddAuthentication(config =>
|
||||||
|
{
|
||||||
|
config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
}).AddJwtBearer(config =>
|
||||||
|
{
|
||||||
|
// TODO: Use Microsoft.Extensions.Options?
|
||||||
|
// var myOptions = new MyOptions(); // Your settings class
|
||||||
|
// builder.Configuration.GetSection("MySection").Bind(myOptions);
|
||||||
|
// myOptions now has the values
|
||||||
|
//
|
||||||
|
// TODO: This is identical in TokenService
|
||||||
|
string? secretToken = builder.Configuration.GetValue<string>("Authentication:Secret");
|
||||||
|
|
||||||
|
if (secretToken == null)
|
||||||
|
{
|
||||||
|
throw new Exception("No Authentication Secret Token set! Please define a value for \"Authentication:SecretToken\" in appsettings.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] secretKey = Encoding.ASCII.GetBytes(secretToken);
|
||||||
|
|
||||||
|
// TODO: Only for debug!
|
||||||
|
config.RequireHttpsMetadata = false;
|
||||||
|
config.SaveToken = true;
|
||||||
|
config.TokenValidationParameters = new TokenValidationParameters()
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(secretKey),
|
||||||
|
ValidateIssuer = false,
|
||||||
|
ValidateAudience = false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy(nameof(UserRole.Developer), policy => policy.RequireRole(nameof(UserRole.Developer)));
|
||||||
|
options.AddPolicy(nameof(UserRole.User), policy =>
|
||||||
|
{
|
||||||
|
// Also allow Developers to do anything a user can do.
|
||||||
|
policy.RequireRole(nameof(UserRole.User), nameof(UserRole.Developer));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
|
|
||||||
@ -24,13 +79,61 @@ string? apiKey = app.Configuration.GetValue<string>("API:OpenAI");
|
|||||||
client.DefaultRequestHeaders.Clear();
|
client.DefaultRequestHeaders.Clear();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
||||||
|
|
||||||
|
app.MapPost("/login", Login);
|
||||||
|
|
||||||
|
app.MapGet("/hello", () => "Hello World!").RequireAuthorization("admin_greetings");
|
||||||
|
|
||||||
|
async Task<IResult> Login(LoginCredentials credentials, TokenService tokenService, HttpContext context)
|
||||||
|
{
|
||||||
|
// TODO: Check with database
|
||||||
|
User? user = null;
|
||||||
|
|
||||||
|
if (credentials is {Username:"developer", Password:"dev"})
|
||||||
|
{
|
||||||
|
user = new User()
|
||||||
|
{
|
||||||
|
Username = credentials.Username,
|
||||||
|
Password = credentials.Password,
|
||||||
|
Id = Guid.CreateVersion7(),
|
||||||
|
Role = UserRole.Developer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (credentials is {Username:"user", Password:"us"})
|
||||||
|
{
|
||||||
|
user = new User()
|
||||||
|
{
|
||||||
|
Username = credentials.Username,
|
||||||
|
Password = credentials.Password,
|
||||||
|
Id = Guid.CreateVersion7(),
|
||||||
|
Role = UserRole.User
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = tokenService.GenerateToken(user);
|
||||||
|
|
||||||
|
return Results.Ok(new { token });
|
||||||
|
}
|
||||||
|
|
||||||
|
app.MapGet("/developer", (ClaimsPrincipal user) =>
|
||||||
|
{
|
||||||
|
Results.Ok(new { message = $"Authenticated as { user?.Identity?.Name }" });
|
||||||
|
}).RequireAuthorization(nameof(UserRole.Developer));
|
||||||
|
|
||||||
|
app.MapGet("/user", (ClaimsPrincipal user) =>
|
||||||
|
{
|
||||||
|
Results.Ok(new { message = $"Authenticated as { user?.Identity?.Name }" });
|
||||||
|
}).RequireAuthorization(nameof(UserRole.User));
|
||||||
|
|
||||||
app.MapGet("/ephemeral_token", async () =>
|
app.MapGet("/ephemeral_token", async () =>
|
||||||
{
|
{
|
||||||
if (apiKey == null)
|
if (apiKey == null)
|
||||||
throw new Exception("API key not set");
|
throw new Exception("API key not set");
|
||||||
|
|
||||||
#pragma warning disable OPENAI002
|
|
||||||
|
|
||||||
var options = new
|
var options = new
|
||||||
{
|
{
|
||||||
model = "gpt-4o-mini-realtime-preview",
|
model = "gpt-4o-mini-realtime-preview",
|
||||||
@ -44,8 +147,6 @@ app.MapGet("/ephemeral_token", async () =>
|
|||||||
//}//ConversationTurnDetectionOptions.CreateServerVoiceActivityTurnDetectionOptions(0.5f, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(500), true)
|
//}//ConversationTurnDetectionOptions.CreateServerVoiceActivityTurnDetectionOptions(0.5f, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(500), true)
|
||||||
};
|
};
|
||||||
|
|
||||||
#pragma warning restore OPENAI002
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
JsonContent content = JsonContent.Create(options);
|
JsonContent content = JsonContent.Create(options);
|
||||||
@ -76,7 +177,7 @@ app.MapGet("/ephemeral_token", async () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}).WithName("GetEphemeralToken");
|
}).WithName("GetEphemeralToken").RequireAuthorization(nameof(UserRole.User));
|
||||||
|
|
||||||
app.MapFallbackToFile("/index.html");
|
app.MapFallbackToFile("/index.html");
|
||||||
|
|
||||||
|
|||||||
56
USEntryCoach.Server/Services/TokenService.cs
Normal file
56
USEntryCoach.Server/Services/TokenService.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using USEntryCoach.Server.Data;
|
||||||
|
|
||||||
|
namespace USEntryCoach.Server.Services;
|
||||||
|
|
||||||
|
public class TokenService
|
||||||
|
{
|
||||||
|
private byte[] _secretToken;
|
||||||
|
private double _jwtExpiryMinutes;
|
||||||
|
private const double DefaultJwtExpiryMinutes = 15;
|
||||||
|
|
||||||
|
public TokenService(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
string? secretToken = configuration.GetValue<string>("Authentication:Secret");
|
||||||
|
|
||||||
|
if (secretToken == null)
|
||||||
|
{
|
||||||
|
throw new Exception("No Authentication Secret Token set! Please define a value for \"Authentication:SecretToken\" in appsettings.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_secretToken = Encoding.ASCII.GetBytes(secretToken);
|
||||||
|
|
||||||
|
double? jwtExpiryMinutes = configuration.GetValue<double?>("Authentication:JwtExpiryMinutes");
|
||||||
|
|
||||||
|
if (jwtExpiryMinutes == null)
|
||||||
|
{
|
||||||
|
// TODO: Use logger
|
||||||
|
Console.WriteLine($"Warning: No expiry time for jwt session tokens defined. Using {DefaultJwtExpiryMinutes} minutes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_jwtExpiryMinutes = jwtExpiryMinutes ?? DefaultJwtExpiryMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateToken(User user)
|
||||||
|
{
|
||||||
|
JwtSecurityTokenHandler tokenHandler = new();
|
||||||
|
|
||||||
|
SecurityTokenDescriptor tokenDescriptor = new()
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity([
|
||||||
|
new Claim(ClaimTypes.Name, user.Username),
|
||||||
|
//new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||||
|
new Claim(ClaimTypes.Role, user.Role.ToString())
|
||||||
|
]),
|
||||||
|
Expires = DateTime.UtcNow.AddMinutes(_jwtExpiryMinutes),
|
||||||
|
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(_secretToken), SecurityAlgorithms.HmacSha256Signature)
|
||||||
|
};
|
||||||
|
|
||||||
|
SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
|
||||||
|
return tokenHandler.WriteToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
|
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
|
||||||
<Version>9.*-*</Version>
|
<Version>9.*-*</Version>
|
||||||
|
|||||||
@ -4,5 +4,11 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"OpenAI": "Please set the key in secrets.json! NEVER HERE!!!"
|
||||||
|
},
|
||||||
|
"Authentication": {
|
||||||
|
"Secret": "Please provide a GUID (without dashes) as secret."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,26 +3,56 @@ import globals from 'globals'
|
|||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
|
import stylistic from '@stylistic/eslint-plugin'
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ['dist'] },
|
{ignores: ['dist']},
|
||||||
{
|
{
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
extends: [
|
||||||
files: ['**/*.{ts,tsx}'],
|
js.configs.recommended,
|
||||||
languageOptions: {
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
ecmaVersion: 2020,
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
globals: globals.browser,
|
],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
//'stylistic/js': stylisticJs
|
||||||
|
stylistic
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
//'brace-style': ['error', 'allman'],
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{allowConstantExport: true},
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||||
|
"vars": "all",
|
||||||
|
"args": "all",
|
||||||
|
"caughtErrors": "all",
|
||||||
|
"ignoreRestSiblings": false,
|
||||||
|
"reportUsedIgnorePattern": false
|
||||||
|
}],
|
||||||
|
"prefer-const": ["warn", {
|
||||||
|
"destructuring": "any",
|
||||||
|
"ignoreReadBeforeAssign": false
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Formatting:
|
||||||
|
"stylistic/brace-style": [
|
||||||
|
'error',
|
||||||
|
'allman',
|
||||||
|
{ allowSingleLine: false }
|
||||||
|
]
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
|
||||||
'react-hooks': reactHooks,
|
|
||||||
'react-refresh': reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|||||||
677
usentrycoach.client/package-lock.json
generated
677
usentrycoach.client/package-lock.json
generated
@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
|
"@stylistic/eslint-plugin": "^4.2.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
@ -302,390 +303,6 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-x64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
|
|
||||||
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
|
||||||
@ -1007,253 +624,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
|
||||||
"version": "4.40.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
|
|
||||||
"integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.40.2",
|
"version": "4.40.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
|
||||||
@ -1267,6 +637,37 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@stylistic/eslint-plugin": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-8hXezgz7jexGHdo5WN6JBEIPHCSFyyU4vgbxevu4YLVS5vl+sxqAAGyXSzfNDyR6xMNSH5H1x67nsXcYMOHtZA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/utils": "^8.23.0",
|
||||||
|
"eslint-visitor-keys": "^4.2.0",
|
||||||
|
"espree": "^10.3.0",
|
||||||
|
"estraverse": "^5.3.0",
|
||||||
|
"picomatch": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": ">=9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@ -2489,20 +1890,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
|
"@stylistic/eslint-plugin": "^4.2.0",
|
||||||
|
"@types/node": "^20",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
@ -24,7 +26,6 @@
|
|||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5"
|
||||||
"@types/node": "^20"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,415 +1,16 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import {ChatControl} from "./ChatClient/ChatControl.tsx";
|
import {ChatControl} from "./ChatClient/ChatControl.tsx";
|
||||||
|
import Login from './Components/Login.tsx';
|
||||||
|
import useLoginToken from "./Hooks/useLoginToken.tsx";
|
||||||
|
|
||||||
interface Forecast {
|
export default function App()
|
||||||
date: string;
|
{
|
||||||
temperatureC: number;
|
const {token, setToken} = useLoginToken();
|
||||||
temperatureF: number;
|
|
||||||
summary: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OpenAiToken {
|
if (!token)
|
||||||
ephemeralToken: string;
|
{
|
||||||
expiresAt: number;
|
return <Login setToken={setToken} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
|
|
||||||
return <ChatControl/>
|
return <ChatControl/>
|
||||||
|
|
||||||
// const [isSessionActive, setIsSessionActive] = useState(false);
|
|
||||||
// const [events, setEvents] = useState([]);
|
|
||||||
// const [dataChannel, setDataChannel] = useState<RTCDataChannel | null>(null);
|
|
||||||
// const peerConnection = useRef<RTCPeerConnection>(null);
|
|
||||||
// const audioElement = useRef<HTMLAudioElement>(null);
|
|
||||||
//
|
|
||||||
// async function startSession() {
|
|
||||||
// // Get a session token for OpenAI Realtime API
|
|
||||||
// const tokenResponse = await fetch("/ephemeral_token");
|
|
||||||
// const data: OpenAiToken = await tokenResponse.json();
|
|
||||||
// const EPHEMERAL_KEY = data.ephemeralToken;
|
|
||||||
//
|
|
||||||
// // Create a peer connection
|
|
||||||
// const pc = new RTCPeerConnection();
|
|
||||||
//
|
|
||||||
// // Set up to play remote audio from the model
|
|
||||||
// audioElement.current = document.createElement("audio");
|
|
||||||
// audioElement.current.autoplay = true;
|
|
||||||
// pc.ontrack = (e) => {
|
|
||||||
// if (audioElement?.current !== null) {
|
|
||||||
// audioElement.current.srcObject = e.streams[0]
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// // Add local audio track for microphone input in the browser
|
|
||||||
// const ms = await navigator.mediaDevices.getUserMedia({
|
|
||||||
// audio: true,
|
|
||||||
// });
|
|
||||||
// pc.addTrack(ms.getTracks()[0]);
|
|
||||||
//
|
|
||||||
// // Set up data channel for sending and receiving events
|
|
||||||
// const dc = pc.createDataChannel("oai-events");
|
|
||||||
// setDataChannel(dc);
|
|
||||||
//
|
|
||||||
// // Start the session using the Session Description Protocol (SDP)
|
|
||||||
// const offer = await pc.createOffer();
|
|
||||||
// await pc.setLocalDescription(offer);
|
|
||||||
//
|
|
||||||
// const baseUrl = "https://api.openai.com/v1/realtime";
|
|
||||||
// const model = "gpt-4o-realtime-preview-2024-12-17";
|
|
||||||
// const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
|
|
||||||
// method: "POST",
|
|
||||||
// body: offer.sdp,
|
|
||||||
// headers: {
|
|
||||||
// Authorization: `Bearer ${EPHEMERAL_KEY}`,
|
|
||||||
// "Content-Type": "application/sdp",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// const answer: RTCSessionDescriptionInit = {
|
|
||||||
// type: "answer",
|
|
||||||
// sdp: await sdpResponse.text(),
|
|
||||||
// };
|
|
||||||
// await pc.setRemoteDescription(answer);
|
|
||||||
//
|
|
||||||
// peerConnection.current = pc;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Stop current session, clean up peer connection and data channel
|
|
||||||
// function stopSession() {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// dataChannel.close();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (peerConnection?.current !== null)
|
|
||||||
// {
|
|
||||||
// peerConnection.current.getSenders().forEach((sender) => {
|
|
||||||
// if (sender.track) {
|
|
||||||
// sender.track.stop();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (peerConnection.current) {
|
|
||||||
// peerConnection.current.close();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// setIsSessionActive(false);
|
|
||||||
// setDataChannel(null);
|
|
||||||
// peerConnection.current = null;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Send a message to the model
|
|
||||||
// function sendClientEvent(message) {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// const timestamp = new Date().toLocaleTimeString();
|
|
||||||
// message.event_id = message.event_id || crypto.randomUUID();
|
|
||||||
//
|
|
||||||
// // send event before setting timestamp since the backend peer doesn't expect this field
|
|
||||||
// dataChannel.send(JSON.stringify(message));
|
|
||||||
//
|
|
||||||
// // if guard just in case the timestamp exists by miracle
|
|
||||||
// if (!message.timestamp) {
|
|
||||||
// message.timestamp = timestamp;
|
|
||||||
// }
|
|
||||||
// setEvents((prev) => [message, ...prev]);
|
|
||||||
// } else {
|
|
||||||
// console.error(
|
|
||||||
// "Failed to send message - no data channel available",
|
|
||||||
// message,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Send a text message to the model
|
|
||||||
// function sendTextMessage(message) {
|
|
||||||
// const event = {
|
|
||||||
// type: "conversation.item.create",
|
|
||||||
// item: {
|
|
||||||
// type: "message",
|
|
||||||
// role: "user",
|
|
||||||
// content: [
|
|
||||||
// {
|
|
||||||
// type: "input_text",
|
|
||||||
// text: message,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// sendClientEvent(event);
|
|
||||||
// sendClientEvent({ type: "response.create" });
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Attach event listeners to the data channel when a new one is created
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// // Append new server events to the list
|
|
||||||
// dataChannel.addEventListener("message", (e) => {
|
|
||||||
// const event = JSON.parse(e.data);
|
|
||||||
// if (!event.timestamp) {
|
|
||||||
// event.timestamp = new Date().toLocaleTimeString();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// setEvents((prev) => [event, ...prev]);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// // Set session active when the data channel is opened
|
|
||||||
// dataChannel.addEventListener("open", () => {
|
|
||||||
// setIsSessionActive(true);
|
|
||||||
// setEvents([]);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }, [dataChannel]);
|
|
||||||
//
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// <nav className="absolute top-0 left-0 right-0 h-16 flex items-center">
|
|
||||||
// <div className="flex items-center gap-4 w-full m-4 pb-2 border-0 border-b border-solid border-gray-200">
|
|
||||||
// <img style={{ width: "24px" }} src={logo} />
|
|
||||||
// <h1>realtime console</h1>
|
|
||||||
// </div>
|
|
||||||
// </nav>
|
|
||||||
// <main className="absolute top-16 left-0 right-0 bottom-0">
|
|
||||||
// <section className="absolute top-0 left-0 right-[380px] bottom-0 flex">
|
|
||||||
// <section className="absolute top-0 left-0 right-0 bottom-32 px-4 overflow-y-auto">
|
|
||||||
// <EventLog events={events} />
|
|
||||||
// </section>
|
|
||||||
// <section className="absolute h-32 left-0 right-0 bottom-0 p-4">
|
|
||||||
// <SessionControls
|
|
||||||
// startSession={startSession}
|
|
||||||
// stopSession={stopSession}
|
|
||||||
// sendClientEvent={sendClientEvent}
|
|
||||||
// sendTextMessage={sendTextMessage}
|
|
||||||
// events={events}
|
|
||||||
// isSessionActive={isSessionActive}
|
|
||||||
// />
|
|
||||||
// </section>
|
|
||||||
// </section>
|
|
||||||
// <section className="absolute top-0 w-[380px] right-0 bottom-0 p-4 pt-0 overflow-y-auto">
|
|
||||||
// <ToolPanel
|
|
||||||
// sendClientEvent={sendClientEvent}
|
|
||||||
// sendTextMessage={sendTextMessage}
|
|
||||||
// events={events}
|
|
||||||
// isSessionActive={isSessionActive}
|
|
||||||
// />
|
|
||||||
// </section>
|
|
||||||
// </main>
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// export default function App() {
|
|
||||||
// const [openAiToken, setOpenAiToken] = useState<OpenAiToken | null>();
|
|
||||||
//
|
|
||||||
// useEffect(() => {
|
|
||||||
// get_the_fucking_token_already();
|
|
||||||
// }, []);
|
|
||||||
//
|
|
||||||
// const contents = openAiToken === undefined
|
|
||||||
// ? <p>Connecting to Open AI...</p>
|
|
||||||
// : <div>
|
|
||||||
// {
|
|
||||||
// openAiToken === null
|
|
||||||
// ? <p>Failed to connect to OpenAI, please reload the page.</p>
|
|
||||||
// : <p>
|
|
||||||
// {openAiToken.ephemeralToken}<br/>
|
|
||||||
// {new Date(openAiToken.expiresAt * 1000).toLocaleString()}
|
|
||||||
// </p>
|
|
||||||
// }
|
|
||||||
// </div>;
|
|
||||||
// return (
|
|
||||||
// <div>
|
|
||||||
// <h1 id="tableLabel">Weather forecast</h1>
|
|
||||||
// <p>This component demonstrates fetching data from the server.</p>
|
|
||||||
// {contents}
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// async function get_the_fucking_token_already() {
|
|
||||||
// const response = await fetch('ephemeral_token');
|
|
||||||
// if (response.ok) {
|
|
||||||
// const data = await response.json();
|
|
||||||
// setOpenAiToken(data);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// function Chatter({ openAiToken }: { openAiToken: OpenAiToken }) {
|
|
||||||
// const [isSessionActive, setIsSessionActive] = useState(false);
|
|
||||||
// const [events, setEvents] = useState([]);
|
|
||||||
// const [dataChannel, setDataChannel] = useState(null);
|
|
||||||
// const peerConnection = useRef(null);
|
|
||||||
// const audioElement = useRef(null);
|
|
||||||
//
|
|
||||||
// async function startSession() {
|
|
||||||
// // Get a session token for OpenAI Realtime API
|
|
||||||
// const tokenResponse = await fetch("/token");
|
|
||||||
// const data = await tokenResponse.json();
|
|
||||||
// const EPHEMERAL_KEY = data.client_secret.value;
|
|
||||||
//
|
|
||||||
// // Create a peer connection
|
|
||||||
// const pc = new RTCPeerConnection();
|
|
||||||
//
|
|
||||||
// // Set up to play remote audio from the model
|
|
||||||
// audioElement.current = document.createElement("audio");
|
|
||||||
// audioElement.current.autoplay = true;
|
|
||||||
// pc.ontrack = (e) => (audioElement.current.srcObject = e.streams[0]);
|
|
||||||
//
|
|
||||||
// // Add local audio track for microphone input in the browser
|
|
||||||
// const ms = await navigator.mediaDevices.getUserMedia({
|
|
||||||
// audio: true,
|
|
||||||
// });
|
|
||||||
// pc.addTrack(ms.getTracks()[0]);
|
|
||||||
//
|
|
||||||
// // Set up data channel for sending and receiving events
|
|
||||||
// const dc = pc.createDataChannel("oai-events");
|
|
||||||
// setDataChannel(dc);
|
|
||||||
//
|
|
||||||
// // Start the session using the Session Description Protocol (SDP)
|
|
||||||
// const offer = await pc.createOffer();
|
|
||||||
// await pc.setLocalDescription(offer);
|
|
||||||
//
|
|
||||||
// const baseUrl = "https://api.openai.com/v1/realtime";
|
|
||||||
// const model = "gpt-4o-realtime-preview-2024-12-17";
|
|
||||||
// const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
|
|
||||||
// method: "POST",
|
|
||||||
// body: offer.sdp,
|
|
||||||
// headers: {
|
|
||||||
// Authorization: `Bearer ${EPHEMERAL_KEY}`,
|
|
||||||
// "Content-Type": "application/sdp",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// const answer = {
|
|
||||||
// type: "answer",
|
|
||||||
// sdp: await sdpResponse.text(),
|
|
||||||
// };
|
|
||||||
// await pc.setRemoteDescription(answer);
|
|
||||||
//
|
|
||||||
// peerConnection.current = pc;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Stop current session, clean up peer connection and data channel
|
|
||||||
// function stopSession() {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// dataChannel.close();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// peerConnection.current.getSenders().forEach((sender) => {
|
|
||||||
// if (sender.track) {
|
|
||||||
// sender.track.stop();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (peerConnection.current) {
|
|
||||||
// peerConnection.current.close();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// setIsSessionActive(false);
|
|
||||||
// setDataChannel(null);
|
|
||||||
// peerConnection.current = null;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Send a message to the model
|
|
||||||
// function sendClientEvent(message) {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// const timestamp = new Date().toLocaleTimeString();
|
|
||||||
// message.event_id = message.event_id || crypto.randomUUID();
|
|
||||||
//
|
|
||||||
// // send event before setting timestamp since the backend peer doesn't expect this field
|
|
||||||
// dataChannel.send(JSON.stringify(message));
|
|
||||||
//
|
|
||||||
// // if guard just in case the timestamp exists by miracle
|
|
||||||
// if (!message.timestamp) {
|
|
||||||
// message.timestamp = timestamp;
|
|
||||||
// }
|
|
||||||
// setEvents((prev) => [message, ...prev]);
|
|
||||||
// } else {
|
|
||||||
// console.error(
|
|
||||||
// "Failed to send message - no data channel available",
|
|
||||||
// message,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Send a text message to the model
|
|
||||||
// function sendTextMessage(message) {
|
|
||||||
// const event = {
|
|
||||||
// type: "conversation.item.create",
|
|
||||||
// item: {
|
|
||||||
// type: "message",
|
|
||||||
// role: "user",
|
|
||||||
// content: [
|
|
||||||
// {
|
|
||||||
// type: "input_text",
|
|
||||||
// text: message,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// sendClientEvent(event);
|
|
||||||
// sendClientEvent({ type: "response.create" });
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Attach event listeners to the data channel when a new one is created
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// // Append new server events to the list
|
|
||||||
// dataChannel.addEventListener("message", (e) => {
|
|
||||||
// const event = JSON.parse(e.data);
|
|
||||||
// if (!event.timestamp) {
|
|
||||||
// event.timestamp = new Date().toLocaleTimeString();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// setEvents((prev) => [event, ...prev]);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// // Set session active when the data channel is opened
|
|
||||||
// dataChannel.addEventListener("open", () => {
|
|
||||||
// setIsSessionActive(true);
|
|
||||||
// setEvents([]);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }, [dataChannel]);
|
|
||||||
//
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// <nav className="absolute top-0 left-0 right-0 h-16 flex items-center">
|
|
||||||
// <div className="flex items-center gap-4 w-full m-4 pb-2 border-0 border-b border-solid border-gray-200">
|
|
||||||
// <img style={{ width: "24px" }} src={logo} />
|
|
||||||
// <h1>realtime console</h1>
|
|
||||||
// </div>
|
|
||||||
// </nav>
|
|
||||||
// <main className="absolute top-16 left-0 right-0 bottom-0">
|
|
||||||
// <section className="absolute top-0 left-0 right-[380px] bottom-0 flex">
|
|
||||||
// <section className="absolute top-0 left-0 right-0 bottom-32 px-4 overflow-y-auto">
|
|
||||||
// <EventLog events={events} />
|
|
||||||
// </section>
|
|
||||||
// <section className="absolute h-32 left-0 right-0 bottom-0 p-4">
|
|
||||||
// <SessionControls
|
|
||||||
// startSession={startSession}
|
|
||||||
// stopSession={stopSession}
|
|
||||||
// sendClientEvent={sendClientEvent}
|
|
||||||
// sendTextMessage={sendTextMessage}
|
|
||||||
// events={events}
|
|
||||||
// isSessionActive={isSessionActive}
|
|
||||||
// />
|
|
||||||
// </section>
|
|
||||||
// </section>
|
|
||||||
// <section className="absolute top-0 w-[380px] right-0 bottom-0 p-4 pt-0 overflow-y-auto">
|
|
||||||
// <ToolPanel
|
|
||||||
// sendClientEvent={sendClientEvent}
|
|
||||||
// sendTextMessage={sendTextMessage}
|
|
||||||
// events={events}
|
|
||||||
// isSessionActive={isSessionActive}
|
|
||||||
// />
|
|
||||||
// </section>
|
|
||||||
// </main>
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import {useState, useRef, useEffect} from 'react';
|
import {useState, useRef, useEffect} from 'react';
|
||||||
|
import useLoginToken from "../Hooks/useLoginToken.tsx";
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
function SessionActive({stopSession} : {stopSession: () => void}) {
|
function SessionActive({stopSession} : {stopSession: () => void})
|
||||||
|
{
|
||||||
return <button onClick={stopSession}>Stop session</button>
|
return <button onClick={stopSession}>Stop session</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
function SessionStopped({startSession} : {startSession: () => void}) {
|
function SessionStopped({startSession} : {startSession: () => void})
|
||||||
|
{
|
||||||
return <button onClick={startSession}>Start session</button>
|
return <button onClick={startSession}>Start session</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,26 +19,37 @@ function SessionControl( { isSessionActive, startSession, stopSession }: { isSes
|
|||||||
: <SessionStopped startSession={startSession}/>);
|
: <SessionStopped startSession={startSession}/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OpenAiToken {
|
const EphemeralTokenResponse = z.object({
|
||||||
ephemeralToken: string;
|
ephemeralToken: z.string(),
|
||||||
expiresAt: number;
|
expiresAt: z.number(),
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export function ChatControl()
|
||||||
|
{
|
||||||
|
const { token } = useLoginToken();
|
||||||
|
|
||||||
export function ChatControl() {
|
|
||||||
const [isSessionActive, setSessionActive] = useState<boolean>(false);
|
const [isSessionActive, setSessionActive] = useState<boolean>(false);
|
||||||
const [dataChannel, setDataChannel] = useState<RTCDataChannel | null>(null);
|
const [dataChannel, setDataChannel] = useState<RTCDataChannel | null>(null);
|
||||||
const audioElement = useRef<HTMLAudioElement>(null);
|
const audioElement = useRef<HTMLAudioElement>(null);
|
||||||
const peerConnection = useRef<RTCPeerConnection>(null);
|
const peerConnection = useRef<RTCPeerConnection>(null);
|
||||||
|
|
||||||
async function StartSession() {
|
async function StartSession()
|
||||||
|
{
|
||||||
// Get a session token for OpenAI Realtime API
|
// Get a session token for OpenAI Realtime API
|
||||||
const response = await fetch('ephemeral_token');
|
const response = await fetch('ephemeral_token', {
|
||||||
if (!response.ok) {
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}asdasd`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok)
|
||||||
|
{
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: OpenAiToken = await response.json();
|
const responseJson:unknown = await response.json();
|
||||||
const ephemeralToken = data.ephemeralToken;
|
const parsedToken = EphemeralTokenResponse.parse(responseJson);
|
||||||
|
const ephemeralToken = parsedToken.ephemeralToken;
|
||||||
|
|
||||||
// Create a peer connection
|
// Create a peer connection
|
||||||
const pc = new RTCPeerConnection();
|
const pc = new RTCPeerConnection();
|
||||||
@ -42,7 +57,8 @@ export function ChatControl() {
|
|||||||
// Set up to play remote audio from the model
|
// Set up to play remote audio from the model
|
||||||
audioElement.current = document.createElement("audio");
|
audioElement.current = document.createElement("audio");
|
||||||
audioElement.current.autoplay = true;
|
audioElement.current.autoplay = true;
|
||||||
pc.ontrack = (e) => {
|
pc.ontrack = (e) =>
|
||||||
|
{
|
||||||
if (audioElement.current !== null)
|
if (audioElement.current !== null)
|
||||||
{
|
{
|
||||||
audioElement.current.srcObject = e.streams[0];
|
audioElement.current.srcObject = e.streams[0];
|
||||||
@ -82,8 +98,10 @@ export function ChatControl() {
|
|||||||
peerConnection.current = pc;
|
peerConnection.current = pc;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopSession() {
|
function stopSession()
|
||||||
if (dataChannel) {
|
{
|
||||||
|
if (dataChannel)
|
||||||
|
{
|
||||||
dataChannel.close();
|
dataChannel.close();
|
||||||
setDataChannel(null);
|
setDataChannel(null);
|
||||||
}
|
}
|
||||||
@ -97,13 +115,16 @@ export function ChatControl() {
|
|||||||
setSessionActive(false);
|
setSessionActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
if (dataChannel) {
|
{
|
||||||
dataChannel.addEventListener("open", () => {
|
if (dataChannel)
|
||||||
|
{
|
||||||
|
dataChannel.addEventListener("open", () =>
|
||||||
|
{
|
||||||
setSessionActive(true);
|
setSessionActive(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [dataChannel])
|
}, [dataChannel])
|
||||||
|
|
||||||
return <SessionControl isSessionActive={isSessionActive} startSession={StartSession} stopSession={stopSession}/>;
|
return <SessionControl isSessionActive={isSessionActive} startSession={() => void StartSession()} stopSession={stopSession}/>;
|
||||||
}
|
}
|
||||||
60
usentrycoach.client/src/Components/Login.tsx
Normal file
60
usentrycoach.client/src/Components/Login.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useState, type FormEvent } from 'react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export default function Login({ setToken } : {setToken: (token: string) => void})
|
||||||
|
{
|
||||||
|
const [username, setUsername] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
|
||||||
|
async function doLogin(event: FormEvent<HTMLFormElement>)
|
||||||
|
{
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const token = await loginUser({username, password});
|
||||||
|
|
||||||
|
setToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form className="login-form" onSubmit={e => void doLogin(e)} method="action">
|
||||||
|
<label>
|
||||||
|
Email:
|
||||||
|
<input type="text" name="username" placeholder="Username" required onChange={event => setUsername(event.target.value)} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Password:
|
||||||
|
<input type="password" name="password" placeholder="Password" required onChange={event => setPassword(event.target.value)} />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginResponse = z.object({
|
||||||
|
token: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loginUser(credentials : {username: string, password: string}): Promise<string>
|
||||||
|
{
|
||||||
|
// Get a session token for OpenAI Realtime API
|
||||||
|
const response = await fetch('login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(credentials)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok)
|
||||||
|
{
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson:unknown = await response.json();
|
||||||
|
const parsedToken = LoginResponse.parse(responseJson);
|
||||||
|
|
||||||
|
return parsedToken.token;
|
||||||
|
}
|
||||||
27
usentrycoach.client/src/Hooks/useLoginToken.tsx
Normal file
27
usentrycoach.client/src/Hooks/useLoginToken.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A React hook that allows to access the session token (which might be in session or local storage).
|
||||||
|
* It also allows to set the token.
|
||||||
|
*/
|
||||||
|
export default function useLoginToken()
|
||||||
|
{
|
||||||
|
const getToken = () =>
|
||||||
|
{
|
||||||
|
const tokenJson = sessionStorage.getItem('token');
|
||||||
|
|
||||||
|
return tokenJson ? JSON.parse(tokenJson) as string : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [token, setToken] = useState<string | null>(getToken());
|
||||||
|
|
||||||
|
const saveToken = (token: string): void =>
|
||||||
|
{
|
||||||
|
sessionStorage.setItem('token', JSON.stringify(token));
|
||||||
|
setToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In React it is common for hooks to return arrays. However, returning an object allows for partial destruction
|
||||||
|
// (that is, user can grab only the values they need).
|
||||||
|
return { token, setToken: saveToken };
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url';
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig, type ProxyOptions } from 'vite';
|
||||||
import plugin from '@vitejs/plugin-react';
|
import plugin from '@vitejs/plugin-react';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@ -16,11 +16,13 @@ const certificateName = "usentrycoach.client";
|
|||||||
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
||||||
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
||||||
|
|
||||||
if (!fs.existsSync(baseFolder)) {
|
if (!fs.existsSync(baseFolder))
|
||||||
|
{
|
||||||
fs.mkdirSync(baseFolder, { recursive: true });
|
fs.mkdirSync(baseFolder, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath))
|
||||||
|
{
|
||||||
if (0 !== child_process.spawnSync('dotnet', [
|
if (0 !== child_process.spawnSync('dotnet', [
|
||||||
'dev-certs',
|
'dev-certs',
|
||||||
'https',
|
'https',
|
||||||
@ -29,7 +31,8 @@ if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
|||||||
'--format',
|
'--format',
|
||||||
'Pem',
|
'Pem',
|
||||||
'--no-password',
|
'--no-password',
|
||||||
], { stdio: 'inherit', }).status) {
|
], { stdio: 'inherit', }).status)
|
||||||
|
{
|
||||||
throw new Error("Could not create certificate.");
|
throw new Error("Could not create certificate.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,6 +40,23 @@ if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
|||||||
const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
|
const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
|
||||||
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7085';
|
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7085';
|
||||||
|
|
||||||
|
// Define a list of all existing backend routes.
|
||||||
|
const backendRoutes = ['/login', '/ephemeral_token'];
|
||||||
|
|
||||||
|
// For development, we have a node.js server running that delivers our frontend.
|
||||||
|
// We have to configure proxies for the backend calls, so that node.js will forward them to the backend.
|
||||||
|
const proxyConfig = backendRoutes.reduce((acc, path) =>
|
||||||
|
{
|
||||||
|
acc[path] = {
|
||||||
|
target: target,
|
||||||
|
// disable certificate verification because the development certificate is self-signed
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, ProxyOptions>);
|
||||||
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [plugin()],
|
plugins: [plugin()],
|
||||||
@ -46,13 +66,8 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: proxyConfig,
|
||||||
'^/ephemeral_token': {
|
port: parseInt(env.DEV_SERVER_PORT ?? '54044'),
|
||||||
target,
|
|
||||||
secure: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
port: parseInt(env.DEV_SERVER_PORT || '54044'),
|
|
||||||
https: {
|
https: {
|
||||||
key: fs.readFileSync(keyFilePath),
|
key: fs.readFileSync(keyFilePath),
|
||||||
cert: fs.readFileSync(certFilePath),
|
cert: fs.readFileSync(certFilePath),
|
||||||
|
|||||||
Reference in New Issue
Block a user