chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AGUI.Client" />
</ItemGroup>
</Project>
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="AGUIWebChatClient.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="@renderMode" />
</head>
<body>
<Routes @rendermode="@renderMode" />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
@code {
private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false);
}
@@ -0,0 +1 @@
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
@@ -0,0 +1,89 @@
/* Used under CC0 license */
.lds-ellipsis {
color: #666;
animation: fade-in 1s;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.lds-ellipsis,
.lds-ellipsis div {
box-sizing: border-box;
}
.lds-ellipsis {
margin: auto;
display: block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ellipsis div {
position: absolute;
top: 33.33333px;
width: 10px;
height: 10px;
border-radius: 50%;
background: currentColor;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 8px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 8px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 32px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 56px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
@@ -0,0 +1,9 @@
@inherits LayoutComponentBase
@Body
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@@ -0,0 +1,20 @@
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@@ -0,0 +1,94 @@
@page "/"
@using System.ComponentModel
@inject IChatClient ChatClient
@inject NavigationManager Nav
@implements IDisposable
<PageTitle>Chat</PageTitle>
<ChatHeader OnNewChat="@ResetConversationAsync" />
<ChatMessageList Messages="@messages" InProgressMessage="@currentResponseMessage">
<NoMessagesContent>
<div>Ask the assistant a question to start a conversation.</div>
</NoMessagesContent>
</ChatMessageList>
<div class="chat-container">
<ChatSuggestions OnSelected="@AddUserMessageAsync" @ref="@chatSuggestions" />
<ChatInput OnSend="@AddUserMessageAsync" @ref="@chatInput" />
</div>
@code {
private const string SystemPrompt = @"
You are a helpful assistant.
";
private int statefulMessageCount;
private readonly ChatOptions chatOptions = new();
private readonly List<ChatMessage> messages = new();
private CancellationTokenSource? currentResponseCancellation;
private ChatMessage? currentResponseMessage;
private ChatInput? chatInput;
private ChatSuggestions? chatSuggestions;
protected override void OnInitialized()
{
statefulMessageCount = 0;
messages.Add(new(ChatRole.System, SystemPrompt));
}
private async Task AddUserMessageAsync(ChatMessage userMessage)
{
CancelAnyCurrentResponse();
// Add the user message to the conversation
messages.Add(userMessage);
chatSuggestions?.Clear();
await chatInput!.FocusAsync();
// Stream and display a new response from the IChatClient
var responseText = new TextContent("");
currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);
StateHasChanged();
currentResponseCancellation = new();
await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token))
{
messages.AddMessages(update, filter: c => c is not TextContent);
responseText.Text += update.Text;
chatOptions.ConversationId = update.ConversationId;
ChatMessageItem.NotifyChanged(currentResponseMessage);
}
// Store the final response in the conversation, and begin getting suggestions
messages.Add(currentResponseMessage!);
statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0;
currentResponseMessage = null;
chatSuggestions?.Update(messages);
}
private void CancelAnyCurrentResponse()
{
// If a response was cancelled while streaming, include it in the conversation so it's not lost
if (currentResponseMessage is not null)
{
messages.Add(currentResponseMessage);
}
currentResponseCancellation?.Cancel();
currentResponseMessage = null;
}
private async Task ResetConversationAsync()
{
CancelAnyCurrentResponse();
messages.Clear();
messages.Add(new(ChatRole.System, SystemPrompt));
chatOptions.ConversationId = null;
statefulMessageCount = 0;
chatSuggestions?.Clear();
await chatInput!.FocusAsync();
}
public void Dispose()
=> currentResponseCancellation?.Cancel();
}
@@ -0,0 +1,11 @@
.chat-container {
position: sticky;
bottom: 0;
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-top: 0.75rem;
padding-bottom: 1.5rem;
border-top-width: 1px;
background-color: #F3F4F6;
border-color: #E5E7EB;
}
@@ -0,0 +1,38 @@
@using System.Web
@if (!string.IsNullOrWhiteSpace(viewerUrl))
{
<a href="@viewerUrl" target="_blank" class="citation">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
<div class="citation-content">
<div class="citation-file">@File</div>
<div>@Quote</div>
</div>
</a>
}
@code {
[Parameter]
public required string File { get; set; }
[Parameter]
public int? PageNumber { get; set; }
[Parameter]
public required string Quote { get; set; }
private string? viewerUrl;
protected override void OnParametersSet()
{
viewerUrl = null;
// If you ingest other types of content besides PDF files, construct a URL to an appropriate viewer here
if (File.EndsWith(".pdf"))
{
var search = Quote?.Trim('.', ',', ' ', '\n', '\r', '\t', '"', '\'');
viewerUrl = $"lib/pdf_viewer/viewer.html?file=/Data/{HttpUtility.UrlEncode(File)}#page={PageNumber}&search={HttpUtility.UrlEncode(search)}&phrase=true";
}
}
}
@@ -0,0 +1,37 @@
.citation {
display: inline-flex;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
margin-top: 1rem;
margin-right: 1rem;
border-bottom: 2px solid #a770de;
gap: 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
background-color: #ffffff;
}
.citation[href]:hover {
outline: 1px solid #865cb1;
}
.citation svg {
width: 1.5rem;
height: 1.5rem;
}
.citation:active {
background-color: rgba(0,0,0,0.05);
}
.citation-content {
display: flex;
flex-direction: column;
}
.citation-file {
font-weight: 600;
}
@@ -0,0 +1,17 @@
<div class="chat-header-container main-background-gradient">
<div class="chat-header-controls page-width">
<button class="btn-default" @onclick="@OnNewChat">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="new-chat-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
New chat
</button>
</div>
<h1 class="page-width">AGUI WebChat</h1>
</div>
@code {
[Parameter]
public EventCallback OnNewChat { get; set; }
}
@@ -0,0 +1,25 @@
.chat-header-container {
top: 0;
padding: 1.5rem;
}
.chat-header-controls {
margin-bottom: 1.5rem;
}
h1 {
overflow: hidden;
text-overflow: ellipsis;
}
.new-chat-icon {
width: 1.25rem;
height: 1.25rem;
color: rgb(55, 65, 81);
}
@media (min-width: 768px) {
.chat-header-container {
position: sticky;
}
}
@@ -0,0 +1,51 @@
@inject IJSRuntime JS
<EditForm Model="@this" OnValidSubmit="@SendMessageAsync">
<label class="input-box page-width">
<textarea @ref="@textArea" @bind="@messageText" placeholder="Type your message..." rows="1"></textarea>
<div class="tools">
<button type="submit" title="Send" class="send-button">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="tool-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
</svg>
</button>
</div>
</label>
</EditForm>
@code {
private ElementReference textArea;
private string? messageText;
[Parameter]
public EventCallback<ChatMessage> OnSend { get; set; }
public ValueTask FocusAsync()
=> textArea.FocusAsync();
private async Task SendMessageAsync()
{
if (messageText is { Length: > 0 } text)
{
messageText = null;
await OnSend.InvokeAsync(new ChatMessage(ChatRole.User, text));
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./Components/Pages/Chat/ChatInput.razor.js");
await module.InvokeVoidAsync("init", textArea);
await module.DisposeAsync();
}
catch (JSDisconnectedException)
{
}
}
}
}
@@ -0,0 +1,57 @@
.input-box {
display: flex;
flex-direction: column;
background: white;
border: 1px solid rgb(229, 231, 235);
border-radius: 8px;
padding: 0.5rem 0.75rem;
margin-top: 0.75rem;
}
.input-box:focus-within {
outline: 2px solid #4152d5;
}
textarea {
resize: none;
border: none;
outline: none;
flex-grow: 1;
}
textarea:placeholder-shown + .tools {
--send-button-color: #aaa;
}
.tools {
display: flex;
margin-top: 1rem;
align-items: center;
}
.tool-icon {
width: 1.25rem;
height: 1.25rem;
}
.send-button {
color: var(--send-button-color);
margin-left: auto;
}
.send-button:hover {
color: black;
}
.attach {
background-color: white;
border-style: dashed;
color: #888;
border-color: #888;
padding: 3px 8px;
}
.attach:hover {
background-color: #f0f0f0;
color: black;
}
@@ -0,0 +1,43 @@
export function init(elem) {
elem.focus();
// Auto-resize whenever the user types or if the value is set programmatically
elem.addEventListener('input', () => resizeToFit(elem));
afterPropertyWritten(elem, 'value', () => resizeToFit(elem));
// Auto-submit the form on 'enter' keypress
elem.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
elem.dispatchEvent(new CustomEvent('change', { bubbles: true }));
elem.closest('form').dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true }));
}
});
}
function resizeToFit(elem) {
const lineHeight = parseFloat(getComputedStyle(elem).lineHeight);
elem.rows = 1;
const numLines = Math.ceil(elem.scrollHeight / lineHeight);
elem.rows = Math.min(5, Math.max(1, numLines));
}
function afterPropertyWritten(target, propName, callback) {
const descriptor = getPropertyDescriptor(target, propName);
Object.defineProperty(target, propName, {
get: function () {
return descriptor.get.apply(this, arguments);
},
set: function () {
const result = descriptor.set.apply(this, arguments);
callback();
return result;
}
});
}
function getPropertyDescriptor(target, propertyName) {
return Object.getOwnPropertyDescriptor(target, propertyName)
|| getPropertyDescriptor(Object.getPrototypeOf(target), propertyName);
}
@@ -0,0 +1,73 @@
@using System.Runtime.CompilerServices
@using System.Text.RegularExpressions
@using System.Linq
@if (Message.Role == ChatRole.User)
{
<div class="user-message">
@Message.Text
</div>
}
else if (Message.Role == ChatRole.Assistant)
{
foreach (var content in Message.Contents)
{
if (content is TextContent { Text: { Length: > 0 } text })
{
<div class="assistant-message">
<div>
<div class="assistant-message-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" />
</svg>
</div>
</div>
<div class="assistant-message-header">Assistant</div>
<div class="assistant-message-text">
<div>@((MarkupString)text)</div>
</div>
</div>
}
else if (content is FunctionCallContent { Name: "Search" } fcc && fcc.Arguments?.TryGetValue("searchPhrase", out var searchPhrase) is true)
{
<div class="assistant-search">
<div class="assistant-search-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</div>
<div class="assistant-search-content">
Searching:
<span class="assistant-search-phrase">@searchPhrase</span>
@if (fcc.Arguments?.TryGetValue("filenameFilter", out var filenameObj) is true && filenameObj is string filename && !string.IsNullOrEmpty(filename))
{
<text> in </text><span class="assistant-search-phrase">@filename</span>
}
</div>
</div>
}
}
}
@code {
private static readonly ConditionalWeakTable<ChatMessage, ChatMessageItem> SubscribersLookup = new();
[Parameter, EditorRequired]
public required ChatMessage Message { get; set; }
[Parameter]
public bool InProgress { get; set;}
protected override void OnInitialized()
{
SubscribersLookup.AddOrUpdate(Message, this);
}
public static void NotifyChanged(ChatMessage source)
{
if (SubscribersLookup.TryGetValue(source, out var subscriber))
{
subscriber.StateHasChanged();
}
}
}
@@ -0,0 +1,67 @@
.user-message {
background: rgb(182 215 232);
align-self: flex-end;
min-width: 25%;
max-width: calc(100% - 5rem);
padding: 0.5rem 1.25rem;
border-radius: 0.25rem;
color: #1F2937;
white-space: pre-wrap;
}
.assistant-message, .assistant-search {
display: grid;
grid-template-rows: min-content;
grid-template-columns: 2rem minmax(0, 1fr);
gap: 0.25rem;
}
.assistant-message-header {
font-weight: 600;
}
.assistant-message-text {
grid-column-start: 2;
}
.assistant-message-icon {
display: flex;
justify-content: center;
align-items: center;
border-radius: 9999px;
width: 1.5rem;
height: 1.5rem;
color: #ffffff;
background: #9b72ce;
}
.assistant-message-icon svg {
width: 1rem;
height: 1rem;
}
.assistant-search {
font-size: 0.875rem;
line-height: 1.25rem;
}
.assistant-search-icon {
display: flex;
justify-content: center;
align-items: center;
width: 1.5rem;
height: 1.5rem;
}
.assistant-search-icon svg {
width: 1rem;
height: 1rem;
}
.assistant-search-content {
align-content: center;
}
.assistant-search-phrase {
font-weight: 600;
}
@@ -0,0 +1,42 @@
@inject IJSRuntime JS
<div class="message-list-container">
<chat-messages class="page-width message-list" in-progress="@(InProgressMessage is not null)">
@foreach (var message in Messages)
{
<ChatMessageItem @key="@message" Message="@message" />
}
@if (InProgressMessage is not null)
{
<ChatMessageItem Message="@InProgressMessage" InProgress="true" />
<LoadingSpinner />
}
else if (IsEmpty)
{
<div class="no-messages">@NoMessagesContent</div>
}
</chat-messages>
</div>
@code {
[Parameter]
public required IEnumerable<ChatMessage> Messages { get; set; }
[Parameter]
public ChatMessage? InProgressMessage { get; set; }
[Parameter]
public RenderFragment? NoMessagesContent { get; set; }
private bool IsEmpty => !Messages.Any(m => (m.Role == ChatRole.User || m.Role == ChatRole.Assistant) && !string.IsNullOrEmpty(m.Text));
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Activates the auto-scrolling behavior
await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/ChatMessageList.razor.js");
}
}
}
@@ -0,0 +1,22 @@
.message-list-container {
margin: 2rem 1.5rem;
flex-grow: 1;
}
.message-list {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.no-messages {
text-align: center;
font-size: 1.25rem;
color: #999;
margin-top: calc(40vh - 18rem);
}
chat-messages > ::deep div:last-of-type {
/* Adds some vertical buffer to so that suggestions don't overlap the output when they appear */
margin-bottom: 2rem;
}
@@ -0,0 +1,34 @@
// The following logic provides auto-scroll behavior for the chat messages list.
// If you don't want that behavior, you can simply not load this module.
window.customElements.define('chat-messages', class ChatMessages extends HTMLElement {
static _isFirstAutoScroll = true;
connectedCallback() {
this._observer = new MutationObserver(mutations => this._scheduleAutoScroll(mutations));
this._observer.observe(this, { childList: true, attributes: true });
}
disconnectedCallback() {
this._observer.disconnect();
}
_scheduleAutoScroll(mutations) {
// Debounce the calls in case multiple DOM updates occur together
cancelAnimationFrame(this._nextAutoScroll);
this._nextAutoScroll = requestAnimationFrame(() => {
const addedUserMessage = mutations.some(m => Array.from(m.addedNodes).some(n => n.parentElement === this && n.classList?.contains('user-message')));
const elem = this.lastElementChild;
if (ChatMessages._isFirstAutoScroll || addedUserMessage || this._elemIsNearScrollBoundary(elem, 300)) {
elem.scrollIntoView({ behavior: ChatMessages._isFirstAutoScroll ? 'instant' : 'smooth' });
ChatMessages._isFirstAutoScroll = false;
}
});
}
_elemIsNearScrollBoundary(elem, threshold) {
const maxScrollPos = document.body.scrollHeight - window.innerHeight;
const remainingScrollDistance = maxScrollPos - window.scrollY;
return remainingScrollDistance < elem.offsetHeight + threshold;
}
});
@@ -0,0 +1,78 @@
@inject IChatClient ChatClient
@if (suggestions is not null)
{
<div class="page-width suggestions">
@foreach (var suggestion in suggestions)
{
<button class="btn-subtle" @onclick="@(() => AddSuggestionAsync(suggestion))">
@suggestion
</button>
}
</div>
}
@code {
private static string Prompt = @"
Suggest up to 3 follow-up questions that I could ask you to help me complete my task.
Each suggestion must be a complete sentence, maximum 6 words.
Each suggestion must be phrased as something that I (the user) would ask you (the assistant) in response to your previous message,
for example 'How do I do that?' or 'Explain ...'.
If there are no suggestions, reply with an empty list.
";
private string[]? suggestions;
private CancellationTokenSource? cancellation;
[Parameter]
public EventCallback<ChatMessage> OnSelected { get; set; }
public void Clear()
{
suggestions = null;
cancellation?.Cancel();
}
public void Update(IReadOnlyList<ChatMessage> messages)
{
// Runs in the background and handles its own cancellation/errors
_ = UpdateSuggestionsAsync(messages);
}
private async Task UpdateSuggestionsAsync(IReadOnlyList<ChatMessage> messages)
{
cancellation?.Cancel();
cancellation = new CancellationTokenSource();
try
{
var response = await ChatClient.GetResponseAsync<string[]>(
[.. ReduceMessages(messages), new(ChatRole.User, Prompt)],
cancellationToken: cancellation.Token);
if (!response.TryGetResult(out suggestions))
{
suggestions = null;
}
StateHasChanged();
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
await DispatchExceptionAsync(ex);
}
}
private async Task AddSuggestionAsync(string text)
{
await OnSelected.InvokeAsync(new(ChatRole.User, text));
}
private IEnumerable<ChatMessage> ReduceMessages(IReadOnlyList<ChatMessage> messages)
{
// Get any leading system messages, plus up to 5 user/assistant messages
// This should be enough context to generate suggestions without unnecessarily resending entire conversations when long
var systemMessages = messages.TakeWhile(m => m.Role == ChatRole.System);
var otherMessages = messages.Where((m, index) => m.Role == ChatRole.User || m.Role == ChatRole.Assistant).Where(m => !string.IsNullOrEmpty(m.Text)).TakeLast(5);
return systemMessages.Concat(otherMessages);
}
}
@@ -0,0 +1,9 @@
.suggestions {
text-align: right;
white-space: nowrap;
gap: 0.5rem;
justify-content: flex-end;
flex-wrap: wrap;
display: flex;
margin-bottom: 0.75rem;
}
@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
@@ -0,0 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using AGUIWebChatClient
@using AGUIWebChatClient.Components
@using AGUIWebChatClient.Components.Layout
@using Microsoft.Extensions.AI
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft. All rights reserved.
using AGUI.Client;
using AGUIWebChatClient.Components;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
string serverUrl = builder.Configuration["AGUI_SERVER_URL"] ?? "http://localhost:5100";
builder.Services.AddHttpClient("aguiserver", httpClient => httpClient.BaseAddress = new Uri(serverUrl));
builder.Services.AddChatClient(sp => new AGUIChatClient(new(
sp.GetRequiredService<IHttpClientFactory>().CreateClient("aguiserver"), "ag-ui")));
WebApplication app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"AGUI_SERVER_URL": "http://localhost:5100"
}
}
}
}
@@ -0,0 +1,93 @@
html {
min-height: 100vh;
}
html, .main-background-gradient {
background: linear-gradient(to bottom, rgb(225 227 233), #f4f4f4 25rem);
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
html::after {
content: '';
background-image: linear-gradient(to right, #3a4ed5, #3acfd5 15%, #d53abf 85%, red);
width: 100%;
height: 2px;
position: fixed;
top: 0;
}
h1 {
font-size: 2.25rem;
line-height: 2.5rem;
font-weight: 600;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.btn-default {
display: flex;
padding: 0.25rem 0.75rem;
gap: 0.25rem;
align-items: center;
border-radius: 0.25rem;
border: 1px solid #9CA3AF;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 600;
background-color: #D1D5DB;
}
.btn-default:hover {
background-color: #E5E7EB;
}
.btn-subtle {
display: flex;
padding: 0.25rem 0.75rem;
gap: 0.25rem;
align-items: center;
border-radius: 0.25rem;
border: 1px solid #D1D5DB;
font-size: 0.875rem;
line-height: 1.25rem;
}
.btn-subtle:hover {
border-color: #93C5FD;
background-color: #DBEAFE;
}
.page-width {
max-width: 1024px;
margin: auto;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB