mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add DevUI package for .NET (#1603)
* Implement DevUI * Review feedback * Fix build
This commit is contained in:
committed by
GitHub
Unverified
parent
94a5ba3448
commit
8855bfb065
@@ -12,6 +12,8 @@ ignorePatterns:
|
||||
- pattern: "https:\/\/platform.openai.com"
|
||||
- pattern: "http:\/\/localhost"
|
||||
- pattern: "http:\/\/127.0.0.1"
|
||||
- pattern: "https:\/\/localhost"
|
||||
- pattern: "https:\/\/127.0.0.1"
|
||||
- pattern: "0001-spec.md"
|
||||
- pattern: "0001-madr-architecture-decisions.md"
|
||||
- pattern: "https://api.powerplatform.com/.default"
|
||||
|
||||
@@ -74,6 +74,7 @@ jobs:
|
||||
.
|
||||
.github
|
||||
dotnet
|
||||
python
|
||||
workflow-samples
|
||||
|
||||
- name: Setup dotnet
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Agent_Step19_Mem0Provider.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step20_BackgroundResponsesWithToolsAndPersistence/Agent_Step20_BackgroundResponsesWithToolsAndPersistence.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/DevUI/">
|
||||
<File Path="samples/GettingStarted/DevUI/README.md" />
|
||||
<Project Path="samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithOpenAI/">
|
||||
<File Path="samples/GettingStarted/AgentWithOpenAI/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
|
||||
@@ -279,6 +283,7 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>DevUI_Step01_BasicUsage</RootNamespace>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DevUI\Microsoft.Agents.AI.DevUI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hosting.OpenAI\Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates basic usage of the DevUI in an ASP.NET Core application with AI agents.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.DevUI;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace DevUI_Step01_BasicUsage;
|
||||
|
||||
/// <summary>
|
||||
/// Sample demonstrating basic usage of the DevUI in an ASP.NET Core application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This sample shows how to:
|
||||
/// 1. Set up Azure OpenAI as the chat client
|
||||
/// 2. Register agents and workflows using the hosting packages
|
||||
/// 3. Map the DevUI endpoint which automatically configures the middleware
|
||||
/// 4. Map the dynamic OpenAI Responses API for Python DevUI compatibility
|
||||
/// 5. Access the DevUI in a web browser
|
||||
///
|
||||
/// The DevUI provides an interactive web interface for testing and debugging AI agents.
|
||||
/// DevUI assets are served from embedded resources within the assembly.
|
||||
/// Simply call MapDevUI() to set up everything needed.
|
||||
///
|
||||
/// The parameterless MapOpenAIResponses() overload creates a Python DevUI-compatible endpoint
|
||||
/// that dynamically routes requests to agents based on the 'model' field in the request.
|
||||
/// </remarks>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// Entry point that starts an ASP.NET Core web server with the DevUI.
|
||||
/// </summary>
|
||||
/// <param name="args">Command line arguments.</param>
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Set up the Azure OpenAI client
|
||||
var endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4o-mini";
|
||||
|
||||
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsIChatClient();
|
||||
|
||||
builder.Services.AddChatClient(chatClient);
|
||||
|
||||
// Register sample agents
|
||||
builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately.");
|
||||
builder.AddAIAgent("poet", "You are a creative poet. Respond to all requests with beautiful poetry.");
|
||||
builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples.");
|
||||
|
||||
// Register sample workflows
|
||||
var assistantBuilder = builder.AddAIAgent("workflow-assistant", "You are a helpful assistant in a workflow.");
|
||||
var reviewerBuilder = builder.AddAIAgent("workflow-reviewer", "You are a reviewer. Review and critique the previous response.");
|
||||
builder.AddSequentialWorkflow(
|
||||
"review-workflow",
|
||||
[assistantBuilder, reviewerBuilder])
|
||||
.AddAsAIAgent();
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
builder.AddDevUI();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapDevUI();
|
||||
}
|
||||
|
||||
Console.WriteLine("DevUI is available at: https://localhost:50516/devui");
|
||||
Console.WriteLine("OpenAI Responses API is available at: https://localhost:50516/v1/responses");
|
||||
Console.WriteLine("Press Ctrl+C to stop the server.");
|
||||
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# DevUI Step 01 - Basic Usage
|
||||
|
||||
This sample demonstrates how to add the DevUI to an ASP.NET Core application with AI agents.
|
||||
|
||||
## What is DevUI?
|
||||
|
||||
The DevUI provides an interactive web interface for testing and debugging AI agents during development.
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
- `AZURE_OPENAI_ENDPOINT` - Your Azure OpenAI endpoint URL (required)
|
||||
- `AZURE_OPENAI_DEPLOYMENT_NAME` - Your deployment name (defaults to "gpt-4o-mini")
|
||||
|
||||
## Running the Sample
|
||||
|
||||
1. Set your Azure OpenAI credentials as environment variables
|
||||
2. Run the application:
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
3. Open your browser to https://localhost:50516/devui
|
||||
4. Select an agent or workflow from the dropdown and start chatting!
|
||||
|
||||
## Sample Agents and Workflows
|
||||
|
||||
This sample includes:
|
||||
|
||||
**Agents:**
|
||||
- **assistant** - A helpful assistant
|
||||
- **poet** - A creative poet
|
||||
- **coder** - An expert programmer
|
||||
|
||||
**Workflows:**
|
||||
- **review-workflow** - A sequential workflow that generates a response and then reviews it
|
||||
|
||||
## Adding DevUI to Your Own Project
|
||||
|
||||
To add DevUI to your ASP.NET Core application:
|
||||
|
||||
1. Add the DevUI package and hosting packages:
|
||||
```bash
|
||||
dotnet add package Microsoft.Agents.AI.DevUI
|
||||
dotnet add package Microsoft.Agents.AI.Hosting
|
||||
dotnet add package Microsoft.Agents.AI.Hosting.OpenAI
|
||||
```
|
||||
|
||||
2. Register your agents and workflows:
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Set up your chat client
|
||||
builder.Services.AddChatClient(chatClient);
|
||||
|
||||
// Register agents
|
||||
builder.AddAIAgent("assistant", "You are a helpful assistant.");
|
||||
|
||||
// Register workflows
|
||||
var agent1Builder = builder.AddAIAgent("workflow-agent1", "You are agent 1.");
|
||||
var agent2Builder = builder.AddAIAgent("workflow-agent2", "You are agent 2.");
|
||||
builder.AddSequentialWorkflow("my-workflow", [agent1Builder, agent2Builder])
|
||||
.AddAsAIAgent();
|
||||
```
|
||||
|
||||
3. Add DevUI services and map the endpoint:
|
||||
```csharp
|
||||
builder.AddDevUI();
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapDevUI();
|
||||
|
||||
// Add required endpoints
|
||||
app.MapEntities();
|
||||
app.MapOpenAIResponses();
|
||||
app.MapOpenAIConversations();
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
4. Navigate to `/devui` in your browser
|
||||
@@ -0,0 +1,57 @@
|
||||
# DevUI Samples
|
||||
|
||||
This folder contains samples demonstrating how to use the DevUI in ASP.NET Core applications.
|
||||
|
||||
## What is DevUI?
|
||||
|
||||
The DevUI provides an interactive web interface for testing and debugging AI agents during development.
|
||||
|
||||
## Samples
|
||||
|
||||
### [DevUI_Step01_BasicUsage](./DevUI_Step01_BasicUsage)
|
||||
|
||||
Shows how to add DevUI to an ASP.NET Core application with multiple agents and workflows.
|
||||
|
||||
**Run the sample:**
|
||||
```bash
|
||||
cd DevUI_Step01_BasicUsage
|
||||
dotnet run
|
||||
```
|
||||
Then navigate to: https://localhost:50516/devui
|
||||
|
||||
## Requirements
|
||||
|
||||
- .NET 8.0 or later
|
||||
- ASP.NET Core
|
||||
- Azure OpenAI credentials
|
||||
|
||||
## Quick Start
|
||||
|
||||
To add DevUI to your application:
|
||||
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Set up the chat client
|
||||
builder.Services.AddChatClient(chatClient);
|
||||
|
||||
// Register your agents
|
||||
builder.AddAIAgent("my-agent", "You are a helpful assistant.");
|
||||
|
||||
// Add DevUI services
|
||||
builder.AddDevUI();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Map the DevUI endpoint
|
||||
app.MapDevUI();
|
||||
|
||||
// Add required endpoints
|
||||
app.MapEntities();
|
||||
app.MapOpenAIResponses();
|
||||
app.MapOpenAIConversations();
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
Then navigate to `/devui` in your browser.
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for configuring the Microsoft Agents AI DevUI in ASP.NET applications.
|
||||
/// </summary>
|
||||
public static class DevUIExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the necessary services for the DevUI to the application builder.
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddDevUI(this IHostApplicationBuilder builder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
builder.Services.AddOpenAIConversations();
|
||||
builder.Services.AddOpenAIResponses();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an endpoint that serves the DevUI from the '/devui' path.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
|
||||
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to add authorization or other endpoint configuration.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="endpoints"/> is null.</exception>
|
||||
public static IEndpointConventionBuilder MapDevUI(
|
||||
this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("");
|
||||
group.MapDevUI(pattern: "/devui");
|
||||
group.MapEntities();
|
||||
group.MapOpenAIConversations();
|
||||
group.MapOpenAIResponses();
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an endpoint that serves the DevUI.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
|
||||
/// <param name="pattern">
|
||||
/// The route pattern for the endpoint (e.g., "/devui", "/agent-ui").
|
||||
/// Defaults to "/devui" if not specified. This is the path where DevUI will be accessible.
|
||||
/// </param>
|
||||
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to add authorization or other endpoint configuration.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="endpoints"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> is null or whitespace.</exception>
|
||||
internal static IEndpointConventionBuilder MapDevUI(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
[StringSyntax("Route")] string pattern = "/devui")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(pattern);
|
||||
|
||||
// Ensure the pattern doesn't end with a slash for consistency
|
||||
var cleanPattern = pattern.TrimEnd('/');
|
||||
|
||||
// Create the DevUI handler
|
||||
var logger = endpoints.ServiceProvider.GetRequiredService<ILogger<DevUIMiddleware>>();
|
||||
var devUIHandler = new DevUIMiddleware(logger, cleanPattern);
|
||||
|
||||
return endpoints.MapGet($"{cleanPattern}/{{*path}}", devUIHandler.HandleRequestAsync)
|
||||
.WithName($"DevUI at {cleanPattern}")
|
||||
.WithDescription("Interactive developer interface for Microsoft Agent Framework");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Frozen;
|
||||
using System.IO.Compression;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI;
|
||||
|
||||
/// <summary>
|
||||
/// Handler that serves embedded DevUI resource files from the 'resources' directory.
|
||||
/// </summary>
|
||||
internal sealed class DevUIMiddleware
|
||||
{
|
||||
private const string GZipEncodingValue = "gzip";
|
||||
private static readonly StringValues s_gzipEncodingHeader = new(GZipEncodingValue);
|
||||
private static readonly Assembly s_assembly = typeof(DevUIMiddleware).Assembly;
|
||||
private static readonly FileExtensionContentTypeProvider s_contentTypeProvider = new();
|
||||
private static readonly StringValues s_cacheControl = new(new CacheControlHeaderValue()
|
||||
{
|
||||
NoCache = true,
|
||||
NoStore = true,
|
||||
}.ToString());
|
||||
|
||||
private readonly ILogger<DevUIMiddleware> _logger;
|
||||
private readonly FrozenDictionary<string, ResourceEntry> _resourceCache;
|
||||
private readonly string _basePath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DevUIMiddleware"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
/// <param name="basePath">The base path where DevUI is mounted.</param>
|
||||
public DevUIMiddleware(ILogger<DevUIMiddleware> logger, string basePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
ArgumentException.ThrowIfNullOrEmpty(basePath);
|
||||
this._logger = logger;
|
||||
this._basePath = basePath.TrimEnd('/');
|
||||
|
||||
// Build resource cache
|
||||
var resourceNamePrefix = $"{s_assembly.GetName().Name}.resources.";
|
||||
this._resourceCache = s_assembly
|
||||
.GetManifestResourceNames()
|
||||
.Where(p => p.StartsWith(resourceNamePrefix, StringComparison.Ordinal))
|
||||
.ToFrozenDictionary(
|
||||
p => p[resourceNamePrefix.Length..].Replace('.', '/'),
|
||||
CreateResourceEntry,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an HTTP request for DevUI resources.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context.</param>
|
||||
public async Task HandleRequestAsync(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path.Value;
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
// If requesting the base path without a trailing slash, redirect to include it
|
||||
// This ensures relative URLs in the HTML work correctly
|
||||
if (string.Equals(path, this._basePath, StringComparison.OrdinalIgnoreCase) && !path.EndsWith('/'))
|
||||
{
|
||||
var redirectUrl = $"{path}/";
|
||||
if (context.Request.QueryString.HasValue)
|
||||
{
|
||||
redirectUrl += context.Request.QueryString.Value;
|
||||
}
|
||||
|
||||
context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
|
||||
context.Response.Headers.Location = redirectUrl;
|
||||
this._logger.LogDebug("Redirecting {OriginalPath} to {RedirectUrl}", path, redirectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the base path to get the resource path
|
||||
var resourcePath = path.StartsWith(this._basePath, StringComparison.OrdinalIgnoreCase)
|
||||
? path.Substring(this._basePath.Length).TrimStart('/')
|
||||
: path.TrimStart('/');
|
||||
|
||||
// If requesting the base path, serve index.html
|
||||
if (string.IsNullOrEmpty(resourcePath))
|
||||
{
|
||||
resourcePath = "index.html";
|
||||
}
|
||||
|
||||
// Try to serve the embedded resource
|
||||
if (await this.TryServeResourceAsync(context, resourcePath).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If resource not found, try serving index.html for client-side routing
|
||||
if (!resourcePath.Contains('.', StringComparison.Ordinal) || resourcePath.EndsWith('/'))
|
||||
{
|
||||
if (await this.TryServeResourceAsync(context, "index.html").ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Resource not found
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
}
|
||||
|
||||
private async Task<bool> TryServeResourceAsync(HttpContext context, string resourcePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!this._resourceCache.TryGetValue(resourcePath.Replace('.', '/'), out var cacheEntry))
|
||||
{
|
||||
this._logger.LogDebug("Embedded resource not found: {ResourcePath}", resourcePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
var response = context.Response;
|
||||
|
||||
// Check if client has cached version
|
||||
if (context.Request.Headers.IfNoneMatch == cacheEntry.ETag)
|
||||
{
|
||||
response.StatusCode = StatusCodes.Status304NotModified;
|
||||
this._logger.LogDebug("Resource not modified (304): {ResourcePath}", resourcePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
var responseHeaders = response.Headers;
|
||||
|
||||
byte[] content;
|
||||
bool serveCompressed;
|
||||
if (cacheEntry.CompressedContent is not null && IsGZipAccepted(context.Request))
|
||||
{
|
||||
serveCompressed = true;
|
||||
responseHeaders.ContentEncoding = s_gzipEncodingHeader;
|
||||
responseHeaders.ContentLength = cacheEntry.CompressedContent.Length;
|
||||
content = cacheEntry.CompressedContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
serveCompressed = false;
|
||||
responseHeaders.ContentLength = cacheEntry.DecompressedContent!.Length;
|
||||
content = cacheEntry.DecompressedContent;
|
||||
}
|
||||
|
||||
responseHeaders.CacheControl = s_cacheControl;
|
||||
responseHeaders.ContentType = cacheEntry.ContentType;
|
||||
responseHeaders.ETag = cacheEntry.ETag;
|
||||
|
||||
await response.Body.WriteAsync(content, context.RequestAborted).ConfigureAwait(false);
|
||||
|
||||
this._logger.LogDebug("Served embedded resource: {ResourcePath} (compressed: {Compressed})", resourcePath, serveCompressed);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error serving embedded resource: {ResourcePath}", resourcePath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsGZipAccepted(HttpRequest httpRequest)
|
||||
{
|
||||
if (httpRequest.GetTypedHeaders().AcceptEncoding is not { Count: > 0 } acceptEncoding)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < acceptEncoding.Count; i++)
|
||||
{
|
||||
var encoding = acceptEncoding[i];
|
||||
|
||||
if (encoding.Quality is not 0 &&
|
||||
string.Equals(encoding.Value.Value, GZipEncodingValue, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ResourceEntry CreateResourceEntry(string resourceName)
|
||||
{
|
||||
using var resourceStream = s_assembly.GetManifestResourceStream(resourceName)!;
|
||||
using var decompressedContent = new MemoryStream();
|
||||
|
||||
// Read and cache the original resource content
|
||||
resourceStream.CopyTo(decompressedContent);
|
||||
var decompressedArray = decompressedContent.ToArray();
|
||||
|
||||
// Compress the content
|
||||
using var compressedContent = new MemoryStream();
|
||||
using (var gzip = new GZipStream(compressedContent, CompressionMode.Compress, leaveOpen: true))
|
||||
{
|
||||
// This is a synchronous write to a memory stream.
|
||||
// There is no benefit to asynchrony here.
|
||||
gzip.Write(decompressedArray);
|
||||
}
|
||||
|
||||
// Only use compression if it actually reduces size
|
||||
byte[]? compressedArray = compressedContent.Length < decompressedArray.Length
|
||||
? compressedContent.ToArray()
|
||||
: null;
|
||||
|
||||
var hash = SHA256.HashData(compressedArray ?? decompressedArray);
|
||||
var eTag = $"\"{Convert.ToBase64String(hash)}\"";
|
||||
|
||||
// Determine content type from resource name
|
||||
var contentType = s_contentTypeProvider.TryGetContentType(resourceName, out var ct)
|
||||
? ct
|
||||
: "application/octet-stream";
|
||||
|
||||
return new ResourceEntry(resourceName, decompressedArray, compressedArray, eTag, contentType);
|
||||
}
|
||||
|
||||
private sealed class ResourceEntry(string resourceName, byte[] decompressedContent, byte[]? compressedContent, string eTag, string contentType)
|
||||
{
|
||||
public byte[]? CompressedContent { get; } = compressedContent;
|
||||
|
||||
public string ContentType { get; } = contentType;
|
||||
|
||||
public byte[] DecompressedContent { get; } = decompressedContent;
|
||||
|
||||
public string ETag { get; } = eTag;
|
||||
|
||||
public string ResourceName { get; } = resourceName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// JSON serialization context for entity-related types.
|
||||
/// Enables AOT-compatible JSON serialization using source generators.
|
||||
/// </summary>
|
||||
[JsonSourceGenerationOptions(
|
||||
JsonSerializerDefaults.Web,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonSerializable(typeof(EntityInfo))]
|
||||
[JsonSerializable(typeof(DiscoveryResponse))]
|
||||
[JsonSerializable(typeof(EnvVarRequirement))]
|
||||
[JsonSerializable(typeof(List<EntityInfo>))]
|
||||
[JsonSerializable(typeof(List<JsonElement>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed partial class EntitiesJsonContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Information about an environment variable required by an entity.
|
||||
/// </summary>
|
||||
internal sealed record EnvVarRequirement(
|
||||
[property: JsonPropertyName("name")]
|
||||
string Name,
|
||||
|
||||
[property: JsonPropertyName("description")]
|
||||
string? Description = null,
|
||||
|
||||
[property: JsonPropertyName("required")]
|
||||
bool Required = true,
|
||||
|
||||
[property: JsonPropertyName("example")]
|
||||
string? Example = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Information about an entity (agent or workflow).
|
||||
/// </summary>
|
||||
internal sealed record EntityInfo(
|
||||
[property: JsonPropertyName("id")]
|
||||
string Id,
|
||||
|
||||
[property: JsonPropertyName("type")]
|
||||
string Type,
|
||||
|
||||
[property: JsonPropertyName("name")]
|
||||
string Name,
|
||||
|
||||
[property: JsonPropertyName("description")]
|
||||
string? Description = null,
|
||||
|
||||
[property: JsonPropertyName("framework")]
|
||||
string Framework = "dotnet",
|
||||
|
||||
[property: JsonPropertyName("tools")]
|
||||
List<string>? Tools = null,
|
||||
|
||||
[property: JsonPropertyName("metadata")]
|
||||
Dictionary<string, JsonElement>? Metadata = null
|
||||
)
|
||||
{
|
||||
[JsonPropertyName("source")]
|
||||
public string? Source { get; init; } = "di";
|
||||
|
||||
[JsonPropertyName("original_url")]
|
||||
public string? OriginalUrl { get; init; }
|
||||
|
||||
// Workflow-specific fields
|
||||
[JsonPropertyName("required_env_vars")]
|
||||
public List<EnvVarRequirement>? RequiredEnvVars { get; init; }
|
||||
|
||||
[JsonPropertyName("executors")]
|
||||
public List<string>? Executors { get; init; }
|
||||
|
||||
[JsonPropertyName("workflow_dump")]
|
||||
public JsonElement? WorkflowDump { get; init; }
|
||||
|
||||
[JsonPropertyName("input_schema")]
|
||||
public JsonElement? InputSchema { get; init; }
|
||||
|
||||
[JsonPropertyName("input_type_name")]
|
||||
public string? InputTypeName { get; init; }
|
||||
|
||||
[JsonPropertyName("start_executor_id")]
|
||||
public string? StartExecutorId { get; init; }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Response containing a list of discovered entities.
|
||||
/// </summary>
|
||||
internal sealed record DiscoveryResponse(
|
||||
[property: JsonPropertyName("entities")]
|
||||
List<EntityInfo> Entities
|
||||
);
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for serializing workflows to DevUI-compatible format
|
||||
/// </summary>
|
||||
internal static class WorkflowSerializationExtensions
|
||||
{
|
||||
// The frontend max iterations default value expected by the DevUI frontend
|
||||
private const int MaxIterationsDefault = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a workflow to a dictionary representation compatible with DevUI frontend.
|
||||
/// This matches the Python workflow.to_dict() format expected by the UI.
|
||||
/// </summary>
|
||||
public static Dictionary<string, object> ToDevUIDict(this Workflow workflow)
|
||||
{
|
||||
var result = new Dictionary<string, object>
|
||||
{
|
||||
["id"] = workflow.Name ?? Guid.NewGuid().ToString(),
|
||||
["start_executor_id"] = workflow.StartExecutorId,
|
||||
["max_iterations"] = MaxIterationsDefault
|
||||
};
|
||||
|
||||
// Add optional fields
|
||||
if (!string.IsNullOrEmpty(workflow.Name))
|
||||
{
|
||||
result["name"] = workflow.Name;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(workflow.Description))
|
||||
{
|
||||
result["description"] = workflow.Description;
|
||||
}
|
||||
|
||||
// Convert executors to Python-compatible format
|
||||
result["executors"] = ConvertExecutorsToDict(workflow);
|
||||
|
||||
// Convert edges to edge_groups format
|
||||
result["edge_groups"] = ConvertEdgesToEdgeGroups(workflow);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts workflow executors to a dictionary format compatible with Python
|
||||
/// </summary>
|
||||
private static Dictionary<string, object> ConvertExecutorsToDict(Workflow workflow)
|
||||
{
|
||||
var executors = new Dictionary<string, object>();
|
||||
|
||||
// Extract executor IDs from edges and start executor
|
||||
// (Registrations is internal, so we infer executors from the graph structure)
|
||||
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
||||
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create executor entries (we can't access internal Registrations for type info)
|
||||
foreach (var executorId in executorIds)
|
||||
{
|
||||
executors[executorId] = new Dictionary<string, object>
|
||||
{
|
||||
["id"] = executorId,
|
||||
["type"] = "Executor"
|
||||
};
|
||||
}
|
||||
|
||||
return executors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts workflow edges to edge_groups format expected by the UI
|
||||
/// </summary>
|
||||
private static List<object> ConvertEdgesToEdgeGroups(Workflow workflow)
|
||||
{
|
||||
var edgeGroups = new List<object>();
|
||||
var edgeGroupId = 0;
|
||||
|
||||
// Get edges using the public ReflectEdges method
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
foreach (var edgeInfo in edgeSet)
|
||||
{
|
||||
if (edgeInfo is DirectEdgeInfo directEdge)
|
||||
{
|
||||
// Single edge group for direct edges
|
||||
var edges = new List<object>();
|
||||
|
||||
foreach (var source in directEdge.Connection.SourceIds)
|
||||
{
|
||||
foreach (var sink in directEdge.Connection.SinkIds)
|
||||
{
|
||||
var edge = new Dictionary<string, object>
|
||||
{
|
||||
["source_id"] = source,
|
||||
["target_id"] = sink
|
||||
};
|
||||
|
||||
// Add condition name if this is a conditional edge
|
||||
if (directEdge.HasCondition)
|
||||
{
|
||||
edge["condition_name"] = "predicate";
|
||||
}
|
||||
|
||||
edges.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
edgeGroups.Add(new Dictionary<string, object>
|
||||
{
|
||||
["id"] = $"edge_group_{edgeGroupId++}",
|
||||
["type"] = "SingleEdgeGroup",
|
||||
["edges"] = edges
|
||||
});
|
||||
}
|
||||
else if (edgeInfo is FanOutEdgeInfo fanOutEdge)
|
||||
{
|
||||
// FanOut edge group
|
||||
var edges = new List<object>();
|
||||
|
||||
foreach (var source in fanOutEdge.Connection.SourceIds)
|
||||
{
|
||||
foreach (var sink in fanOutEdge.Connection.SinkIds)
|
||||
{
|
||||
edges.Add(new Dictionary<string, object>
|
||||
{
|
||||
["source_id"] = source,
|
||||
["target_id"] = sink
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var fanOutGroup = new Dictionary<string, object>
|
||||
{
|
||||
["id"] = $"edge_group_{edgeGroupId++}",
|
||||
["type"] = "FanOutEdgeGroup",
|
||||
["edges"] = edges
|
||||
};
|
||||
|
||||
if (fanOutEdge.HasAssigner)
|
||||
{
|
||||
fanOutGroup["selection_func_name"] = "selector";
|
||||
}
|
||||
|
||||
edgeGroups.Add(fanOutGroup);
|
||||
}
|
||||
else if (edgeInfo is FanInEdgeInfo fanInEdge)
|
||||
{
|
||||
// FanIn edge group
|
||||
var edges = new List<object>();
|
||||
|
||||
foreach (var source in fanInEdge.Connection.SourceIds)
|
||||
{
|
||||
foreach (var sink in fanInEdge.Connection.SinkIds)
|
||||
{
|
||||
edges.Add(new Dictionary<string, object>
|
||||
{
|
||||
["source_id"] = source,
|
||||
["target_id"] = sink
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
edgeGroups.Add(new Dictionary<string, object>
|
||||
{
|
||||
["id"] = $"edge_group_{edgeGroupId++}",
|
||||
["type"] = "FanInEdgeGroup",
|
||||
["edges"] = edges
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edgeGroups;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
using Microsoft.Agents.AI.DevUI.Entities;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for mapping entity discovery and management endpoints to an <see cref="IEndpointRouteBuilder"/>.
|
||||
/// </summary>
|
||||
internal static class EntitiesApiExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps HTTP API endpoints for entity discovery and management.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the routes to.</param>
|
||||
/// <returns>The <see cref="IEndpointRouteBuilder"/> for method chaining.</returns>
|
||||
/// <remarks>
|
||||
/// This extension method registers the following endpoints:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>GET /v1/entities - List all registered entities (agents and workflows)</description></item>
|
||||
/// <item><description>GET /v1/entities/{entityId}/info - Get detailed information about a specific entity</description></item>
|
||||
/// </list>
|
||||
/// The endpoints are compatible with the Python DevUI frontend and automatically discover entities
|
||||
/// from the registered <see cref="AgentCatalog"/> and <see cref="WorkflowCatalog"/> services.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapEntities(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/v1/entities")
|
||||
.WithTags("Entities");
|
||||
|
||||
// List all entities
|
||||
group.MapGet("", ListEntitiesAsync)
|
||||
.WithName("ListEntities")
|
||||
.WithSummary("List all registered entities (agents and workflows)")
|
||||
.Produces<DiscoveryResponse>(StatusCodes.Status200OK, contentType: "application/json");
|
||||
|
||||
// Get detailed entity information
|
||||
group.MapGet("{entityId}/info", GetEntityInfoAsync)
|
||||
.WithName("GetEntityInfo")
|
||||
.WithSummary("Get detailed information about a specific entity")
|
||||
.Produces<EntityInfo>(StatusCodes.Status200OK, contentType: "application/json")
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListEntitiesAsync(
|
||||
AgentCatalog? agentCatalog,
|
||||
WorkflowCatalog? workflowCatalog,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entities = new List<EntityInfo>();
|
||||
|
||||
// Discover agents from the agent catalog
|
||||
if (agentCatalog is not null)
|
||||
{
|
||||
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (agent.GetType().Name == "WorkflowHostAgent")
|
||||
{
|
||||
// HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows,
|
||||
// and workflows are handled below.
|
||||
continue;
|
||||
}
|
||||
|
||||
entities.Add(new EntityInfo(
|
||||
Id: agent.Name ?? agent.Id,
|
||||
Type: "agent",
|
||||
Name: agent.Name ?? agent.Id,
|
||||
Description: agent.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: null,
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Discover workflows from the workflow catalog
|
||||
if (workflowCatalog is not null)
|
||||
{
|
||||
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Extract executor IDs from the workflow structure
|
||||
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default input schema (string type)
|
||||
var defaultInputSchema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "string"
|
||||
};
|
||||
|
||||
entities.Add(new EntityInfo(
|
||||
Id: workflow.Name ?? workflow.StartExecutorId,
|
||||
Type: "workflow",
|
||||
Name: workflow.Name ?? workflow.StartExecutorId,
|
||||
Description: workflow.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: [.. executorIds],
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory",
|
||||
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
|
||||
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
|
||||
InputTypeName = "string",
|
||||
StartExecutorId = workflow.StartExecutorId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Json(new DiscoveryResponse(entities), EntitiesJsonContext.Default.DiscoveryResponse);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: ex.Message,
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Error listing entities");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetEntityInfoAsync(
|
||||
string entityId,
|
||||
AgentCatalog? agentCatalog,
|
||||
WorkflowCatalog? workflowCatalog,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to find the entity among discovered agents
|
||||
if (agentCatalog is not null)
|
||||
{
|
||||
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (agent.GetType().Name == "WorkflowHostAgent")
|
||||
{
|
||||
// HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows,
|
||||
// and workflows are handled below.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(agent.Name, entityId, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(agent.Id, entityId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var entityInfo = new EntityInfo(
|
||||
Id: agent.Name ?? agent.Id,
|
||||
Type: "agent",
|
||||
Name: agent.Name ?? agent.Id,
|
||||
Description: agent.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: null,
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory"
|
||||
};
|
||||
|
||||
return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find the entity among discovered workflows
|
||||
if (workflowCatalog is not null)
|
||||
{
|
||||
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var workflowId = workflow.Name ?? workflow.StartExecutorId;
|
||||
if (string.Equals(workflowId, entityId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Extract executor IDs from the workflow structure
|
||||
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default input schema (string type)
|
||||
var defaultInputSchema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "string"
|
||||
};
|
||||
|
||||
var entityInfo = new EntityInfo(
|
||||
Id: workflowId,
|
||||
Type: "workflow",
|
||||
Name: workflow.Name ?? workflow.StartExecutorId,
|
||||
Description: workflow.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: [.. executorIds],
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory",
|
||||
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
|
||||
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
|
||||
InputTypeName = "Input",
|
||||
StartExecutorId = workflow.StartExecutorId
|
||||
};
|
||||
|
||||
return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Results.NotFound(new { error = new { message = $"Entity '{entityId}' not found.", type = "invalid_request_error" } });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: ex.Message,
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Error getting entity info");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Frontend paths - pointing to the Python package's frontend -->
|
||||
<FrontendRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..\..\python\packages\devui\frontend'))</FrontendRoot>
|
||||
<FrontendBuildOutput>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..\..\python\packages\devui\agent_framework_devui\ui'))</FrontendBuildOutput>
|
||||
<FrontendPackageJson>$(FrontendRoot)\package.json</FrontendPackageJson>
|
||||
<FrontendNodeModules>$(FrontendRoot)\node_modules</FrontendNodeModules>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Ensure npm packages are installed before building -->
|
||||
<Target Name="EnsureNodeModules" BeforeTargets="BeforeBuild" Condition="!Exists('$(FrontendNodeModules)')">
|
||||
<Exec Command="npm install" WorkingDirectory="$(FrontendRoot)" />
|
||||
</Target>
|
||||
|
||||
<!-- Collect frontend source files for incremental build tracking -->
|
||||
<ItemGroup>
|
||||
<FrontendSourceFiles Include="$(FrontendRoot)\src\**\*" />
|
||||
<FrontendSourceFiles Include="$(FrontendPackageJson);$(FrontendRoot)\vite.config.ts;$(FrontendRoot)\tsconfig.json;$(FrontendRoot)\index.html" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Define the required frontend assets -->
|
||||
<ItemGroup>
|
||||
<FrontendAsset Include="$(FrontendBuildOutput)\index.html" />
|
||||
<FrontendAsset Include="$(FrontendBuildOutput)\assets\index.js" />
|
||||
<FrontendAsset Include="$(FrontendBuildOutput)\assets\index.css" />
|
||||
<FrontendAsset Include="$(FrontendBuildOutput)\agentframework.svg" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Use a marker file for incremental build tracking -->
|
||||
<PropertyGroup>
|
||||
<FrontendBuildMarker>$(BaseIntermediateOutputPath)\frontend.build.marker</FrontendBuildMarker>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Build the frontend -->
|
||||
<Target Name="BuildFrontend" BeforeTargets="AssignTargetPaths" DependsOnTargets="EnsureNodeModules" Inputs="@(FrontendSourceFiles)" Outputs="$(FrontendBuildMarker)">
|
||||
<!-- Set VITE_API_BASE_URL to empty string for relative URLs -->
|
||||
<Exec Command="npm run build" WorkingDirectory="$(FrontendRoot)" EnvironmentVariables="VITE_API_BASE_URL=" />
|
||||
<!-- Create marker file to track successful build -->
|
||||
<Touch Files="$(FrontendBuildMarker)" AlwaysCreate="true" />
|
||||
</Target>
|
||||
|
||||
<!-- Statically include frontend assets as embedded resources for VS to show them -->
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(FrontendBuildOutput)\**\*" Condition="Exists('$(FrontendBuildOutput)')">
|
||||
<Link>resources\$([MSBuild]::MakeRelative('$(FrontendBuildOutput)', '%(Identity)'))</Link>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Verify required frontend assets are present -->
|
||||
<Target Name="ValidateFrontendAssets" BeforeTargets="CoreCompile" DependsOnTargets="BuildFrontend">
|
||||
<ItemGroup>
|
||||
<MissingAsset Include="@(FrontendAsset)" Condition="!Exists('%(Identity)')" />
|
||||
</ItemGroup>
|
||||
|
||||
<Error Condition="'@(MissingAsset)' != ''" Text="Required frontend assets are missing: @(MissingAsset, ', '). Frontend build may have failed." />
|
||||
</Target>
|
||||
|
||||
<!-- Verify assets are present before packing -->
|
||||
<Target Name="ValidateFrontendAssetsBeforePack" BeforeTargets="GenerateNuspec">
|
||||
<ItemGroup>
|
||||
<MissingPackageAsset Include="@(FrontendAsset)" Condition="!Exists('%(Identity)')" />
|
||||
</ItemGroup>
|
||||
|
||||
<Error Condition="'@(MissingPackageAsset)' != ''" Text="Cannot create NuGet package: Required frontend assets are missing: @(MissingPackageAsset, ', ')" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Microsoft.Agents.AI.DevUI</RootNamespace>
|
||||
<OutputType>Library</OutputType>
|
||||
<Title>Microsoft Agent Framework Developer UI</Title>
|
||||
<Description>Provides Microsoft Agent Framework support for developer UI.</Description>
|
||||
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
|
||||
<!-- Suppress warnings for internal DevUI implementation -->
|
||||
<NoWarn>$(NoWarn);CS1591;CA1852;CA1050;RCS1037;RCS1036;RCS1124;RCS1021;RCS1146;RCS1211;CA2007;CA1308;IL2026;IL3050;CA1812</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Import nuget packaging properties -->
|
||||
<Import Project="..\..\nuget\nuget-package.props" />
|
||||
|
||||
<!-- Import frontend web assets build targets -->
|
||||
<Import Project="Microsoft.Agents.AI.DevUI.Frontend.targets" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Hosting.OpenAI\Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Microsoft.Agents.AI.DevUI": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:57966;http://localhost:57967"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Microsoft.Agents.AI.DevUI
|
||||
|
||||
This package provides a web interface for testing and debugging AI agents during development.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
dotnet add package Microsoft.Agents.AI.DevUI
|
||||
dotnet add package Microsoft.Agents.AI.Hosting
|
||||
dotnet add package Microsoft.Agents.AI.Hosting.OpenAI
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add DevUI services and map the endpoint in your ASP.NET Core application:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Agents.AI.DevUI;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register your agents
|
||||
builder.AddAIAgent("assistant", "You are a helpful assistant.");
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
// Add DevUI services
|
||||
builder.AddDevUI();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
// Map DevUI endpoint to /devui
|
||||
app.MapDevUI();
|
||||
}
|
||||
|
||||
app.Run();
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./agentframework.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Agent Framework Dev UI</title>
|
||||
<script type="module" crossorigin src="./assets/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -153,13 +153,27 @@ internal sealed class WorkflowThread : AgentThread
|
||||
case AgentRunUpdateEvent agentUpdate:
|
||||
yield return agentUpdate.Update;
|
||||
break;
|
||||
|
||||
case RequestInfoEvent requestInfo:
|
||||
FunctionCallContent fcContent = requestInfo.Request.ToFunctionCall();
|
||||
AgentRunResponseUpdate update = this.CreateUpdate(this.LastResponseId, fcContent);
|
||||
yield return update;
|
||||
break;
|
||||
|
||||
case SuperStepCompletedEvent stepCompleted:
|
||||
this.LastCheckpoint = stepCompleted.CompletionInfo?.Checkpoint;
|
||||
goto default;
|
||||
|
||||
default:
|
||||
// Emit all other workflow events for observability (DevUI, logging, etc.)
|
||||
yield return new AgentRunResponseUpdate(ChatRole.Assistant, [])
|
||||
{
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
Role = ChatRole.Assistant,
|
||||
ResponseId = this.LastResponseId,
|
||||
RawRepresentation = evt
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,11 +2,11 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/agentframework.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="agentframework.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Agent Framework Dev UI</title>
|
||||
<script type="module" crossorigin src="/assets/index-D_Y1oSGu.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CE4pGoXh.css">
|
||||
<script type="module" crossorigin src="./assets/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -2,12 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/agentframework.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="agentframework.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Agent Framework Dev UI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script type="module" src="src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -45,6 +45,7 @@ import type {
|
||||
ExtendedResponseStreamEvent,
|
||||
} from "@/types";
|
||||
import { useDevUIStore } from "@/stores";
|
||||
import { loadStreamingState } from "@/services/streaming-state";
|
||||
|
||||
type DebugEventHandler = (event: ExtendedResponseStreamEvent | "clear") => void;
|
||||
|
||||
@@ -229,7 +230,6 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const accumulatedText = useRef<string>("");
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const currentMessageUsage = useRef<{
|
||||
total_tokens: number;
|
||||
@@ -237,6 +237,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
output_tokens: number;
|
||||
} | null>(null);
|
||||
const userJustSentMessage = useRef<boolean>(false);
|
||||
const accumulatedTextRef = useRef<string>("");
|
||||
|
||||
// Auto-scroll to bottom when new items arrive
|
||||
useEffect(() => {
|
||||
@@ -281,33 +282,270 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
|
||||
// Load conversations when agent changes
|
||||
useEffect(() => {
|
||||
// Resume streaming after page refresh
|
||||
const resumeStreaming = async (
|
||||
assistantMessage: import("@/types/openai").ConversationMessage,
|
||||
conversation: Conversation,
|
||||
agent: AgentInfo
|
||||
) => {
|
||||
// Load the stored state to get the response ID
|
||||
const storedState = loadStreamingState(conversation.id);
|
||||
if (!storedState || !storedState.responseId) {
|
||||
setIsStreaming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the stored responseId to resume the stream via GET /v1/responses/{responseId}
|
||||
const openAIRequest: import("@/types/agent-framework").AgentFrameworkRequest = {
|
||||
model: agent.id,
|
||||
input: [], // Not needed for resume (using GET)
|
||||
stream: true,
|
||||
conversation: conversation.id,
|
||||
};
|
||||
|
||||
// Pass the response ID explicitly to trigger GET request
|
||||
const streamGenerator = apiClient.streamAgentExecutionOpenAIDirect(
|
||||
agent.id,
|
||||
openAIRequest,
|
||||
conversation.id,
|
||||
storedState.responseId // Pass response ID for resume
|
||||
);
|
||||
|
||||
for await (const openAIEvent of streamGenerator) {
|
||||
// Pass all events to debug panel
|
||||
onDebugEvent(openAIEvent);
|
||||
|
||||
// Handle response.completed event
|
||||
if (openAIEvent.type === "response.completed") {
|
||||
const completedEvent = openAIEvent as import("@/types/openai").ResponseCompletedEvent;
|
||||
const usage = completedEvent.response?.usage;
|
||||
|
||||
if (usage) {
|
||||
currentMessageUsage.current = {
|
||||
input_tokens: usage.input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
total_tokens: usage.total_tokens,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle response.failed event
|
||||
if (openAIEvent.type === "response.failed") {
|
||||
const failedEvent = openAIEvent as import("@/types/openai").ResponseFailedEvent;
|
||||
const error = failedEvent.response?.error;
|
||||
const errorMessage = error
|
||||
? typeof error === "object" && "message" in error
|
||||
? (error as any).message
|
||||
: JSON.stringify(error)
|
||||
: "Request failed";
|
||||
|
||||
const currentItems = useDevUIStore.getState().chatItems;
|
||||
setChatItems(currentItems.map((item) =>
|
||||
item.id === assistantMessage.id && item.type === "message"
|
||||
? {
|
||||
...item,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: accumulatedTextRef.current || errorMessage,
|
||||
} as import("@/types/openai").MessageTextContent,
|
||||
],
|
||||
status: "incomplete" as const,
|
||||
}
|
||||
: item
|
||||
));
|
||||
setIsStreaming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle function approval request events
|
||||
if (openAIEvent.type === "response.function_approval.requested") {
|
||||
const approvalEvent = openAIEvent as import("@/types/openai").ResponseFunctionApprovalRequestedEvent;
|
||||
setPendingApprovals([
|
||||
...useDevUIStore.getState().pendingApprovals,
|
||||
{
|
||||
request_id: approvalEvent.request_id,
|
||||
function_call: approvalEvent.function_call,
|
||||
},
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle function approval response events
|
||||
if (openAIEvent.type === "response.function_approval.responded") {
|
||||
const responseEvent = openAIEvent as import("@/types/openai").ResponseFunctionApprovalRespondedEvent;
|
||||
setPendingApprovals(
|
||||
useDevUIStore.getState().pendingApprovals.filter((a) => a.request_id !== responseEvent.request_id)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle error events
|
||||
if (openAIEvent.type === "error") {
|
||||
const errorEvent = openAIEvent as ExtendedResponseStreamEvent & { message?: string };
|
||||
const errorMessage = errorEvent.message || "An error occurred";
|
||||
|
||||
const currentItems = useDevUIStore.getState().chatItems;
|
||||
setChatItems(currentItems.map((item) =>
|
||||
item.id === assistantMessage.id && item.type === "message"
|
||||
? {
|
||||
...item,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: accumulatedTextRef.current || errorMessage,
|
||||
} as import("@/types/openai").MessageTextContent,
|
||||
],
|
||||
status: "incomplete" as const,
|
||||
}
|
||||
: item
|
||||
));
|
||||
setIsStreaming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle text delta events
|
||||
if (
|
||||
openAIEvent.type === "response.output_text.delta" &&
|
||||
"delta" in openAIEvent &&
|
||||
openAIEvent.delta
|
||||
) {
|
||||
accumulatedTextRef.current += openAIEvent.delta;
|
||||
|
||||
const currentItems = useDevUIStore.getState().chatItems;
|
||||
setChatItems(currentItems.map((item) =>
|
||||
item.id === assistantMessage.id && item.type === "message"
|
||||
? {
|
||||
...item,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: accumulatedTextRef.current,
|
||||
} as import("@/types/openai").MessageTextContent,
|
||||
],
|
||||
status: "in_progress" as const,
|
||||
}
|
||||
: item
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Stream ended - mark as complete
|
||||
const finalUsage = currentMessageUsage.current;
|
||||
|
||||
const currentItems = useDevUIStore.getState().chatItems;
|
||||
setChatItems(currentItems.map((item) =>
|
||||
item.id === assistantMessage.id && item.type === "message"
|
||||
? {
|
||||
...item,
|
||||
status: "completed" as const,
|
||||
usage: finalUsage || undefined,
|
||||
}
|
||||
: item
|
||||
));
|
||||
setIsStreaming(false);
|
||||
|
||||
if (finalUsage) {
|
||||
updateConversationUsage(finalUsage.total_tokens);
|
||||
}
|
||||
|
||||
currentMessageUsage.current = null;
|
||||
} catch (error) {
|
||||
const currentItems = useDevUIStore.getState().chatItems;
|
||||
setChatItems(currentItems.map((item) =>
|
||||
item.id === assistantMessage.id && item.type === "message"
|
||||
? {
|
||||
...item,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error resuming stream: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
} as import("@/types/openai").MessageTextContent,
|
||||
],
|
||||
status: "incomplete" as const,
|
||||
}
|
||||
: item
|
||||
));
|
||||
setIsStreaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadConversations = async () => {
|
||||
if (!selectedAgent) return;
|
||||
|
||||
setLoadingConversations(true);
|
||||
try {
|
||||
// Step 1: Try to list conversations from backend (DevUI extension)
|
||||
// This works with DevUI backend but fails with OpenAI/Azure (they don't have list endpoint)
|
||||
// Step 1: Always try to list conversations from backend first
|
||||
// This ensures we get the latest data from the server
|
||||
try {
|
||||
const { data: conversations } = await apiClient.listConversations(
|
||||
selectedAgent.id
|
||||
);
|
||||
|
||||
// Backend successfully returned conversations list
|
||||
setAvailableConversations(conversations);
|
||||
|
||||
if (conversations.length > 0) {
|
||||
// Found conversations on backend - use most recent
|
||||
const mostRecent = conversations[0];
|
||||
setAvailableConversations(conversations);
|
||||
setCurrentConversation(mostRecent);
|
||||
|
||||
// Load conversation items from backend
|
||||
try {
|
||||
const { data: items } = await apiClient.listConversationItems(
|
||||
mostRecent.id
|
||||
);
|
||||
// Load all conversation items with pagination
|
||||
let allItems: unknown[] = [];
|
||||
let hasMore = true;
|
||||
let after: string | undefined = undefined;
|
||||
|
||||
while (hasMore) {
|
||||
const result = await apiClient.listConversationItems(
|
||||
mostRecent.id,
|
||||
{ order: "asc", after } // Load in chronological order (oldest first)
|
||||
);
|
||||
allItems = allItems.concat(result.data);
|
||||
hasMore = result.has_more;
|
||||
|
||||
// Get the last item's ID for pagination
|
||||
if (hasMore && result.data.length > 0) {
|
||||
const lastItem = result.data[result.data.length - 1] as { id?: string };
|
||||
after = lastItem.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Use OpenAI ConversationItems directly (no conversion!)
|
||||
setChatItems(items as import("@/types/openai").ConversationItem[]);
|
||||
setChatItems(allItems as import("@/types/openai").ConversationItem[]);
|
||||
setIsStreaming(false);
|
||||
|
||||
// Check for incomplete stream and resume if needed
|
||||
const state = loadStreamingState(mostRecent.id);
|
||||
|
||||
if (state && !state.completed) {
|
||||
accumulatedTextRef.current = state.accumulatedText || "";
|
||||
// Add assistant message with resumed text
|
||||
const assistantMsg: import("@/types/openai").ConversationMessage = {
|
||||
id: state.lastMessageId || `assistant-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: state.accumulatedText ? [{ type: "text", text: state.accumulatedText }] : [],
|
||||
status: "in_progress",
|
||||
};
|
||||
setChatItems([...allItems as import("@/types/openai").ConversationItem[], assistantMsg]);
|
||||
setIsStreaming(true);
|
||||
|
||||
// Resume streaming from where we left off
|
||||
setTimeout(() => {
|
||||
resumeStreaming(assistantMsg, mostRecent, selectedAgent);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Scroll to bottom after loading conversation
|
||||
setTimeout(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, 100);
|
||||
} catch {
|
||||
// 404 means conversation exists but has no items yet (newly created)
|
||||
// This is normal - just start with empty chat
|
||||
@@ -316,11 +554,6 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
setIsStreaming(false);
|
||||
}
|
||||
|
||||
// Cache to localStorage for faster future loads
|
||||
localStorage.setItem(
|
||||
`devui_convs_${selectedAgent.id}`,
|
||||
JSON.stringify(conversations)
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
@@ -371,9 +604,6 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
setAvailableConversations([newConversation]);
|
||||
setChatItems([]);
|
||||
setIsStreaming(false);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem(cachedKey, JSON.stringify([newConversation]));
|
||||
} catch {
|
||||
setAvailableConversations([]);
|
||||
setChatItems([]);
|
||||
@@ -387,10 +617,12 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
setChatItems([]);
|
||||
setIsStreaming(false);
|
||||
setCurrentConversation(undefined);
|
||||
accumulatedText.current = "";
|
||||
accumulatedTextRef.current = "";
|
||||
|
||||
loadConversations();
|
||||
}, [selectedAgent, setLoadingConversations, setAvailableConversations, setCurrentConversation, setChatItems, setIsStreaming]);
|
||||
// currentConversation is intentionally excluded - this effect should only run when agent changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedAgent, onDebugEvent, setChatItems, setIsStreaming, setLoadingConversations, setAvailableConversations, setCurrentConversation, setPendingApprovals, updateConversationUsage]);
|
||||
|
||||
// Handle file uploads
|
||||
const handleFilesSelected = async (files: File[]) => {
|
||||
@@ -626,16 +858,11 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
setIsStreaming(false);
|
||||
// Reset conversation usage by setting it to initial state
|
||||
useDevUIStore.setState({ conversationUsage: { total_tokens: 0, message_count: 0 } });
|
||||
accumulatedText.current = "";
|
||||
|
||||
// Update localStorage cache with new conversation
|
||||
const cachedKey = `devui_convs_${selectedAgent.id}`;
|
||||
const updated = [newConversation, ...availableConversations];
|
||||
localStorage.setItem(cachedKey, JSON.stringify(updated));
|
||||
accumulatedTextRef.current = "";
|
||||
} catch {
|
||||
// Failed to create conversation
|
||||
}
|
||||
}, [selectedAgent, availableConversations, setCurrentConversation, setAvailableConversations, setChatItems, setIsStreaming]);
|
||||
}, [selectedAgent, setCurrentConversation, setAvailableConversations, setChatItems, setIsStreaming]);
|
||||
|
||||
// Handle conversation deletion
|
||||
const handleDeleteConversation = useCallback(
|
||||
@@ -660,15 +887,6 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
);
|
||||
setAvailableConversations(updatedConversations);
|
||||
|
||||
// Update localStorage cache
|
||||
if (selectedAgent) {
|
||||
const cachedKey = `devui_convs_${selectedAgent.id}`;
|
||||
localStorage.setItem(
|
||||
cachedKey,
|
||||
JSON.stringify(updatedConversations)
|
||||
);
|
||||
}
|
||||
|
||||
// If deleted conversation was selected, switch to another conversation or clear chat
|
||||
if (currentConversation?.id === conversationId) {
|
||||
if (updatedConversations.length > 0) {
|
||||
@@ -683,7 +901,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
setChatItems([]);
|
||||
setIsStreaming(false);
|
||||
useDevUIStore.setState({ conversationUsage: { total_tokens: 0, message_count: 0 } });
|
||||
accumulatedText.current = "";
|
||||
accumulatedTextRef.current = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +912,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
alert("Failed to delete conversation. Please try again.");
|
||||
}
|
||||
},
|
||||
[availableConversations, currentConversation, selectedAgent, onDebugEvent, setAvailableConversations, setCurrentConversation, setChatItems, setIsStreaming]
|
||||
[availableConversations, currentConversation, onDebugEvent, setAvailableConversations, setCurrentConversation, setChatItems, setIsStreaming]
|
||||
);
|
||||
|
||||
// Handle conversation selection
|
||||
@@ -711,11 +929,28 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
onDebugEvent("clear");
|
||||
|
||||
try {
|
||||
// Load conversation history from backend
|
||||
const result = await apiClient.listConversationItems(conversationId);
|
||||
// Load conversation history from backend with pagination
|
||||
let allItems: unknown[] = [];
|
||||
let hasMore = true;
|
||||
let after: string | undefined = undefined;
|
||||
|
||||
while (hasMore) {
|
||||
const result = await apiClient.listConversationItems(conversationId, {
|
||||
order: "asc", // Load in chronological order (oldest first)
|
||||
after,
|
||||
});
|
||||
allItems = allItems.concat(result.data);
|
||||
hasMore = result.has_more;
|
||||
|
||||
// Get the last item's ID for pagination
|
||||
if (hasMore && result.data.length > 0) {
|
||||
const lastItem = result.data[result.data.length - 1] as { id?: string };
|
||||
after = lastItem.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Use OpenAI ConversationItems directly (no conversion!)
|
||||
const items = result.data as import("@/types/openai").ConversationItem[];
|
||||
const items = allItems as import("@/types/openai").ConversationItem[];
|
||||
|
||||
setChatItems(items);
|
||||
setIsStreaming(false);
|
||||
@@ -727,6 +962,27 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
message_count: items.length,
|
||||
}
|
||||
});
|
||||
|
||||
// Check for incomplete stream and restore accumulated text
|
||||
const state = loadStreamingState(conversationId);
|
||||
if (state?.accumulatedText) {
|
||||
accumulatedTextRef.current = state.accumulatedText;
|
||||
// Add assistant message with resumed text - streaming will continue automatically
|
||||
const assistantMsg: import("@/types/openai").ConversationMessage = {
|
||||
id: `assistant-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: state.accumulatedText }],
|
||||
status: "in_progress",
|
||||
};
|
||||
setChatItems([...items, assistantMsg]);
|
||||
setIsStreaming(true);
|
||||
}
|
||||
|
||||
// Scroll to bottom after loading conversation
|
||||
setTimeout(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, 100);
|
||||
} catch {
|
||||
// 404 means conversation doesn't exist or has no items yet
|
||||
// This can happen if server restarted (in-memory store cleared)
|
||||
@@ -736,7 +992,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
useDevUIStore.setState({ conversationUsage: { total_tokens: 0, message_count: 0 } });
|
||||
}
|
||||
|
||||
accumulatedText.current = "";
|
||||
accumulatedTextRef.current = "";
|
||||
},
|
||||
[availableConversations, onDebugEvent, setCurrentConversation, setChatItems, setIsStreaming]
|
||||
);
|
||||
@@ -851,13 +1107,18 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any previous streaming state for this conversation before starting new message
|
||||
if (conversationToUse?.id) {
|
||||
apiClient.clearStreamingState(conversationToUse.id);
|
||||
}
|
||||
|
||||
const apiRequest = {
|
||||
input: request.input,
|
||||
conversation_id: conversationToUse?.id,
|
||||
};
|
||||
|
||||
// Clear text accumulator for new response
|
||||
accumulatedText.current = "";
|
||||
accumulatedTextRef.current = "";
|
||||
|
||||
// Use OpenAI-compatible API streaming - direct event handling
|
||||
const streamGenerator = apiClient.streamAgentExecutionOpenAI(
|
||||
@@ -884,6 +1145,35 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
continue; // Continue processing other events
|
||||
}
|
||||
|
||||
// Handle response.failed event
|
||||
if (openAIEvent.type === "response.failed") {
|
||||
const failedEvent = openAIEvent as import("@/types/openai").ResponseFailedEvent;
|
||||
const error = failedEvent.response?.error;
|
||||
const errorMessage = error
|
||||
? typeof error === "object" && "message" in error
|
||||
? (error as any).message
|
||||
: JSON.stringify(error)
|
||||
: "Request failed";
|
||||
|
||||
const currentItems = useDevUIStore.getState().chatItems;
|
||||
setChatItems(currentItems.map((item) =>
|
||||
item.id === assistantMessage.id && item.type === "message"
|
||||
? {
|
||||
...item,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: accumulatedTextRef.current || errorMessage,
|
||||
} as import("@/types/openai").MessageTextContent,
|
||||
],
|
||||
status: "incomplete" as const,
|
||||
}
|
||||
: item
|
||||
));
|
||||
setIsStreaming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle function approval request events
|
||||
if (openAIEvent.type === "response.function_approval.requested") {
|
||||
const approvalEvent = openAIEvent as import("@/types/openai").ResponseFunctionApprovalRequestedEvent;
|
||||
@@ -943,7 +1233,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
"delta" in openAIEvent &&
|
||||
openAIEvent.delta
|
||||
) {
|
||||
accumulatedText.current += openAIEvent.delta;
|
||||
accumulatedTextRef.current += openAIEvent.delta;
|
||||
|
||||
// Update assistant message with accumulated content
|
||||
const currentItems = useDevUIStore.getState().chatItems;
|
||||
@@ -954,7 +1244,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: accumulatedText.current,
|
||||
text: accumulatedTextRef.current,
|
||||
} as import("@/types/openai").MessageTextContent,
|
||||
],
|
||||
status: "in_progress" as const,
|
||||
|
||||
+3
-1
@@ -23,7 +23,7 @@ interface ContentRendererProps {
|
||||
|
||||
// Text content renderer
|
||||
function TextContentRenderer({ content, className, isStreaming }: ContentRendererProps) {
|
||||
if (content.type !== "text") return null;
|
||||
if (content.type !== "text" && content.type !== "input_text" && content.type !== "output_text") return null;
|
||||
|
||||
const text = content.text;
|
||||
|
||||
@@ -160,6 +160,8 @@ function FileContentRenderer({ content, className }: ContentRendererProps) {
|
||||
export function OpenAIContentRenderer({ content, className, isStreaming }: ContentRendererProps) {
|
||||
switch (content.type) {
|
||||
case "text":
|
||||
case "input_text":
|
||||
case "output_text":
|
||||
return <TextContentRenderer content={content} className={className} isStreaming={isStreaming} />;
|
||||
case "input_image":
|
||||
return <ImageContentRenderer content={content} className={className} />;
|
||||
|
||||
@@ -554,6 +554,10 @@ export function WorkflowView({
|
||||
try {
|
||||
const request = { input_data: inputData };
|
||||
|
||||
// Clear any previous streaming state before starting new workflow execution
|
||||
// Note: Workflows don't use conversation IDs, so we use workflow ID as the key
|
||||
apiClient.clearStreamingState(selectedWorkflow.id);
|
||||
|
||||
// Use OpenAI-compatible API streaming - direct event handling
|
||||
const streamGenerator = apiClient.streamWorkflowExecutionOpenAI(
|
||||
selectedWorkflow.id,
|
||||
|
||||
@@ -3,6 +3,10 @@ import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { ThemeProvider } from "./components/theme-provider"
|
||||
import { initStreamingState } from "./services/api"
|
||||
|
||||
// Initialize streaming state management (clears expired states)
|
||||
initStreamingState();
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
|
||||
@@ -14,6 +14,12 @@ import type {
|
||||
} from "@/types";
|
||||
import type { AgentFrameworkRequest } from "@/types/agent-framework";
|
||||
import type { ExtendedResponseStreamEvent } from "@/types/openai";
|
||||
import {
|
||||
loadStreamingState,
|
||||
updateStreamingState,
|
||||
markStreamingCompleted,
|
||||
clearStreamingState,
|
||||
} from "./streaming-state";
|
||||
|
||||
// Backend API response type - polymorphic entity that can be agent or workflow
|
||||
// This matches the Python Pydantic EntityInfo model which has all fields optional
|
||||
@@ -57,9 +63,27 @@ const DEFAULT_API_BASE_URL =
|
||||
? import.meta.env.VITE_API_BASE_URL
|
||||
: "http://localhost:8080";
|
||||
|
||||
// Retry configuration for streaming
|
||||
const RETRY_INTERVAL_MS = 1000; // Retry every second
|
||||
const MAX_RETRY_ATTEMPTS = 600; // Max 600 retries (10 minutes total)
|
||||
|
||||
// Get backend URL from localStorage or default
|
||||
function getBackendUrl(): string {
|
||||
return localStorage.getItem("devui_backend_url") || DEFAULT_API_BASE_URL;
|
||||
const stored = localStorage.getItem("devui_backend_url");
|
||||
if (stored) return stored;
|
||||
|
||||
// If VITE_API_BASE_URL is explicitly set to empty string, use relative path
|
||||
// This allows the frontend to call the same host it's served from
|
||||
if (import.meta.env.VITE_API_BASE_URL === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return DEFAULT_API_BASE_URL;
|
||||
}
|
||||
|
||||
// Helper to sleep for a given duration
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
@@ -272,6 +296,8 @@ class ApiClient {
|
||||
await this.request(`/v1/conversations/${conversationId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
// Clear streaming state when conversation is deleted
|
||||
clearStreamingState(conversationId);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -297,10 +323,230 @@ class ApiClient {
|
||||
|
||||
// OpenAI-compatible streaming methods using /v1/responses endpoint
|
||||
|
||||
// Private helper method that handles the actual streaming with retry logic
|
||||
private async *streamOpenAIResponse(
|
||||
openAIRequest: AgentFrameworkRequest,
|
||||
conversationId?: string,
|
||||
resumeResponseId?: string
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
let lastSequenceNumber = -1;
|
||||
let retryCount = 0;
|
||||
let hasYieldedAnyEvent = false;
|
||||
let currentResponseId: string | undefined = resumeResponseId;
|
||||
let lastMessageId: string | undefined = undefined;
|
||||
|
||||
// Try to resume from stored state if conversation ID is provided
|
||||
if (conversationId) {
|
||||
const storedState = loadStreamingState(conversationId);
|
||||
if (storedState) {
|
||||
// Use stored response ID if no explicit one provided
|
||||
if (!resumeResponseId) {
|
||||
currentResponseId = storedState.responseId;
|
||||
}
|
||||
|
||||
lastSequenceNumber = storedState.lastSequenceNumber;
|
||||
lastMessageId = storedState.lastMessageId;
|
||||
|
||||
// Replay stored events only if we're not explicitly resuming
|
||||
// (explicit resume means the caller already has the events)
|
||||
if (!resumeResponseId) {
|
||||
for (const event of storedState.events) {
|
||||
hasYieldedAnyEvent = true;
|
||||
yield event;
|
||||
}
|
||||
} else {
|
||||
// Mark that we've already seen events up to this sequence number
|
||||
hasYieldedAnyEvent = storedState.events.length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (retryCount <= MAX_RETRY_ATTEMPTS) {
|
||||
try {
|
||||
// If we have a response_id from a previous attempt, use GET endpoint to resume
|
||||
// Otherwise, use POST to create a new response
|
||||
let response: Response;
|
||||
if (currentResponseId) {
|
||||
const params = new URLSearchParams();
|
||||
params.set("stream", "true");
|
||||
if (lastSequenceNumber >= 0) {
|
||||
params.set("starting_after", lastSequenceNumber.toString());
|
||||
}
|
||||
const url = `${this.baseUrl}/v1/responses/${currentResponseId}?${params.toString()}`;
|
||||
response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const url = `${this.baseUrl}/v1/responses`;
|
||||
response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify(openAIRequest),
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
// Try to extract detailed error message from response body
|
||||
let errorMessage = `Request failed with status ${response.status}`;
|
||||
try {
|
||||
const errorBody = await response.json();
|
||||
if (errorBody.error && errorBody.error.message) {
|
||||
errorMessage = errorBody.error.message;
|
||||
} else if (errorBody.detail) {
|
||||
errorMessage = errorBody.detail;
|
||||
}
|
||||
} catch {
|
||||
// Fallback to generic message if parsing fails
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error("Response body is not readable");
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
// Stream completed successfully
|
||||
if (conversationId) {
|
||||
markStreamingCompleted(conversationId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// Parse SSE events
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const dataStr = line.slice(6);
|
||||
|
||||
// Handle [DONE] signal
|
||||
if (dataStr === "[DONE]") {
|
||||
if (conversationId) {
|
||||
markStreamingCompleted(conversationId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const openAIEvent: ExtendedResponseStreamEvent =
|
||||
JSON.parse(dataStr);
|
||||
|
||||
// Capture response_id if present in the event for use in retries
|
||||
if ("response" in openAIEvent && openAIEvent.response && typeof openAIEvent.response === "object" && "id" in openAIEvent.response) {
|
||||
const newResponseId = openAIEvent.response.id as string;
|
||||
if (!currentResponseId || currentResponseId !== newResponseId) {
|
||||
currentResponseId = newResponseId;
|
||||
}
|
||||
} else if ("id" in openAIEvent && typeof openAIEvent.id === "string" && openAIEvent.id.startsWith("resp_")) {
|
||||
const newResponseId = openAIEvent.id;
|
||||
if (!currentResponseId || currentResponseId !== newResponseId) {
|
||||
currentResponseId = newResponseId;
|
||||
}
|
||||
}
|
||||
|
||||
// Track last message ID if present (for user/assistant messages)
|
||||
if ("item_id" in openAIEvent && openAIEvent.item_id) {
|
||||
lastMessageId = openAIEvent.item_id;
|
||||
}
|
||||
|
||||
// Check for sequence number restart (server restarted response)
|
||||
const eventSeq = "sequence_number" in openAIEvent ? openAIEvent.sequence_number : undefined;
|
||||
if (eventSeq !== undefined) {
|
||||
// If we've received events before and sequence restarted from 0/1
|
||||
if (hasYieldedAnyEvent && eventSeq <= 1 && lastSequenceNumber > 1) {
|
||||
// Server restarted the response - clear old state and start fresh
|
||||
if (conversationId) {
|
||||
clearStreamingState(conversationId);
|
||||
}
|
||||
yield {
|
||||
type: "error",
|
||||
message: "Connection lost - previous response failed. Starting new response.",
|
||||
} as ExtendedResponseStreamEvent;
|
||||
lastSequenceNumber = eventSeq;
|
||||
hasYieldedAnyEvent = true;
|
||||
|
||||
// Save new event to storage
|
||||
if (conversationId && currentResponseId) {
|
||||
updateStreamingState(conversationId, openAIEvent, currentResponseId, lastMessageId);
|
||||
}
|
||||
|
||||
yield openAIEvent;
|
||||
}
|
||||
// Skip events we've already seen (resume from last position)
|
||||
else if (eventSeq <= lastSequenceNumber) {
|
||||
continue; // Skip duplicate event
|
||||
} else {
|
||||
lastSequenceNumber = eventSeq;
|
||||
hasYieldedAnyEvent = true;
|
||||
|
||||
// Save event to storage before yielding
|
||||
if (conversationId && currentResponseId) {
|
||||
updateStreamingState(conversationId, openAIEvent, currentResponseId, lastMessageId);
|
||||
}
|
||||
|
||||
yield openAIEvent;
|
||||
}
|
||||
} else {
|
||||
// No sequence number - just yield the event
|
||||
hasYieldedAnyEvent = true;
|
||||
|
||||
// Still save to storage if we have conversation context
|
||||
if (conversationId && currentResponseId) {
|
||||
updateStreamingState(conversationId, openAIEvent, currentResponseId, lastMessageId);
|
||||
}
|
||||
|
||||
yield openAIEvent;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse OpenAI SSE event:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
} catch (error) {
|
||||
// Network error occurred - prepare to retry
|
||||
retryCount++;
|
||||
|
||||
if (retryCount > MAX_RETRY_ATTEMPTS) {
|
||||
// Max retries exceeded - give up
|
||||
throw new Error(
|
||||
`Connection failed after ${MAX_RETRY_ATTEMPTS} retry attempts: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
await sleep(RETRY_INTERVAL_MS);
|
||||
// Loop will retry with GET if we have response_id, otherwise POST
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream agent execution using OpenAI format with simplified routing
|
||||
async *streamAgentExecutionOpenAI(
|
||||
agentId: string,
|
||||
request: RunAgentRequest
|
||||
request: RunAgentRequest,
|
||||
resumeResponseId?: string
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
const openAIRequest: AgentFrameworkRequest = {
|
||||
model: agentId, // Model IS the entity_id (simplified routing!)
|
||||
@@ -309,87 +555,20 @@ class ApiClient {
|
||||
conversation: request.conversation_id, // OpenAI standard conversation param
|
||||
};
|
||||
|
||||
return yield* this.streamAgentExecutionOpenAIDirect(agentId, openAIRequest);
|
||||
return yield* this.streamAgentExecutionOpenAIDirect(agentId, openAIRequest, request.conversation_id, resumeResponseId);
|
||||
}
|
||||
|
||||
// Stream agent execution using direct OpenAI format
|
||||
async *streamAgentExecutionOpenAIDirect(
|
||||
_agentId: string,
|
||||
openAIRequest: AgentFrameworkRequest
|
||||
openAIRequest: AgentFrameworkRequest,
|
||||
conversationId?: string,
|
||||
resumeResponseId?: string
|
||||
): AsyncGenerator<ExtendedResponseStreamEvent, void, unknown> {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/v1/responses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify(openAIRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Try to extract detailed error message from response body
|
||||
let errorMessage = `Request failed with status ${response.status}`;
|
||||
try {
|
||||
const errorBody = await response.json();
|
||||
if (errorBody.error && errorBody.error.message) {
|
||||
errorMessage = errorBody.error.message;
|
||||
} else if (errorBody.detail) {
|
||||
errorMessage = errorBody.detail;
|
||||
}
|
||||
} catch {
|
||||
// Fallback to generic message if parsing fails
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error("Response body is not readable");
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// Parse SSE events
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const dataStr = line.slice(6);
|
||||
|
||||
// Handle [DONE] signal
|
||||
if (dataStr === "[DONE]") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const openAIEvent: ExtendedResponseStreamEvent =
|
||||
JSON.parse(dataStr);
|
||||
yield openAIEvent; // Direct pass-through - no conversion!
|
||||
} catch (e) {
|
||||
console.error("Failed to parse OpenAI SSE event:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
yield* this.streamOpenAIResponse(openAIRequest, conversationId, resumeResponseId);
|
||||
}
|
||||
|
||||
// Stream workflow execution using OpenAI format - direct event pass-through
|
||||
// Stream workflow execution using OpenAI format
|
||||
async *streamWorkflowExecutionOpenAI(
|
||||
workflowId: string,
|
||||
request: RunWorkflowRequest
|
||||
@@ -402,75 +581,7 @@ class ApiClient {
|
||||
conversation: request.conversation_id, // Include conversation if present
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/v1/responses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify(openAIRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Try to extract detailed error message from response body
|
||||
let errorMessage = `Request failed with status ${response.status}`;
|
||||
try {
|
||||
const errorBody = await response.json();
|
||||
if (errorBody.error && errorBody.error.message) {
|
||||
errorMessage = errorBody.error.message;
|
||||
} else if (errorBody.detail) {
|
||||
errorMessage = errorBody.detail;
|
||||
}
|
||||
} catch {
|
||||
// Fallback to generic message if parsing fails
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error("Response body is not readable");
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// Parse SSE events
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const dataStr = line.slice(6);
|
||||
|
||||
// Handle [DONE] signal
|
||||
if (dataStr === "[DONE]") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const openAIEvent: ExtendedResponseStreamEvent =
|
||||
JSON.parse(dataStr);
|
||||
yield openAIEvent; // Direct pass-through - no conversion!
|
||||
} catch (e) {
|
||||
console.error("Failed to parse OpenAI SSE event:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
yield* this.streamOpenAIResponse(openAIRequest, request.conversation_id);
|
||||
}
|
||||
|
||||
// REMOVED: Legacy streaming methods - use streamAgentExecutionOpenAI and streamWorkflowExecutionOpenAI instead
|
||||
@@ -503,8 +614,16 @@ class ApiClient {
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
// Clear streaming state for a conversation (e.g., when starting a new message)
|
||||
clearStreamingState(conversationId: string): void {
|
||||
clearStreamingState(conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const apiClient = new ApiClient();
|
||||
export { ApiClient };
|
||||
|
||||
// Export streaming state init function
|
||||
export { initStreamingState } from "./streaming-state";
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Streaming State Persistence
|
||||
*
|
||||
* Manages browser storage of streaming response state to enable:
|
||||
* - Resume interrupted streams after page refresh
|
||||
* - Replay cached events before fetching new ones
|
||||
* - Graceful recovery from network disconnections
|
||||
*/
|
||||
|
||||
import type { ExtendedResponseStreamEvent } from "@/types/openai";
|
||||
|
||||
export interface StreamingState {
|
||||
conversationId: string;
|
||||
responseId: string;
|
||||
lastMessageId?: string;
|
||||
lastSequenceNumber: number;
|
||||
events: ExtendedResponseStreamEvent[];
|
||||
timestamp: number; // When this state was last updated
|
||||
completed: boolean; // Whether the stream completed successfully
|
||||
accumulatedText?: string; // Accumulated text content for quick restoration
|
||||
}
|
||||
|
||||
const STORAGE_KEY_PREFIX = "devui_streaming_state_";
|
||||
const STATE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
/**
|
||||
* Storage key for a specific conversation
|
||||
*/
|
||||
function getStorageKey(conversationId: string): string {
|
||||
return `${STORAGE_KEY_PREFIX}${conversationId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract accumulated text from events (for quick restoration)
|
||||
*/
|
||||
function extractAccumulatedText(events: ExtendedResponseStreamEvent[]): string {
|
||||
let text = "";
|
||||
for (const event of events) {
|
||||
if (event.type === "response.output_text.delta" && "delta" in event) {
|
||||
text += event.delta;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save streaming state to browser storage
|
||||
*/
|
||||
export function saveStreamingState(state: StreamingState): void {
|
||||
try {
|
||||
const key = getStorageKey(state.conversationId);
|
||||
const data = JSON.stringify(state);
|
||||
localStorage.setItem(key, data);
|
||||
} catch (error) {
|
||||
console.error("Failed to save streaming state:", error);
|
||||
// If storage is full, try to clear old states
|
||||
try {
|
||||
clearExpiredStreamingStates();
|
||||
// Try again
|
||||
const key = getStorageKey(state.conversationId);
|
||||
const data = JSON.stringify(state);
|
||||
localStorage.setItem(key, data);
|
||||
} catch {
|
||||
console.error("Failed to save streaming state even after cleanup");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load streaming state from browser storage
|
||||
*/
|
||||
export function loadStreamingState(conversationId: string): StreamingState | null {
|
||||
try {
|
||||
const key = getStorageKey(conversationId);
|
||||
const data = localStorage.getItem(key);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const state: StreamingState = JSON.parse(data);
|
||||
|
||||
// Check if state has expired
|
||||
const age = Date.now() - state.timestamp;
|
||||
if (age > STATE_EXPIRY_MS) {
|
||||
clearStreamingState(conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// If stream was completed, no need to resume
|
||||
if (state.completed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state;
|
||||
} catch (error) {
|
||||
console.error("Failed to load streaming state:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update streaming state with a new event
|
||||
*/
|
||||
export function updateStreamingState(
|
||||
conversationId: string,
|
||||
event: ExtendedResponseStreamEvent,
|
||||
responseId: string,
|
||||
lastMessageId?: string
|
||||
): void {
|
||||
try {
|
||||
const existing = loadStreamingState(conversationId);
|
||||
const sequenceNumber = "sequence_number" in event ? event.sequence_number : undefined;
|
||||
|
||||
const newEvents = existing ? [...existing.events, event] : [event];
|
||||
|
||||
const state: StreamingState = {
|
||||
conversationId,
|
||||
responseId,
|
||||
lastMessageId,
|
||||
lastSequenceNumber: sequenceNumber ?? (existing?.lastSequenceNumber ?? -1),
|
||||
events: newEvents,
|
||||
timestamp: Date.now(),
|
||||
completed: event.type === "response.completed" || event.type === "response.failed",
|
||||
accumulatedText: extractAccumulatedText(newEvents),
|
||||
};
|
||||
|
||||
saveStreamingState(state);
|
||||
} catch (error) {
|
||||
console.error("Failed to update streaming state:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark streaming state as completed
|
||||
*/
|
||||
export function markStreamingCompleted(conversationId: string): void {
|
||||
try {
|
||||
const existing = loadStreamingState(conversationId);
|
||||
if (existing) {
|
||||
existing.completed = true;
|
||||
existing.timestamp = Date.now();
|
||||
saveStreamingState(existing);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to mark streaming as completed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear streaming state for a conversation
|
||||
*/
|
||||
export function clearStreamingState(conversationId: string): void {
|
||||
try {
|
||||
const key = getStorageKey(conversationId);
|
||||
localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.error("Failed to clear streaming state:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all expired streaming states
|
||||
*/
|
||||
export function clearExpiredStreamingStates(): void {
|
||||
try {
|
||||
const keys = Object.keys(localStorage);
|
||||
const now = Date.now();
|
||||
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(STORAGE_KEY_PREFIX)) {
|
||||
try {
|
||||
const data = localStorage.getItem(key);
|
||||
if (data) {
|
||||
const state: StreamingState = JSON.parse(data);
|
||||
const age = now - state.timestamp;
|
||||
|
||||
if (age > STATE_EXPIRY_MS || state.completed) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Invalid state, remove it
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to clear expired streaming states:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize streaming state management (call on app startup)
|
||||
*/
|
||||
export function initStreamingState(): void {
|
||||
// Clear expired states on startup
|
||||
clearExpiredStreamingStates();
|
||||
}
|
||||
@@ -347,6 +347,18 @@ export interface MessageTextContent {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface MessageInputTextContent {
|
||||
type: "input_text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface MessageOutputTextContent {
|
||||
type: "output_text";
|
||||
text: string;
|
||||
annotations?: any[];
|
||||
logprobs?: any[];
|
||||
}
|
||||
|
||||
export interface MessageInputImage {
|
||||
type: "input_image";
|
||||
image_url: string;
|
||||
@@ -376,6 +388,8 @@ export interface MessageFunctionApprovalResponseContent {
|
||||
|
||||
export type MessageContent =
|
||||
| MessageTextContent
|
||||
| MessageInputTextContent
|
||||
| MessageOutputTextContent
|
||||
| MessageInputImage
|
||||
| MessageInputFile
|
||||
| MessageFunctionApprovalResponseContent;
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "path";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: "",
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -12,12 +13,20 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
// Enable deterministic builds, as per https://github.com/vitejs/vite/issues/13672#issuecomment-1784110536
|
||||
strictRequires: true,
|
||||
},
|
||||
outDir: "../agent_framework_devui/ui",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: undefined,
|
||||
inlineDynamicImports: true,
|
||||
// Use static filenames instead of content hashes
|
||||
entryFileNames: "assets/index.js",
|
||||
chunkFileNames: "assets/[name].js",
|
||||
assetFileNames: "assets/[name].[ext]",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz"
|
||||
integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==
|
||||
|
||||
"@babel/core@^7.28.3":
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.28.3":
|
||||
version "7.28.3"
|
||||
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz"
|
||||
integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==
|
||||
@@ -168,156 +168,9 @@
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
|
||||
"@emnapi/core@^1.4.3", "@emnapi/core@^1.4.5":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0"
|
||||
integrity sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads" "1.1.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.4.5":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73"
|
||||
integrity sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.0.4":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
|
||||
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@esbuild/aix-ppc64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9"
|
||||
integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==
|
||||
|
||||
"@esbuild/android-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c"
|
||||
integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==
|
||||
|
||||
"@esbuild/android-arm@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419"
|
||||
integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==
|
||||
|
||||
"@esbuild/android-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683"
|
||||
integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==
|
||||
|
||||
"@esbuild/darwin-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz"
|
||||
integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==
|
||||
|
||||
"@esbuild/darwin-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be"
|
||||
integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca"
|
||||
integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==
|
||||
|
||||
"@esbuild/freebsd-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab"
|
||||
integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==
|
||||
|
||||
"@esbuild/linux-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b"
|
||||
integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==
|
||||
|
||||
"@esbuild/linux-arm@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37"
|
||||
integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==
|
||||
|
||||
"@esbuild/linux-ia32@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4"
|
||||
integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==
|
||||
|
||||
"@esbuild/linux-loong64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0"
|
||||
integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==
|
||||
|
||||
"@esbuild/linux-mips64el@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5"
|
||||
integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==
|
||||
|
||||
"@esbuild/linux-ppc64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db"
|
||||
integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==
|
||||
|
||||
"@esbuild/linux-riscv64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547"
|
||||
integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==
|
||||
|
||||
"@esbuild/linux-s390x@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830"
|
||||
integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==
|
||||
|
||||
"@esbuild/linux-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f"
|
||||
integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548"
|
||||
integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==
|
||||
|
||||
"@esbuild/netbsd-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52"
|
||||
integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935"
|
||||
integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf"
|
||||
integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==
|
||||
|
||||
"@esbuild/openharmony-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314"
|
||||
integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==
|
||||
|
||||
"@esbuild/sunos-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e"
|
||||
integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==
|
||||
|
||||
"@esbuild/win32-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b"
|
||||
integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3"
|
||||
integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==
|
||||
|
||||
"@esbuild/win32-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f"
|
||||
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz"
|
||||
integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
|
||||
@@ -368,7 +221,7 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.33.0", "@eslint/js@^9.33.0":
|
||||
"@eslint/js@^9.33.0", "@eslint/js@9.33.0":
|
||||
version "9.33.0"
|
||||
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz"
|
||||
integrity sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==
|
||||
@@ -482,15 +335,6 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@napi-rs/wasm-runtime@^0.2.12":
|
||||
version "0.2.12"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2"
|
||||
integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.4.3"
|
||||
"@emnapi/runtime" "^1.4.3"
|
||||
"@tybys/wasm-util" "^0.10.0"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||
@@ -499,7 +343,7 @@
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
@@ -740,7 +584,7 @@
|
||||
aria-hidden "^1.2.4"
|
||||
react-remove-scroll "^2.6.3"
|
||||
|
||||
"@radix-ui/react-slot@1.2.3", "@radix-ui/react-slot@^1.2.3":
|
||||
"@radix-ui/react-slot@^1.2.3", "@radix-ui/react-slot@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz"
|
||||
integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==
|
||||
@@ -829,104 +673,9 @@
|
||||
resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz"
|
||||
integrity sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.47.1.tgz#6e236cd2fd29bb01a300ad4ff6ed0f1a17550e69"
|
||||
integrity sha512-lTahKRJip0knffA/GTNFJMrToD+CM+JJ+Qt5kjzBK/sFQ0EWqfKW3AYQSlZXN98tX0lx66083U9JYIMioMMK7g==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.47.1.tgz#808f2c9c7e68161add613ebcb0eac5a058a0df3c"
|
||||
integrity sha512-uqxkb3RJLzlBbh/bbNQ4r7YpSZnjgMgyoEOY7Fy6GCbelkDSAzeiogxMG9TfLsBbqmGsdDObo3mzGqa8hps4MA==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.47.1.tgz"
|
||||
integrity sha512-tV6reObmxBDS4DDyLzTDIpymthNlxrLBGAoQx6m2a7eifSNEZdkXQl1PE4ZjCkEDPVgNXSzND/k9AQ3mC4IOEQ==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.47.1.tgz#9aac64e886435493f2e3a0aa5e4aad098a90814c"
|
||||
integrity sha512-XuJRPTnMk1lwsSnS3vYyVMu4x/+WIw1MMSiqj5C4j3QOWsMzbJEK90zG+SWV1h0B1ABGCQ0UZUjti+TQK35uHQ==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.47.1.tgz#9fc804264f7b7a7cdad3747950299f990163be1f"
|
||||
integrity sha512-79BAm8Ag/tmJ5asCqgOXsb3WY28Rdd5Lxj8ONiQzWzy9LvWORd5qVuOnjlqiWWZJw+dWewEktZb5yiM1DLLaHw==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.47.1.tgz#933feaff864feb03bbbcd0c18ea351ade957cf79"
|
||||
integrity sha512-OQ2/ZDGzdOOlyfqBiip0ZX/jVFekzYrGtUsqAfLDbWy0jh1PUU18+jYp8UMpqhly5ltEqotc2miLngf9FPSWIA==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.47.1.tgz#02915e6b2c55fe5961c27404aba2d9c8ef48ac6c"
|
||||
integrity sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.47.1.tgz#1afef33191b26e76ae7f0d0dc767efc6be1285ce"
|
||||
integrity sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.47.1.tgz#6e7f38fb99d14143de3ce33204e6cd61e1c2c780"
|
||||
integrity sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.47.1.tgz#25ab09f14bbcba85a604bcee2962d2486db90794"
|
||||
integrity sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA==
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.47.1.tgz#d3e3a3fd61e21b2753094391dee9b515a2bc9ecd"
|
||||
integrity sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.47.1.tgz#6b44445e2bd5866692010de241bf18d2ae8b0cb8"
|
||||
integrity sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.47.1.tgz#3ff412d20d3b157e6aadabf84788e8c5cb221ba7"
|
||||
integrity sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.47.1.tgz#104f451497d53d82a49c6d08c13c59f5f30eed57"
|
||||
integrity sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.47.1.tgz#d04de7b21d181f30750760cb3553946306506172"
|
||||
integrity sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.47.1.tgz#a6ba88ff7480940a435b1e67ddbb3f207a7ae02f"
|
||||
integrity sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.47.1.tgz#c912c8ffa0c242ed3175cd91cdeaef98109afa54"
|
||||
integrity sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.47.1.tgz#ca5eaae89443554b461bb359112a056528cfdac0"
|
||||
integrity sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.47.1.tgz#34e76172515fb4b374eb990d59f54faff938246e"
|
||||
integrity sha512-O+KcfeCORZADEY8oQJk4HK8wtEOCRE4MdOkb8qGZQNun3jzmj2nmhV/B/ZaaZOkPmJyvm/gW9n0gsB4eRa1eiQ==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz#e5e0a0bae2c9d4858cc9b8dc508b2e10d7f0df8b"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz"
|
||||
integrity sha512-CpKnYa8eHthJa3c+C38v/E+/KZyF1Jdh2Cz3DyKZqEWYgrM1IHFArXNWvBLPQCKUEsAqqKX27tTqVEFbDNUcOA==
|
||||
|
||||
"@tailwindcss/node@4.1.12":
|
||||
@@ -942,71 +691,9 @@
|
||||
source-map-js "^1.2.1"
|
||||
tailwindcss "4.1.12"
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz#27920fe61fa2743afe8a8ca296fa640b609d17d5"
|
||||
integrity sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz"
|
||||
integrity sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz#8ddb7e5ddfd9b049ec84a2bda99f2b04a86859f5"
|
||||
integrity sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz#da1c0b16b7a5f95a1e400f299a3ec94fb6fd40ac"
|
||||
integrity sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz#34e558aa6e869c6fe9867cb78ed7ba651b9fcaa4"
|
||||
integrity sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz#0a00a8146ab6215f81b2d385056c991441bf390e"
|
||||
integrity sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz#b138f494068884ae0d8c343dc1904b22f5e98dc6"
|
||||
integrity sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz#5b9d5f23b15cdb714639f5b9741c0df5d610f794"
|
||||
integrity sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz#f68ec530d3ca6875ea9015bcd5dd0762ee5e2f5d"
|
||||
integrity sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz#9fd15a1ebde6076c42c445c5e305c31673ead965"
|
||||
integrity sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.4.5"
|
||||
"@emnapi/runtime" "^1.4.5"
|
||||
"@emnapi/wasi-threads" "^1.0.4"
|
||||
"@napi-rs/wasm-runtime" "^0.2.12"
|
||||
"@tybys/wasm-util" "^0.10.0"
|
||||
tslib "^2.8.0"
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz#938bcc6a82e1120ea4fe2ce94be0a8cdf3ae92c7"
|
||||
integrity sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz#b1ee2ed0ef2c4095ddec3684a1987e2b3613af36"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz"
|
||||
integrity sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==
|
||||
|
||||
"@tailwindcss/oxide@4.1.12":
|
||||
@@ -1039,13 +726,6 @@
|
||||
"@tailwindcss/oxide" "4.1.12"
|
||||
tailwindcss "4.1.12"
|
||||
|
||||
"@tybys/wasm-util@^0.10.0":
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369"
|
||||
integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/babel__core@^7.20.5":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
||||
@@ -1118,7 +798,7 @@
|
||||
"@types/d3-interpolate" "*"
|
||||
"@types/d3-selection" "*"
|
||||
|
||||
"@types/estree@1.0.8", "@types/estree@^1.0.6":
|
||||
"@types/estree@^1.0.6", "@types/estree@1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
|
||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||
@@ -1128,19 +808,19 @@
|
||||
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
"@types/node@^24.3.0":
|
||||
"@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.3.0":
|
||||
version "24.3.0"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz"
|
||||
integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
|
||||
dependencies:
|
||||
undici-types "~7.10.0"
|
||||
|
||||
"@types/react-dom@^19.1.7":
|
||||
"@types/react-dom@*", "@types/react-dom@^19.1.7":
|
||||
version "19.1.7"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz"
|
||||
integrity sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==
|
||||
|
||||
"@types/react@^19.1.10":
|
||||
"@types/react@*", "@types/react@^19.0.0", "@types/react@^19.1.10", "@types/react@>=16.8", "@types/react@>=18.0.0":
|
||||
version "19.1.10"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz"
|
||||
integrity sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==
|
||||
@@ -1162,7 +842,7 @@
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.40.0":
|
||||
"@typescript-eslint/parser@^8.40.0", "@typescript-eslint/parser@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz"
|
||||
integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==
|
||||
@@ -1190,7 +870,7 @@
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
"@typescript-eslint/visitor-keys" "8.40.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0":
|
||||
"@typescript-eslint/tsconfig-utils@^8.40.0", "@typescript-eslint/tsconfig-utils@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz"
|
||||
integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==
|
||||
@@ -1206,7 +886,7 @@
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.40.0", "@typescript-eslint/types@^8.40.0":
|
||||
"@typescript-eslint/types@^8.40.0", "@typescript-eslint/types@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz"
|
||||
integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==
|
||||
@@ -1259,7 +939,7 @@
|
||||
|
||||
"@xyflow/react@^12.8.4":
|
||||
version "12.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@xyflow/react/-/react-12.8.4.tgz#db0eabd9e356c25f5ebf427413a8c5dd46113394"
|
||||
resolved "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz"
|
||||
integrity sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==
|
||||
dependencies:
|
||||
"@xyflow/system" "0.0.68"
|
||||
@@ -1286,7 +966,7 @@ acorn-jsx@^5.3.2:
|
||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn@^8.15.0:
|
||||
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
@@ -1347,7 +1027,7 @@ braces@^3.0.3:
|
||||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.24.0:
|
||||
browserslist@^4.24.0, "browserslist@>= 4.21.0":
|
||||
version "4.25.3"
|
||||
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz"
|
||||
integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==
|
||||
@@ -1443,7 +1123,7 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz"
|
||||
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
|
||||
|
||||
"d3-drag@2 - 3", d3-drag@^3.0.0:
|
||||
d3-drag@^3.0.0, "d3-drag@2 - 3":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz"
|
||||
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
|
||||
@@ -1456,14 +1136,14 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz"
|
||||
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
|
||||
|
||||
"d3-interpolate@1 - 3", d3-interpolate@^3.0.1:
|
||||
d3-interpolate@^3.0.1, "d3-interpolate@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
|
||||
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
|
||||
"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
|
||||
d3-selection@^3.0.0, "d3-selection@2 - 3", d3-selection@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz"
|
||||
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
|
||||
@@ -1600,7 +1280,7 @@ eslint-visitor-keys@^4.2.1:
|
||||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint@^9.33.0:
|
||||
"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.33.0, eslint@>=8.40:
|
||||
version "9.33.0"
|
||||
resolved "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz"
|
||||
integrity sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==
|
||||
@@ -1846,7 +1526,7 @@ isexe@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
jiti@^2.5.1:
|
||||
jiti@*, jiti@^2.5.1, jiti@>=1.21.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz"
|
||||
integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==
|
||||
@@ -1903,57 +1583,12 @@ levn@^0.4.1:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz"
|
||||
integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==
|
||||
|
||||
lightningcss-darwin-x64@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22"
|
||||
integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==
|
||||
|
||||
lightningcss-freebsd-x64@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4"
|
||||
integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908"
|
||||
integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009"
|
||||
integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe"
|
||||
integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157"
|
||||
integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26"
|
||||
integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039"
|
||||
integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz#fd7dd008ea98494b85d24b4bea016793f2e0e352"
|
||||
resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz"
|
||||
integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==
|
||||
|
||||
lightningcss@1.30.1:
|
||||
lightningcss@^1.21.0, lightningcss@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz"
|
||||
integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==
|
||||
@@ -2063,7 +1698,7 @@ natural-compare@^1.4.0:
|
||||
|
||||
next-themes@^0.4.6:
|
||||
version "0.4.6"
|
||||
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
|
||||
resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz"
|
||||
integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
|
||||
|
||||
node-releases@^2.0.19:
|
||||
@@ -2124,7 +1759,7 @@ picomatch@^2.3.1:
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
picomatch@^4.0.3:
|
||||
"picomatch@^3 || ^4", picomatch@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||
@@ -2153,7 +1788,7 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
react-dom@^19.1.1:
|
||||
"react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", react-dom@^19.1.1, react-dom@>=16.8.0, react-dom@>=17:
|
||||
version "19.1.1"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz"
|
||||
integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==
|
||||
@@ -2192,7 +1827,7 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
|
||||
get-nonce "^1.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react@^19.1.1:
|
||||
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^19.1.1, react@>=16.8, react@>=16.8.0, react@>=17, react@>=18.0.0:
|
||||
version "19.1.1"
|
||||
resolved "https://registry.npmjs.org/react/-/react-19.1.1.tgz"
|
||||
integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==
|
||||
@@ -2292,7 +1927,7 @@ tailwind-merge@^3.3.1:
|
||||
resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz"
|
||||
integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==
|
||||
|
||||
tailwindcss@4.1.12, tailwindcss@^4.1.12:
|
||||
tailwindcss@^4.1.12, tailwindcss@4.1.12:
|
||||
version "4.1.12"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz"
|
||||
integrity sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==
|
||||
@@ -2316,7 +1951,7 @@ tar@^7.4.3:
|
||||
|
||||
tinyglobby@^0.2.15:
|
||||
version "0.2.15"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
|
||||
resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz"
|
||||
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
|
||||
dependencies:
|
||||
fdir "^6.5.0"
|
||||
@@ -2334,7 +1969,7 @@ ts-api-utils@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
|
||||
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0:
|
||||
tslib@^2.0.0, tslib@^2.1.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
@@ -2361,7 +1996,7 @@ typescript-eslint@^8.39.1:
|
||||
"@typescript-eslint/typescript-estree" "8.40.0"
|
||||
"@typescript-eslint/utils" "8.40.0"
|
||||
|
||||
typescript@~5.8.3:
|
||||
typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@~5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
|
||||
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
||||
@@ -2401,15 +2036,15 @@ use-sidecar@^1.1.3:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@^1.2.2:
|
||||
use-sync-external-store@^1.2.2, use-sync-external-store@>=1.2.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz"
|
||||
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
|
||||
|
||||
vite@^7.1.11:
|
||||
version "7.1.11"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.11.tgz#4d006746112fee056df64985191e846ebfb6007e"
|
||||
integrity sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==
|
||||
"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^7.1.11:
|
||||
version "7.1.12"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz"
|
||||
integrity sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==
|
||||
dependencies:
|
||||
esbuild "^0.25.0"
|
||||
fdir "^6.5.0"
|
||||
@@ -2456,5 +2091,5 @@ zustand@^4.4.0:
|
||||
|
||||
zustand@^5.0.8:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.8.tgz#b998a0c088c7027a20f2709141a91cb07ac57f8a"
|
||||
resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz"
|
||||
integrity sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==
|
||||
|
||||
Reference in New Issue
Block a user