From 604ec5b9ad35719b76a7661ec711f234ed7c3d29 Mon Sep 17 00:00:00 2001 From: David Friedel Date: Sun, 28 Dec 2025 20:37:44 +0000 Subject: [PATCH] Initial commit - CiteLynq Web Sample --- CiteLynq.WebSample.csproj | 9 + Program.cs | 261 +++++++++++ Properties/launchSettings.json | 13 + README.md | 268 +++++++++++ wwwroot/index.html | 794 +++++++++++++++++++++++++++++++++ 5 files changed, 1345 insertions(+) create mode 100644 CiteLynq.WebSample.csproj create mode 100644 Program.cs create mode 100644 Properties/launchSettings.json create mode 100644 README.md create mode 100644 wwwroot/index.html diff --git a/CiteLynq.WebSample.csproj b/CiteLynq.WebSample.csproj new file mode 100644 index 0000000..6568b3d --- /dev/null +++ b/CiteLynq.WebSample.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..256b35d --- /dev/null +++ b/Program.cs @@ -0,0 +1,261 @@ +using System.Text; +using System.Text.Json; + +var builder = WebApplication.CreateBuilder(args); + +// Add services +builder.Services.AddHttpClient(); +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +var app = builder.Build(); + +app.UseCors(); +app.UseDefaultFiles(); +app.UseStaticFiles(); + +// Update this to point to your local API or production API +const string API_BASE_URL = "https://citelynq.com/api"; + +// Proxy endpoint for full-text search +app.MapGet("/api/search", async (HttpContext context, IHttpClientFactory httpClientFactory, ILogger logger) => +{ + try + { + var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault(); + + if (string.IsNullOrEmpty(apiKey)) + { + return Results.BadRequest(new { error = "API key is required" }); + } + + // Get query parameters + var query = context.Request.Query["q"].FirstOrDefault(); + var source = context.Request.Query["source"].FirstOrDefault(); + var limit = context.Request.Query["limit"].FirstOrDefault() ?? "20"; + + if (string.IsNullOrEmpty(query)) + { + return Results.BadRequest(new { error = "Search query (q) is required" }); + } + + // Build URL for CiteLynq API + var httpClient = httpClientFactory.CreateClient(); + var url = $"{API_BASE_URL}/search?q={Uri.EscapeDataString(query)}&limit={limit}"; + + if (!string.IsNullOrEmpty(source)) + { + url += $"&source={Uri.EscapeDataString(source)}"; + } + + logger.LogInformation("Calling CiteLynq API: {Url}", url); + + var httpRequest = new HttpRequestMessage(HttpMethod.Get, url); + httpRequest.Headers.Add("X-API-Key", apiKey); + + var response = await httpClient.SendAsync(httpRequest); + var responseContent = await response.Content.ReadAsStringAsync(); + + logger.LogInformation("CiteLynq API Response Status: {StatusCode}", response.StatusCode); + + if (!response.IsSuccessStatusCode) + { + logger.LogError("CiteLynq API Error: {Response}", responseContent); + try + { + var errorObj = JsonSerializer.Deserialize(responseContent); + return Results.Json(errorObj, statusCode: (int)response.StatusCode); + } + catch + { + return Results.Json(new { error = responseContent }, statusCode: (int)response.StatusCode); + } + } + + return Results.Content(responseContent, "application/json"); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in search endpoint"); + return Results.Json(new { error = ex.Message, details = ex.ToString() }, statusCode: 500); + } +}); + +// Proxy endpoint for semantic search +app.MapGet("/api/search/semantic", async (HttpContext context, IHttpClientFactory httpClientFactory, ILogger logger) => +{ + try + { + var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault(); + + if (string.IsNullOrEmpty(apiKey)) + { + return Results.BadRequest(new { error = "API key is required" }); + } + + // Get query parameters + var query = context.Request.Query["q"].FirstOrDefault(); + var source = context.Request.Query["source"].FirstOrDefault(); + var limit = context.Request.Query["limit"].FirstOrDefault() ?? "20"; + + if (string.IsNullOrEmpty(query)) + { + return Results.BadRequest(new { error = "Search query (q) is required" }); + } + + // Build URL for CiteLynq API + var httpClient = httpClientFactory.CreateClient(); + var url = $"{API_BASE_URL}/search/semantic?q={Uri.EscapeDataString(query)}&limit={limit}"; + + if (!string.IsNullOrEmpty(source)) + { + url += $"&source={Uri.EscapeDataString(source)}"; + } + + logger.LogInformation("Calling CiteLynq API: {Url}", url); + + var httpRequest = new HttpRequestMessage(HttpMethod.Get, url); + httpRequest.Headers.Add("X-API-Key", apiKey); + + var response = await httpClient.SendAsync(httpRequest); + var responseContent = await response.Content.ReadAsStringAsync(); + + logger.LogInformation("CiteLynq API Response Status: {StatusCode}", response.StatusCode); + + if (!response.IsSuccessStatusCode) + { + logger.LogError("CiteLynq API Error: {Response}", responseContent); + try + { + var errorObj = JsonSerializer.Deserialize(responseContent); + return Results.Json(errorObj, statusCode: (int)response.StatusCode); + } + catch + { + return Results.Json(new { error = responseContent }, statusCode: (int)response.StatusCode); + } + } + + return Results.Content(responseContent, "application/json"); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in semantic search endpoint"); + return Results.Json(new { error = ex.Message, details = ex.ToString() }, statusCode: 500); + } +}); + +// Proxy endpoint for date-based search +app.MapGet("/api/search/bydate", async (HttpContext context, IHttpClientFactory httpClientFactory, ILogger logger) => +{ + try + { + var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault(); + + if (string.IsNullOrEmpty(apiKey)) + { + return Results.BadRequest(new { error = "API key is required" }); + } + + // Get query parameters + var query = context.Request.Query["q"].FirstOrDefault(); + var month = context.Request.Query["month"].FirstOrDefault(); + var day = context.Request.Query["day"].FirstOrDefault(); + var source = context.Request.Query["source"].FirstOrDefault(); + var limit = context.Request.Query["limit"].FirstOrDefault() ?? "20"; + + if (string.IsNullOrEmpty(query)) + { + return Results.BadRequest(new { error = "Search query (q) is required" }); + } + + if (string.IsNullOrEmpty(month) || string.IsNullOrEmpty(day)) + { + return Results.BadRequest(new { error = "Month and day parameters are required for date search" }); + } + + // Build URL for CiteLynq API + var httpClient = httpClientFactory.CreateClient(); + var url = $"{API_BASE_URL}/search/bydate?q={Uri.EscapeDataString(query)}&month={month}&day={day}&limit={limit}"; + + if (!string.IsNullOrEmpty(source)) + { + url += $"&source={Uri.EscapeDataString(source)}"; + } + + logger.LogInformation("Calling CiteLynq API: {Url}", url); + + var httpRequest = new HttpRequestMessage(HttpMethod.Get, url); + httpRequest.Headers.Add("X-API-Key", apiKey); + + var response = await httpClient.SendAsync(httpRequest); + var responseContent = await response.Content.ReadAsStringAsync(); + + logger.LogInformation("CiteLynq API Response Status: {StatusCode}", response.StatusCode); + + if (!response.IsSuccessStatusCode) + { + logger.LogError("CiteLynq API Error: {Response}", responseContent); + try + { + var errorObj = JsonSerializer.Deserialize(responseContent); + return Results.Json(errorObj, statusCode: (int)response.StatusCode); + } + catch + { + return Results.Json(new { error = responseContent }, statusCode: (int)response.StatusCode); + } + } + + return Results.Content(responseContent, "application/json"); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in date search endpoint"); + return Results.Json(new { error = ex.Message, details = ex.ToString() }, statusCode: 500); + } +}); + +// Proxy endpoint to get article details +app.MapGet("/api/articles/{id}", async (string id, HttpContext context, IHttpClientFactory httpClientFactory) => +{ + try + { + var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault(); + + if (string.IsNullOrEmpty(apiKey)) + { + return Results.BadRequest(new { error = "API key is required" }); + } + + var httpClient = httpClientFactory.CreateClient(); + var httpRequest = new HttpRequestMessage(HttpMethod.Get, $"{API_BASE_URL}/articles/{id}"); + httpRequest.Headers.Add("X-API-Key", apiKey); + + var response = await httpClient.SendAsync(httpRequest); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + return Results.Json( + JsonSerializer.Deserialize(responseContent), + statusCode: (int)response.StatusCode + ); + } + + return Results.Content(responseContent, "application/json"); + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } +}); + +app.Run(); diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..6dd15b6 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5100", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..483220e --- /dev/null +++ b/README.md @@ -0,0 +1,268 @@ +# CiteLynq Web Sample + +A sample ASP.NET Core web application demonstrating how to use the CiteLynq API to search for verified citations across multiple authoritative sources. + +## Features + +✅ ASP.NET Core 9.0 minimal API +✅ Static file hosting with wwwroot +✅ Proxy endpoints to hide API keys from client +✅ Clean, modern UI with gradient design +✅ Search across multiple data sources +✅ Display formatted citations (APA, MLA, Bluebook) +✅ Filter by source type (ArXiv, PubMed, CourtListener, etc.) +✅ Responsive design +✅ Local storage for API key persistence + +## Quick Start + +### 1. Get an API Key + +1. Visit [https://citelynq.com/app/api-keys](https://citelynq.com/app/api-keys) +2. Create a new API key +3. Copy the key (starts with `cl_`) + +### 2. Run the Application + +```bash +cd /repos/CiteFlow/samples/CiteLynq.WebSample +dotnet run +``` + +The application will start on `http://localhost:5100` + +### 3. Use the Application + +1. Open your browser to `http://localhost:5100` +2. Enter your API key +3. Enter a search query (e.g., "climate change") +4. Click "Search" + +## Project Structure + +``` +CiteLynq.WebSample/ +├── Program.cs # ASP.NET Core minimal API +├── CiteLynq.WebSample.csproj # Project file +├── Properties/ +│ └── launchSettings.json # Launch configuration +└── wwwroot/ + └── index.html # Single-page application +``` + +## How It Works + +### Backend (Program.cs) + +The backend provides proxy endpoints to securely call the CiteLynq API: + +1. **GET /api/search** - Proxies search requests + - Accepts: query parameters (q, sourceType, page, pageSize) + - Headers: X-API-Key + - Returns: Search results from CiteLynq API + +2. **GET /api/articles/{id}** - Proxies article detail requests + - Accepts: article ID as path parameter + - Headers: X-API-Key + - Returns: Article details from CiteLynq API + +**Why use a proxy?** +- Hides your API key from client-side code +- Prevents CORS issues +- Adds an abstraction layer for future enhancements +- Enables server-side caching if needed + +### Frontend (wwwroot/index.html) + +The frontend is a self-contained single-page application: + +- **API Key Storage**: Saves API key in localStorage +- **Search Interface**: Query input and source filter +- **Results Display**: Shows citations with metadata +- **Responsive Design**: Works on desktop and mobile + +## API Endpoints + +### Search Citations + +```http +GET /api/search?q=climate%20change&sourceType=ArXiv&page=1&pageSize=20 +X-API-Key: cl_your-api-key-here +``` + +**Response:** +```json +{ + "data": { + "results": [ + { + "id": "arxiv-2301.12345", + "sourceType": "ArXiv", + "title": "Climate Change Research", + "authors": ["Dr. Jane Smith"], + "abstract": "...", + "publishedDate": "2023-01-15", + "url": "https://arxiv.org/abs/2301.12345", + "citations": { + "apa": "Smith, J. (2023)...", + "mla": "Smith, Jane...", + "chicago": "..." + } + } + ], + "totalCount": 145 + } +} +``` + +### Get Article Details + +```http +GET /api/articles/arxiv-2301.12345 +X-API-Key: cl_your-api-key-here +``` + +## Configuration + +### Change API Base URL + +Edit `Program.cs` line 24 to point to a different API: + +```csharp +const string API_BASE_URL = "https://citelynq.com/api"; +// Or for local development: +// const string API_BASE_URL = "http://localhost:5000/api"; +``` + +### Change Port + +Edit `Properties/launchSettings.json`: + +```json +{ + "profiles": { + "http": { + "applicationUrl": "http://localhost:5100" + } + } +} +``` + +## Development + +### Prerequisites + +- .NET 9.0 SDK +- Visual Studio 2022 / VS Code / Rider + +### Build + +```bash +dotnet build +``` + +### Run + +```bash +dotnet run +``` + +### Publish + +```bash +dotnet publish -c Release -o ./publish +``` + +## Customization + +### Add More Endpoints + +Add new proxy endpoints in `Program.cs`: + +```csharp +app.MapGet("/api/sources", async (HttpContext context, IHttpClientFactory httpClientFactory) => +{ + var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault(); + var httpClient = httpClientFactory.CreateClient(); + var httpRequest = new HttpRequestMessage(HttpMethod.Get, $"{API_BASE_URL}/sources"); + httpRequest.Headers.Add("X-API-Key", apiKey); + var response = await httpClient.SendAsync(httpRequest); + return Results.Content(await response.Content.ReadAsStringAsync(), "application/json"); +}); +``` + +### Customize UI + +Edit `wwwroot/index.html`: + +```css +/* Change color scheme */ +body { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} +``` + +### Add Pagination + +Add pagination controls in the HTML: + +```javascript +function displayResults(data) { + // ... existing code ... + + if (data.totalPages > 1) { + html += ``; + } +} +``` + +## Security Notes + +⚠️ **Important:** + +1. **API Key Protection**: The proxy pattern keeps API keys server-side +2. **CORS**: Configured for development (allow all origins). Restrict in production. +3. **Input Validation**: Always validate user input +4. **HTTPS**: Use HTTPS in production environments + +## Troubleshooting + +### Port Already in Use + +Change the port in `Properties/launchSettings.json` + +### CORS Errors + +The application includes CORS middleware for development. For production, configure specific origins: + +```csharp +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.WithOrigins("https://yourdomain.com") + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); +``` + +### API Key Not Working + +1. Verify the key starts with `cl_` +2. Check the key is active in your account +3. Ensure the key has available credits + +## Learn More + +- **CiteLynq API Docs**: [https://citelynq.com/docs](https://citelynq.com/docs) +- **Get API Key**: [https://citelynq.com/app/api-keys](https://citelynq.com/app/api-keys) +- **ASP.NET Core**: [https://docs.microsoft.com/aspnet/core](https://docs.microsoft.com/aspnet/core) + +## License + +This sample is provided as-is for demonstration purposes. diff --git a/wwwroot/index.html b/wwwroot/index.html new file mode 100644 index 0000000..0d9499c --- /dev/null +++ b/wwwroot/index.html @@ -0,0 +1,794 @@ + + + + + + CiteLynq Web Sample + + + +
+
+

🔍 CiteLynq Web Sample

+

Search verified citations from authoritative sources

+
+ +
+
+ + + Get your API key from citelynq.com/app/api-keys +
+ +

🔎 Search Citations

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

📊 Search Results

+
+
+
+
+ + + +