2024-08-01 19:53:28 +00:00
using AIStudio.Agents ;
using AIStudio.Chat ;
2026-08-23 09:09:11 +00:00
using AIStudio.Tools.Security ;
2026-09-04 13:48:07 +00:00
using AIStudio.Tools.Web ;
2024-08-01 19:53:28 +00:00
using Microsoft.AspNetCore.Components ;
2024-08-21 06:30:01 +00:00
namespace AIStudio.Components ;
2024-08-01 19:53:28 +00:00
2025-04-27 07:06:05 +00:00
public partial class ReadWebContent : MSGComponentBase
2024-08-01 19:53:28 +00:00
{
2026-09-04 13:48:07 +00: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 19:53:28 +00:00
[Inject]
2026-09-04 13:48:07 +00:00
private WebPageRetrievalService WebPageRetrievalService { get ; init ; } = null ! ;
[Inject]
private ILogger < ReadWebContent > Logger { get ; init ; } = null ! ;
2024-08-01 19:53:28 +00:00
[Inject]
private AgentTextContentCleaner AgentTextContentCleaner { get ; init ; } = null ! ;
2024-08-05 19:12:52 +00:00
2026-08-23 09:09:11 +00:00
[Inject]
private PromptInjectionGuardService PromptInjectionGuardService { get ; init ; } = null ! ;
2024-08-01 19:53:28 +00:00
[Parameter]
public string Content { get ; set ; } = string . Empty ;
[Parameter]
public EventCallback < string > ContentChanged { get ; set ; }
[Parameter]
2025-08-26 08:59:56 +00:00
public AIStudio . Settings . Provider ProviderSettings { get ; set ; } = AIStudio . Settings . Provider . NONE ;
2024-08-01 19:53:28 +00:00
[Parameter]
public bool AgentIsRunning { get ; set ; }
[Parameter]
public EventCallback < bool > AgentIsRunningChanged { get ; set ; }
[Parameter]
public bool Preselect { get ; set ; }
2025-09-01 17:13:02 +00:00
[Parameter]
public EventCallback < bool > PreselectChanged { get ; set ; }
2024-08-01 19:53:28 +00:00
[Parameter]
public bool PreselectContentCleanerAgent { get ; set ; }
2025-09-01 17:13:02 +00:00
[Parameter]
public EventCallback < bool > PreselectContentCleanerAgentChanged { get ; set ; }
2024-08-01 19:53:28 +00:00
2025-08-26 08:59:56 +00:00
private readonly Process < ReadWebContentSteps > process = Process < ReadWebContentSteps > . INSTANCE ;
2024-08-01 19:53:28 +00:00
private ProcessStepValue processStep ;
private string providedURL = string . Empty ;
private bool urlIsValid ;
private bool isProviderValid ;
2025-08-26 08:59:56 +00:00
private AIStudio . Settings . Provider providerSettings = AIStudio . Settings . Provider . NONE ;
2024-08-01 19:53:28 +00:00
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ( )
{
2025-02-23 14:05:29 +00:00
this . ProviderSettings = this . SettingsManager . GetPreselectedProvider ( Tools . Components . AGENT_TEXT_CONTENT_CLEANER , this . ProviderSettings . Id , true ) ;
2025-09-01 17:13:02 +00:00
this . providerSettings = this . ProviderSettings ;
this . ValidateProvider ( this . PreselectContentCleanerAgent ) ;
2024-08-01 19:53:28 +00:00
await base . OnInitializedAsync ( ) ;
}
protected override async Task OnParametersSetAsync ( )
{
2024-08-05 19:12:52 +00:00
if ( ! this . SettingsManager . ConfigurationData . TextContentCleaner . PreselectAgentOptions )
2024-08-01 19:53:28 +00:00
this . providerSettings = this . ProviderSettings ;
2025-09-01 17:13:02 +00:00
this . ValidateProvider ( this . PreselectContentCleanerAgent ) ;
2024-08-01 19:53:28 +00: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 13:48:07 +00: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 (
new Uri ( this . providedURL ) ,
new WebPageRetrievalOptions
{
TimeoutSeconds = TIMEOUT_SECONDS ,
TargetChosenByUser = true ,
} ) ;
2024-08-01 19:53:28 +00:00
this . processStep = this . process [ ReadWebContentSteps . PARSING ] ;
this . StateHasChanged ( ) ;
2026-09-04 13:48:07 +00:00
markdown = retrievedPage . ExtractedPage . Markdown ;
2026-08-23 09:09:11 +00:00
markdown = await this . PromptInjectionGuardService . SanitizeAsync ( markdown , PromptInjectionSource . WebContent ( this . providedURL ) ) ;
2024-08-01 19:53:28 +00:00
2025-09-01 17:13:02 +00:00
if ( this . PreselectContentCleanerAgent & & this . providerSettings ! = AIStudio . Settings . Provider . NONE )
2024-08-01 19:53:28 +00:00
{
this . AgentTextContentCleaner . ProviderSettings = this . providerSettings ;
var additionalData = new Dictionary < string , string >
{
{ "sourceURL" , this . providedURL } ,
} ;
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 13:48:07 +00:00
catch ( Exception exception )
2024-08-01 19:53:28 +00:00
{
if ( this . AgentIsRunning )
{
this . processStep = this . process [ ReadWebContentSteps . START ] ;
this . AgentIsRunning = false ;
await this . AgentIsRunningChanged . InvokeAsync ( this . AgentIsRunning ) ;
this . StateHasChanged ( ) ;
}
2026-09-04 13:48:07 +00: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.
//
this . Logger . LogWarning ( exception , "Could not load the web content from '{ProvidedUrl}'." , this . providedURL ) ;
await this . MessageBus . SendError ( new ( Icons . Material . Filled . CloudOff , string . Format ( this . T ( "The content of '{0}' could not be loaded: {1}" ) , this . providedURL , exception . Message ) ) ) ;
2024-08-01 19:53:28 +00:00
}
this . Content = markdown ;
await this . ContentChanged . InvokeAsync ( this . Content ) ;
}
private bool IsReady
{
get
{
if ( ! this . urlIsValid )
return false ;
2025-09-01 17:13:02 +00:00
if ( this . PreselectContentCleanerAgent & & ! this . isProviderValid )
2024-08-01 19:53:28 +00:00
return false ;
return true ;
}
}
2025-09-01 17:13:02 +00: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 19:53:28 +00:00
private string? ValidateProvider ( bool shouldUseAgent )
{
2025-08-26 08:59:56 +00:00
if ( shouldUseAgent & & this . providerSettings = = AIStudio . Settings . Provider . NONE )
2024-08-01 19:53:28 +00:00
{
this . isProviderValid = false ;
2025-04-27 07:06:05 +00:00
return T ( "Please select a provider to use the cleanup agent." ) ;
2024-08-01 19:53:28 +00:00
}
this . isProviderValid = true ;
return null ;
}
private string? ValidateURL ( string url )
{
if ( string . IsNullOrWhiteSpace ( url ) )
{
this . urlIsValid = false ;
2025-04-27 07:06:05 +00:00
return T ( "Please provide a URL to load the content from." ) ;
2024-08-01 19:53:28 +00:00
}
var urlParsingResult = Uri . TryCreate ( url , UriKind . Absolute , out var uriResult ) ;
if ( ! urlParsingResult )
{
this . urlIsValid = false ;
2025-04-27 07:06:05 +00:00
return T ( "Please provide a valid URL." ) ;
2024-08-01 19:53:28 +00:00
}
if ( uriResult is not { Scheme : "http" or "https" } )
{
this . urlIsValid = false ;
2025-04-27 07:06:05 +00:00
return T ( "Please provide a valid HTTP or HTTPS URL." ) ;
2024-08-01 19:53:28 +00:00
}
this . urlIsValid = true ;
return null ;
}
}