commit eafafda65d44b32ccddc7d3cc4cb52cd7c208322 Author: David Friedel Date: Sun Dec 28 18:50:57 2025 +0000 Initial commit - ViewEngine.WebSample diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d3f180 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Build results +bin/ +obj/ +[Dd]ebug/ +[Rr]elease/ + +# Visual Studio +.vs/ +*.user +*.suo + +# JetBrains Rider +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# User secrets +appsettings.*.json +!appsettings.json diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..fbddecd --- /dev/null +++ b/Program.cs @@ -0,0 +1,200 @@ +using ViewEngine.Client; +using ViewEngine.Client.Models; + +var builder = WebApplication.CreateBuilder(args); + +// Add services +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +var app = builder.Build(); + +app.UseCors(); +app.UseDefaultFiles(); +app.UseStaticFiles(); + +// Proxy endpoint to submit retrieval request +app.MapPost("/api/retrieve", async (RetrieveRequest request) => +{ + try + { + if (string.IsNullOrEmpty(request.ApiKey)) + { + return Results.BadRequest(new { error = "API key is required" }); + } + + using var client = new ViewEngineClient(request.ApiKey); + + var submitRequest = new SubmitRetrievalRequest + { + Url = request.Url, + ForceRefresh = request.ForceRefresh, + PreferredPlatform = request.PreferredPlatform, + GenerateSummary = request.GenerateSummary + }; + + var response = await client.SubmitRetrievalAsync(submitRequest); + + return Results.Ok(new + { + requestId = response.RequestId, + status = response.Status, + message = response.Message, + estimatedWaitTimeSeconds = response.EstimatedWaitTimeSeconds + }); + } + catch (HttpRequestException ex) + { + return Results.Problem(ex.Message, statusCode: 502); + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } +}); + +// Get status of a retrieval request +app.MapGet("/api/status/{requestId}", async (Guid requestId, string apiKey) => +{ + try + { + if (string.IsNullOrEmpty(apiKey)) + { + return Results.BadRequest(new { error = "API key is required" }); + } + + using var client = new ViewEngineClient(apiKey); + var status = await client.GetRetrievalStatusAsync(requestId); + + return Results.Ok(new + { + requestId = status.RequestId, + url = status.Url, + status = status.Status, + message = status.Message, + error = status.Error, + content = status.Content, + createdAt = status.CreatedAt, + completedAt = status.CompletedAt + }); + } + catch (HttpRequestException ex) + { + return Results.Problem(ex.Message, statusCode: 502); + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } +}); + +// Get page content for a completed retrieval +app.MapGet("/api/content/{requestId}", async (Guid requestId, string apiKey) => +{ + try + { + if (string.IsNullOrEmpty(apiKey)) + { + return Results.BadRequest(new { error = "API key is required" }); + } + + using var client = new ViewEngineClient(apiKey); + var pageData = await client.GetPageContentAsync(requestId); + + return Results.Ok(new + { + title = pageData.Title, + url = pageData.Url, + body = pageData.Body, + metaDescription = pageData.MetaDescription, + faviconUrl = pageData.FaviconUrl, + thumbnail = pageData.Thumbnail, + routes = pageData.Routes, + bodyRoutes = pageData.BodyRoutes, + summary = pageData.Summary, + httpStatusCode = pageData.HttpStatusCode, + isSuccess = pageData.IsSuccess, + isClientError = pageData.IsClientError, + isServerError = pageData.IsServerError + }); + } + catch (HttpRequestException ex) + { + return Results.Problem(ex.Message, statusCode: 502); + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } +}); + +// One-shot retrieve and wait endpoint (convenience method) +app.MapPost("/api/retrieve-and-wait", async (RetrieveRequest request, CancellationToken cancellationToken) => +{ + try + { + if (string.IsNullOrEmpty(request.ApiKey)) + { + return Results.BadRequest(new { error = "API key is required" }); + } + + using var client = new ViewEngineClient(request.ApiKey); + + var submitRequest = new SubmitRetrievalRequest + { + Url = request.Url, + ForceRefresh = request.ForceRefresh, + PreferredPlatform = request.PreferredPlatform, + GenerateSummary = request.GenerateSummary, + TimeoutSeconds = 120 + }; + + var pageData = await client.RetrieveAndWaitAsync(submitRequest, cancellationToken: cancellationToken); + + return Results.Ok(new + { + title = pageData.Title, + url = pageData.Url, + body = pageData.Body, + metaDescription = pageData.MetaDescription, + faviconUrl = pageData.FaviconUrl, + thumbnail = pageData.Thumbnail, + routes = pageData.Routes, + bodyRoutes = pageData.BodyRoutes, + summary = pageData.Summary, + httpStatusCode = pageData.HttpStatusCode, + isSuccess = pageData.IsSuccess, + isClientError = pageData.IsClientError, + isServerError = pageData.IsServerError + }); + } + catch (OperationCanceledException) + { + return Results.StatusCode(408); // Request Timeout + } + catch (HttpRequestException ex) + { + return Results.Problem(ex.Message, statusCode: 502); + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } +}); + +app.Run(); + +record RetrieveRequest( + string ApiKey, + string Url, + bool ForceRefresh = false, + string? PreferredPlatform = null, + bool GenerateSummary = false +); diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..36b72fe --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,22 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5100", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7100;http://localhost:5100", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd7dc7d --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# ViewEngine Web Sample + +A standalone web application that demonstrates how to use the ViewEngine REST API. Users can test page retrieval and see the results including content, thumbnails, and metadata. + +## Features + +- 🔑 **API Key Management** - Securely stored in browser localStorage +- 🌐 **URL Retrieval** - Enter any URL to retrieve its content +- 📊 **Real-time Status** - Live polling shows retrieval progress +- 📸 **Thumbnail Display** - View captured page thumbnails +- 📄 **Content Preview** - See the full page data JSON +- 🎨 **Modern UI** - Beautiful, responsive interface + +## Getting Started + +### Prerequisites + +- .NET 9.0 SDK +- A ViewEngine API key (get one from https://www.viewengine.io) + +### Running the Application + +1. **Navigate to the project directory:** + ```bash + cd ViewEngine.WebSample + ``` + +2. **Run the application:** + ```bash + dotnet run + ``` + +3. **Open your browser:** + - Navigate to `http://localhost:5000` or `https://localhost:5001` + +4. **Enter your API key:** + - Paste your ViewEngine API key in the API Key field + - The key is stored locally in your browser for convenience + +5. **Test a retrieval:** + - Enter a URL (e.g., `https://example.com`) + - Optionally check "Force fresh retrieval" to bypass cache + - Click "Retrieve Page" + - Watch the real-time progress as the page is processed + - View the results including thumbnail and content + +## How It Works + +### Architecture + +``` +Browser <-> WebSample Server <-> ViewEngine API +``` + +The WebSample server acts as a proxy to: +- Keep your API key secure (not exposed in browser JavaScript) +- Handle CORS issues +- Simplify the API interaction + +### API Endpoints + +The application creates three proxy endpoints: + +1. **POST /api/retrieve** - Submit a retrieval request +2. **GET /api/status/{requestId}** - Poll for retrieval status +3. **GET /api/content/{requestId}** - Download the page content + +### Workflow + +1. User enters API key and URL +2. WebSample sends request to ViewEngine API +3. Polls for status updates every 2 seconds +4. When complete, downloads and displays: + - Page metadata + - Thumbnail image + - Full JSON content + +## Configuration + +To use a different ViewEngine API endpoint, edit `Program.cs`: + +```csharp +const string API_BASE_URL = "https://your-custom-endpoint.com"; +``` + +For local development with the ViewEngine API running locally: + +```csharp +const string API_BASE_URL = "http://localhost:5072"; +``` + +## Security Notes + +- API keys are stored in browser localStorage (client-side only) +- The proxy server never logs or stores API keys +- All requests to ViewEngine API include proper authentication headers +- CORS is enabled to allow browser-based requests + +## Troubleshooting + +### "Invalid API key" error +- Verify your API key is correct +- Check that your API key has the `mcp:retrieve` scope + +### "Connection error" +- Ensure the ViewEngine API is accessible +- Check your internet connection +- Verify the API_BASE_URL is correct + +### Results not appearing +- Check browser console for errors +- Verify the job completed successfully +- Ensure the response includes content data + +## Example URLs to Try + +- `https://example.com` - Simple page +- `https://news.google.com` - News site +- `https://github.com` - GitHub homepage +- `https://www.viewengine.io` - ViewEngine homepage + +## Building for Production + +```bash +dotnet publish -c Release -o ./publish +``` + +Then deploy the contents of `./publish` to your web server. + +## License + +This sample application is provided as-is for demonstration purposes. diff --git a/ViewEngine.WebSample.csproj b/ViewEngine.WebSample.csproj new file mode 100644 index 0000000..95eb1ff --- /dev/null +++ b/ViewEngine.WebSample.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/wwwroot/index.html b/wwwroot/index.html new file mode 100644 index 0000000..c1cad63 --- /dev/null +++ b/wwwroot/index.html @@ -0,0 +1,588 @@ + + + + + + ViewEngine Web Sample + + + +
+
+

🚀 ViewEngine Web Sample

+

Test the ViewEngine REST API and see page retrieval in action

+
+ +
+
+ + + Your API key is stored locally and never sent to this demo server +
+ +
+ + +
+ +
+
+ + +
+
+ +
+ + + Private mode requires your own feeders. Community mode may be processed by any available feeder. +
+ + + +
+
+ +
+

📊 Results

+ + + + + + + + +
+
+ + + +