Getting Started Smart PDF Viewer using UserToken with Azure Service
18 Nov 201824 minutes to read
This guide provides step-by-step instructions for integrating and using Syncfusion’s Smart PDF Viewer with User Token and Custom Azure AI service in your Blazor App.
Prerequisites
Before you begin, ensure you have:
Getting Started for User Token with Custom Azure AI service in Smart PDF Viewer
After completing this setup, you can:
Step 1: Create User Token Service
The UserTokenService
is responsible for generating secure tokens for users. These tokens can be used to authenticate requests to your Custom Azure AI Service.
Implementation Steps
- Create a new class file named
UserTokenService.cs
in your project - Add the following implementation:
// This class handles user token management including generation, tracking, and resetting.
public class UserTokenService
{
private readonly IJSRuntime _jsRuntime;
private const string TokenFilePath = "user_tokens.json"; // Path to the token storage file
private static readonly TimeZoneInfo IndianStandardTime = TimeZoneInfo.FindSystemTimeZoneById("India Standard Time");
// Constructor to initialize JavaScript runtime for browser interactions
public UserTokenService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
// Retrieves a unique fingerprint for the user using JavaScript
public async Task<string> GetUserFingerprintAsync()
{
return await _jsRuntime.InvokeAsync<string>("fingerPrint");
}
// Gets the remaining tokens for a user, resetting if needed
public async Task<int> GetRemainingTokensAsync(string userCode)
{
Dictionary<string, UserTokenInfo> tokens = await CheckAndResetTokensAsync(userCode);
return tokens.ContainsKey(userCode) ? tokens[userCode].RemainingTokens : 15000 ;
}
// Updates the token count for a user and saves it to the file
public async Task UpdateTokensAsync(string userCode, int tokens)
{
Dictionary<string, UserTokenInfo> tokenData = await ReadTokensFromFileAsync();
if (tokenData.ContainsKey(userCode))
{
tokenData[userCode].RemainingTokens = tokens;
}
else
{
tokenData[userCode] = new UserTokenInfo
{
UserId = userCode,
DateOfLogin = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, IndianStandardTime),
RemainingTokens = tokens
};
}
await WriteTokensToFileAsync(tokenData);
}
// Checks if 24 hours have passed since last login and resets tokens if needed
public async Task<Dictionary<string, UserTokenInfo>> CheckAndResetTokensAsync(string userCode)
{
Dictionary<string, UserTokenInfo> tokenData = await ReadTokensFromFileAsync();
if (tokenData.ContainsKey(userCode))
{
UserTokenInfo userTokenInfo = tokenData[userCode];
DateTime currentTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, IndianStandardTime);
TimeSpan timeDifference = currentTime - userTokenInfo.DateOfLogin;
if (timeDifference.TotalHours > 24)
{
userTokenInfo.RemainingTokens = 15000; // Reset tokens
userTokenInfo.DateOfLogin = currentTime; // Update login time
await WriteTokensToFileAsync(tokenData);
}
}
return tokenData;
}
// Reads token data from the JSON file
private async Task<Dictionary<string, UserTokenInfo>> ReadTokensFromFileAsync()
{
if (!File.Exists(TokenFilePath))
{
Dictionary<string, UserTokenInfo> initialData = new Dictionary<string, UserTokenInfo>();
await WriteTokensToFileAsync(initialData);
return initialData;
}
string json = await File.ReadAllTextAsync(TokenFilePath);
Dictionary<string, UserTokenInfo>? tokenData = JsonSerializer.Deserialize<Dictionary<string, UserTokenInfo>>(json);
return tokenData ?? new Dictionary<string, UserTokenInfo>();
}
// Reads token data from the JSON file
private async Task WriteTokensToFileAsync(Dictionary<string, UserTokenInfo> tokenData)
{
string json = JsonSerializer.Serialize(tokenData, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(TokenFilePath, json);
}
// Displays an alert banner in the browser with token reset info
public async Task ShowAlert(string userCode)
{
string message = await ReturnAlertMessage(userCode);
await _jsRuntime.InvokeVoidAsync("showBanner", message.ToString());
}
// Generates the alert message with token reset time and GitHub link
public async Task<string> ReturnAlertMessage(string userCode)
{
Dictionary<string, UserTokenInfo> tokenData = await ReadTokensFromFileAsync();
if (tokenData.ContainsKey(userCode))
{
UserTokenInfo userTokenInfo = tokenData[userCode];
string resetTime = userTokenInfo.DateOfLogin.AddHours(24).ToString("f");
string message = $"You have reached your token limit. Your tokens will reset on {resetTime}. Download our <a href=\"https://github.com/syncfusion/smart-ai-samples/tree/master/blazor\" target=\"_blank\">Syncfusion Smart AI Samples</a> from GitHub to explore this sample locally with your own API key.";
return message;
}
return "";
}
}
// Model class to store user token information
public class UserTokenInfo
{
public string UserId { get; set; }
public DateTime DateOfLogin { get; set; }
public int RemainingTokens { get; set; }
}
Step 2: Implement User Token API Controller
The UserTokensController
class serves as the API layer for interacting with the user token system.This controller is essential for enabling secure and dynamic token tracking for users interacting with your Custom Azure AI Service.
- Create a new class file named
UserTokensController.cs
in your project - Add the following implementation:
// Defines the route for the API controller and marks it as an API controller
[Route("api/[controller]")]
[ApiController]
public class UserTokensController : ControllerBase
{
private readonly IWebHostEnvironment _env; // Provides access to web hosting environment properties
private UserTokenService userToken { get; set; } // Service to manage user tokens
// Constructor to inject dependencies: hosting environment and token service
public UserTokensController(IWebHostEnvironment env, UserTokenService user)
{
_env = env;
userToken = user;
}
// API endpoint to get remaining tokens for a user
// Route: GET api/usertokens/get_remaining_tokens/{userId}
[HttpGet("get_remaining_tokens/{userId}")]
public async Task<IActionResult> GetRemainingTokens(string userId)
{
// Construct the full path to the token file
string filePath = Path.Combine(_env.ContentRootPath, "user_tokens.json");
// Get the current remaining tokens for the user
int remainingTokens = await userToken.GetRemainingTokensAsync(userId);
// Get the alert message if the user is near or at the token limit
string alertMessage = await userToken.ReturnAlertMessage(userId);
// If tokens are low (≤ 300), return the current token count and alert message
if (remainingTokens <= 300)
{
return Ok(new { remainingTokens, alertMessage });
}
// Otherwise, deduct 550 tokens and update the token count
await userToken.UpdateTokensAsync(userId, (int)(remainingTokens - 550));
// Return the updated token count and alert message
return Ok(new { remainingTokens, alertMessage });
}
}
Step 3: Create a Custom Azure AI Service
The Syncfusion Smart PDF Viewer are designed to work with different AI backends through the IChatInferenceService
interface. This section shows you how to create a custom implementation that connects the Smart PDF Viewer to the Azure AI service.
Understanding the Interface
The IChatInferenceService
interface is the bridge between Syncfusion Smart PDF Viewer and AI services:
- Create a new file named
AzureAIService.cs
- Add the following implementation:
// AzureAIService integrates with Azure OpenAI to generate chat completions and manage token usage.
public class AzureAIService : IChatInferenceService
{
private readonly UserTokenService _userTokenService;
private ChatParameters chatParameters_history = new ChatParameters();
private IChatClient _chatClient;
public AzureAIService(UserTokenService userTokenService, IChatClient client)
{
_userTokenService = userTokenService;
this._chatClient = client ?? throw new ArgumentNullException(nameof(client));
}
/// <summary>
/// Gets a text completion from the Azure OpenAI service.
/// </summary>
/// <param name="prompt">The user prompt to send to the AI service.</param>
/// <param name="returnAsJson">Indicates whether the response should be returned in JSON format. Defaults to <c>true</c></param>
/// <param name="appendPreviousResponse">Indicates whether to append previous responses to the conversation history. Defaults to <c>false</c></param>
/// <param name="systemRole">Specifies the systemRole that is sent to AI Clients. Defaults to <c>null</c></param>
/// <returns>The AI-generated completion as a string.</returns>
public async Task<string> GetCompletionAsync(string prompt, bool returnAsJson = true, bool appendPreviousResponse = false, string systemRole = null, int outputTokens = 2000)
{
string systemMessage = returnAsJson ? "You are a helpful assistant that only returns and replies with valid, iterable RFC8259 compliant JSON in your responses unless I ask for any other format. Do not provide introductory words such as 'Here is your result' or 'json', etc. in the response" : !string.IsNullOrEmpty(systemRole) ? systemRole : "You are a helpful assistant";
try
{
ChatParameters chatParameters = appendPreviousResponse ? chatParameters_history : new ChatParameters();
if (appendPreviousResponse)
{
if (chatParameters.Messages == null)
{
chatParameters.Messages = new List<ChatMessage>() {
new ChatMessage(ChatRole.System,systemMessage),
};
}
chatParameters.Messages.Add(new ChatMessage(ChatRole.User, prompt));
}
else
{
chatParameters.Messages = new List<ChatMessage>(2) {
new ChatMessage (ChatRole.System, systemMessage),
new ChatMessage(ChatRole.User,prompt),
};
}
chatParameters.MaxTokens = outputTokens;
string completion = await GenerateResponseAsync(chatParameters);
if (appendPreviousResponse)
{
chatParameters_history?.Messages?.Add(new ChatMessage(ChatRole.Assistant, completion));
}
return completion;
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex.Message}");
return "";
}
}
/// <summary>
/// Sends the chat parameters to the AI client and returns the response.
/// Also checks and updates token usage.
/// </summary>
/// <param name="options">Chat parameters including messages and settings.</param>
/// <returns>AI-generated response text.</returns
public async Task<string> GenerateResponseAsync(ChatParameters options)
{
string userCode = await _userTokenService.GetUserFingerprintAsync();
int remainingTokens = await _userTokenService.GetRemainingTokensAsync(userCode);
int inputTokens = options.Messages.Sum(message => message.Text.Length / 4);
if (remainingTokens <= inputTokens)
{
await _userTokenService.ShowAlert(userCode);
return null;
}
// Create a completion request with the provided parameters
ChatOptions completionRequest = new ChatOptions
{
Temperature = options.Temperature ?? 0.5f,
TopP = options.TopP ?? 1.0f,
MaxOutputTokens = options.MaxTokens ?? 2000,
FrequencyPenalty = options.FrequencyPenalty ?? 0.0f,
PresencePenalty = options.PresencePenalty ?? 0.0f,
StopSequences = options.StopSequences
};
try
{
ChatResponse completion = await _chatClient.GetResponseAsync(options.Messages, completionRequest);
await _userTokenService.UpdateTokensAsync(userCode, (int)(remainingTokens - completion.Usage.TotalTokenCount));
return completion.Text.ToString();
}
catch (Exception ex)
{
throw;
}
}
}
Step 4: Add a script file to your application and refer it to the head tag.
<head>
<script src="index.js" type="text/javascript"></script>
</head>
Step 5: Add the following code to render the JS component in the blazor to the newly added JS file.
// Generates a unique fingerprint for the user based on canvas rendering and SHA-256 hashing
async function fingerPrint() {
try {
// Create a hidden canvas element
var canvas = document.body.appendChild(document.createElement('canvas'));
canvas.width = 600;
canvas.height = 300;
canvas.style.display = "none";
// Drawing parameters
const ctx = canvas.getContext("2d");
const size = 24;
const diamondSize = 28;
const gap = 4;
const startX = 30;
const startY = 30;
const blue = "#1A3276";
const orange = "#F28C00";
// Pattern map for drawing squares and diamonds
const colorMap = [
["blue", "blue", "diamond"],
["blue", "orange", "blue"],
["blue", "blue", "blue"]
];
// Draw a square at specified coordinates
function drawSquare(x, y, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, size, size);
}
// Draw a diamond shape at specified coordinates
function drawDiamond(centerX, centerY, size, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(centerX, centerY - size / 2);
ctx.lineTo(centerX + size / 2, centerY);
ctx.lineTo(centerX, centerY + size / 2);
ctx.lineTo(centerX - size / 2, centerY);
ctx.closePath();
ctx.fill();
}
// Render the pattern on canvas
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 3; col++) {
const type = colorMap[row][col];
const x = startX + col * (size + gap);
const y = startY + row * (size + gap);
if (type === "blue") drawSquare(x, y, blue);
else if (type === "orange") drawSquare(x, y, orange);
else if (type === "diamond") drawDiamond(x + size / 2, y + size / 2, diamondSize, orange);
}
}
// Add text and shapes to increase fingerprint uniqueness
ctx.font = "20px Arial";
ctx.fillStyle = blue;
ctx.textBaseline = "middle";
ctx.fillText("Syncfusion", startX + 3 * (size + gap) + 20, startY + size + gap);
// Add overlapping circles with blend mode
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = "rgb(255,0,255)";
ctx.beginPath(); ctx.arc(50, 200, 50, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "rgb(0,255,255)";
ctx.beginPath(); ctx.arc(100, 200, 50, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "rgb(255,255,0)";
ctx.beginPath(); ctx.arc(75, 250, 50, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "rgb(255,0,255)";
ctx.beginPath();
ctx.arc(200, 200, 75, 0, Math.PI * 2, true);
ctx.arc(200, 200, 25, 0, Math.PI * 2, true);
ctx.fill("evenodd");
// Hash the canvas data to generate a unique ID
const sha256 = async function (str) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
};
const visitorID = sha256(canvas.toDataURL());
return visitorID;
}
catch (error) {
console.error(error);
return null;
}
}
// Displays a banner message on the page
function showBanner(messageText) {
// Check if the banner already exists
if (document.getElementById("custom-banner")) {
hideSpinner();
return;
}
// Create the banner container
let banner = document.createElement("div");
banner.id = "custom-banner";
banner.className = "e-banner";
// Banner content
let message = document.createElement("p");
message.innerHTML = messageText;
message.className = "banner-message";
// Create the close button
let closeButton = document.createElement("span");
closeButton.innerHTML = "×"; // HTML entity for '×' symbol
closeButton.className = "close-button";
closeButton.onclick = closeBanner;
// Append elements
banner.appendChild(message);
banner.appendChild(closeButton);
document.body.insertBefore(banner, document.body.firstChild);
hideSpinner();
}
// Fetches remaining tokens for a user from the backend API
async function getRemainingTokens(userId) {
try {
const baseElement = document.querySelector('base');
const baseUrl = baseElement ? baseElement.href : window.location.origin;
const response = await fetch(`${baseUrl}api/UserTokens/get_remaining_tokens/${userId}`);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error("Error fetching remaining tokens:", error);
}
return 0;
}
function closeBanner() {
let banner = document.getElementById("custom-banner");
if (banner) {
document.body.removeChild(banner);
}
}
function hideSpinner() {
var spinnerElement = document.querySelector('.e-spinner-pane.e-spin-show');
if (spinnerElement) {
spinnerElement.classList.remove('e-spin-show');
spinnerElement.classList.add('e-spin-hide');
}
}
Step 6: Configure the Blazor App
Configure your Blazor application to use the User Token with Azure AI service with Syncfusion Smart PDF Viewer. This involves registering necessary services and setting up the dependency injection container.
using Azure.AI.OpenAI;
using System.ClientModel;
using Microsoft.Extensions.AI;
using Syncfusion.Blazor.AI;
...
var builder = WebApplication.CreateBuilder(args);
....
builder.Services.AddSyncfusionBlazor();
// Define your Azure OpenAI credentials and model
string azureOpenAIKey = "Your API Key"; // Replace with your actual Azure OpenAI API key
string azureOpenAIEndpoint = "Your Endpoint"; // Replace with your Azure OpenAI endpoint URL
string azureOpenAIModel = "Your model name"; // Replace with your deployed model name
// Create an AzureOpenAIClient instance using the endpoint and API key
AzureOpenAIClient azureOpenAIClient = new AzureOpenAIClient(
new Uri(azureOpenAIEndpoint),
new ApiKeyCredential(azureOpenAIKey)
);
// Get a chat client from the AzureOpenAIClient and cast it to IChatClient
IChatClient azureOpenAIChatClient = azureOpenAIClient.GetChatClient(azureOpenAIModel).AsIChatClient();
builder.Services.AddChatClient(azureOpenAIChatClient);
builder.Services.AddScoped<UserTokenService>();
// Register AzureAIService as the implementation of IChatInferenceService
builder.Services.AddScoped<IChatInferenceService, AzureAIService>(sp =>
{
UserTokenService userTokenService = sp.GetRequiredService<UserTokenService>();
return new AzureAIService(userTokenService, azureOpenAIChatClient);
});
var app = builder.Build();
....
Here,
- apiKey: “Azure OpenAI API Key”;
- deploymentName: “Azure OpenAI deployment name”;
- endpoint: “Azure OpenAI deployment end point URL”;
For Azure OpenAI, first deploy an Azure OpenAI Service resource and model, then values for apiKey
, deploymentName
and endpoint
will all be provided to you.
NOTE