2024-08-01 21:53:28 +02:00
using AIStudio.Agents ;
using AIStudio.Chat ;
2026-08-23 11:09:11 +02:00
using AIStudio.Tools.Security ;
2026-09-04 15:48:07 +02:00
using AIStudio.Tools.Web ;
2024-08-01 21:53:28 +02:00
using Microsoft.AspNetCore.Components ;
2024-08-21 08:30:01 +02:00
namespace AIStudio.Components ;
2024-08-01 21:53:28 +02:00
2025-04-27 09:06:05 +02:00
public partial class ReadWebContent : MSGComponentBase
2024-08-01 21:53:28 +02:00
{
2026-09-04 15:48:07 +02:00
/// <summary>
/// How long loading one page may take.
/// </summary>
/// <remarks>
/// The user is watching a progress indicator while this runs, so it is shorter than what the
/// tools allow themselves for a page fetched in the background.
/// </remarks>
private const int TIMEOUT_SECONDS = 60 ;
2024-08-01 21:53:28 +02:00
[Inject]
2026-09-04 15:48:07 +02:00
private WebPageRetrievalService WebPageRetrievalService { get ; init ; } = null !;
[Inject]
private ILogger < ReadWebContent > Logger { get ; init ; } = null !;
2024-08-01 21:53:28 +02:00
[Inject]
private AgentTextContentCleaner AgentTextContentCleaner { get ; init ; } = null !;
2024-08-05 21:12:52 +02:00
2026-08-23 11:09:11 +02:00
[Inject]
private PromptInjectionGuardService PromptInjectionGuardService { get ; init ; } = null !;
2024-08-01 21:53:28 +02:00
[Parameter]
public string Content { get ; set ; } = string . Empty ;
[Parameter]
public EventCallback < string > ContentChanged { get ; set ; }
2026-09-13 16:24:36 +02:00
/// <summary>
/// The URL the content is loaded from.
/// </summary>
/// <remarks>
/// The URL belongs to the parent, so that it is cleared when the parent resets its form and
/// is kept when the parent stores its state.
/// </remarks>
[Parameter]
public string URL { get ; set ; } = string . Empty ;
[Parameter]
public EventCallback < string > URLChanged { get ; set ; }
2024-08-01 21:53:28 +02:00
[Parameter]
2025-08-26 10:59:56 +02:00
public AIStudio . Settings . Provider ProviderSettings { get ; set ; } = AIStudio . Settings . Provider . NONE ;
2024-08-01 21:53:28 +02:00
[Parameter]
public bool AgentIsRunning { get ; set ; }
[Parameter]
public EventCallback < bool > AgentIsRunningChanged { get ; set ; }
[Parameter]
public bool Preselect { get ; set ; }
2025-09-01 19:13:02 +02:00
[Parameter]
public EventCallback < bool > PreselectChanged { get ; set ; }
2024-08-01 21:53:28 +02:00
[Parameter]
public bool PreselectContentCleanerAgent { get ; set ; }
2025-09-01 19:13:02 +02:00
[Parameter]
public EventCallback < bool > PreselectContentCleanerAgentChanged { get ; set ; }
2024-08-01 21:53:28 +02:00
2025-08-26 10:59:56 +02:00
private readonly Process < ReadWebContentSteps > process = Process < ReadWebContentSteps >. INSTANCE ;
2024-08-01 21:53:28 +02:00
private ProcessStepValue processStep ;
private bool isProviderValid ;
2025-08-26 10:59:56 +02:00
private AIStudio . Settings . Provider providerSettings = AIStudio . Settings . Provider . NONE ;
2024-08-01 21:53:28 +02:00
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ()
{
2025-02-23 15:05:29 +01:00
this . ProviderSettings = this . SettingsManager . GetPreselectedProvider ( Tools . Components . AGENT_TEXT_CONTENT_CLEANER , this . ProviderSettings . Id , true );
2025-09-01 19:13:02 +02:00
this . providerSettings = this . ProviderSettings ;
this . ValidateProvider ( this . PreselectContentCleanerAgent );
2024-08-01 21:53:28 +02:00
await base . OnInitializedAsync ();
}
protected override async Task OnParametersSetAsync ()
{
2024-08-05 21:12:52 +02:00
if (! this . SettingsManager . ConfigurationData . TextContentCleaner . PreselectAgentOptions )
2024-08-01 21:53:28 +02:00
this . providerSettings = this . ProviderSettings ;
2025-09-01 19:13:02 +02:00
this . ValidateProvider ( this . PreselectContentCleanerAgent );
2024-08-01 21:53:28 +02:00
await base . OnParametersSetAsync ();
}
#endregion
private async Task LoadFromWeb ()
{
if (! this . IsReady )
return ;
var markdown = string . Empty ;
try
{
this . processStep = this . process [ ReadWebContentSteps . LOADING ];
this . StateHasChanged ();
2026-09-04 15:48:07 +02:00
//
// The same retrieval the read web page tool uses, so a page is fetched and read one
// way throughout AI Studio. The difference is the target policy: here the user typed
// the URL, so their own network is not off limits.
//
var retrievedPage = await this . WebPageRetrievalService . RetrieveAsync (
2026-09-13 16:24:36 +02:00
new Uri ( this . URL ),
2026-09-04 15:48:07 +02:00
new WebPageRetrievalOptions
{
TimeoutSeconds = TIMEOUT_SECONDS ,
TargetChosenByUser = true ,
});
2024-08-01 21:53:28 +02:00
this . processStep = this . process [ ReadWebContentSteps . PARSING ];
this . StateHasChanged ();
2026-09-04 15:48:07 +02:00
markdown = retrievedPage . ExtractedPage . Markdown ;
2026-09-13 16:24:36 +02:00
markdown = await this . PromptInjectionGuardService . SanitizeAsync ( markdown , PromptInjectionSource . WebContent ( this . URL ));
2024-08-01 21:53:28 +02:00
2025-09-01 19:13:02 +02:00
if ( this . PreselectContentCleanerAgent && this . providerSettings != AIStudio . Settings . Provider . NONE )
2024-08-01 21:53:28 +02:00
{
this . AgentTextContentCleaner . ProviderSettings = this . providerSettings ;
var additionalData = new Dictionary < string , string >
{
2026-09-13 16:24:36 +02:00
{ "sourceURL" , this . URL },
2024-08-01 21:53:28 +02:00
};
this . processStep = this . process [ ReadWebContentSteps . CLEANING ];
this . AgentIsRunning = true ;
await this . AgentIsRunningChanged . InvokeAsync ( this . AgentIsRunning );
this . StateHasChanged ();
var contentBlock = await this . AgentTextContentCleaner . ProcessInput ( new ContentBlock
{
Time = DateTimeOffset . UtcNow ,
ContentType = ContentType . TEXT ,
Role = ChatRole . USER ,
Content = new ContentText
{
Text = markdown ,
},
}, additionalData );
markdown = contentBlock . Content is ContentText text ? text . Text : markdown ;
this . processStep = this . process [ ReadWebContentSteps . DONE ];
this . AgentIsRunning = false ;
await this . AgentIsRunningChanged . InvokeAsync ( this . AgentIsRunning );
this . StateHasChanged ();
}
}
2026-09-04 15:48:07 +02:00
catch ( Exception exception )
2024-08-01 21:53:28 +02:00
{
if ( this . AgentIsRunning )
{
this . processStep = this . process [ ReadWebContentSteps . START ];
this . AgentIsRunning = false ;
await this . AgentIsRunningChanged . InvokeAsync ( this . AgentIsRunning );
this . StateHasChanged ();
}
2026-09-04 15:48:07 +02:00
//
// Say why nothing was loaded. An empty text field looks like a page without content,
// and the reasons a page cannot be read are things the user can act on: a link to a
// PDF rather than a page, a host that does not answer, a server refusing the request.
//
2026-09-13 16:24:36 +02:00
this . Logger . LogWarning ( exception , "Could not load the web content from '{ProvidedUrl}'." , this . URL );
await this . MessageBus . SendError ( new ( Icons . Material . Filled . CloudOff , string . Format ( this . T ( "The content of '{0}' could not be loaded: {1}" ), this . URL , exception . Message )));
2024-08-01 21:53:28 +02:00
}
this . Content = markdown ;
await this . ContentChanged . InvokeAsync ( this . Content );
}
private bool IsReady
{
get
{
2026-09-13 16:24:36 +02:00
if (! this . UrlIsValid )
2024-08-01 21:53:28 +02:00
return false ;
2026-09-13 16:24:36 +02:00
2025-09-01 19:13:02 +02:00
if ( this . PreselectContentCleanerAgent && ! this . isProviderValid )
2024-08-01 21:53:28 +02:00
return false ;
2026-09-13 16:24:36 +02:00
2024-08-01 21:53:28 +02:00
return true ;
}
}
2026-09-13 16:24:36 +02:00
/// <summary>
/// Whether the current URL can be loaded.
/// </summary>
/// <remarks>
/// Asked of the current value instead of remembered from the last validation run: the parent
/// clears the URL when it resets its form, and the form validation does not run again at that
/// point. The fetch button would otherwise stay enabled with an empty field.
/// </remarks>
private bool UrlIsValid => this . ValidateURL ( this . URL ) is null ;
private async Task URLValueChanged ( string url )
{
await this . URLChanged . InvokeAsync ( url );
}
2025-09-01 19:13:02 +02:00
private async Task ShowWebContentReaderChanged ( bool state )
{
await this . PreselectChanged . InvokeAsync ( state );
}
private async Task UseContentCleanerAgentChanged ( bool state )
{
await this . PreselectContentCleanerAgentChanged . InvokeAsync ( state );
}
2024-08-01 21:53:28 +02:00
private string? ValidateProvider ( bool shouldUseAgent )
{
2025-08-26 10:59:56 +02:00
if ( shouldUseAgent && this . providerSettings == AIStudio . Settings . Provider . NONE )
2024-08-01 21:53:28 +02:00
{
this . isProviderValid = false ;
2025-04-27 09:06:05 +02:00
return T ( "Please select a provider to use the cleanup agent." );
2024-08-01 21:53:28 +02:00
}
this . isProviderValid = true ;
return null ;
}
private string? ValidateURL ( string url )
{
if ( string . IsNullOrWhiteSpace ( url ))
2025-04-27 09:06:05 +02:00
return T ( "Please provide a URL to load the content from." );
2024-08-01 21:53:28 +02:00
var urlParsingResult = Uri . TryCreate ( url , UriKind . Absolute , out var uriResult );
if (! urlParsingResult )
2025-04-27 09:06:05 +02:00
return T ( "Please provide a valid URL." );
2024-08-01 21:53:28 +02:00
if ( uriResult is not { Scheme : "http" or "https" })
2025-04-27 09:06:05 +02:00
return T ( "Please provide a valid HTTP or HTTPS URL." );
2024-08-01 21:53:28 +02:00
return null ;
}
}