From cec7a768d6550ed1e095ffe9054e313f95907bc6 Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Thu, 12 Mar 2026 23:58:20 +0500 Subject: [PATCH 1/4] Add get_formatted_summary MCP tool for interview session export --- .../InterviewSessionRepository.cs | 82 +++++++++++++++++++ .../InterviewSessionTool.cs | 20 +++++ 2 files changed, 102 insertions(+) diff --git a/src/InterviewCoach.Mcp.InterviewData/InterviewSessionRepository.cs b/src/InterviewCoach.Mcp.InterviewData/InterviewSessionRepository.cs index ba53137..9c8f7b0 100644 --- a/src/InterviewCoach.Mcp.InterviewData/InterviewSessionRepository.cs +++ b/src/InterviewCoach.Mcp.InterviewData/InterviewSessionRepository.cs @@ -11,6 +11,7 @@ public interface IInterviewSessionRepository Task GetInterviewSessionAsync(Guid id); Task UpdateInterviewSessionAsync(InterviewSession interviewSession); Task CompleteInterviewSessionAsync(Guid id); + Task GetFormattedSummaryAsync(Guid id); } public class InterviewSessionRepository(InterviewDataDbContext db) : IInterviewSessionRepository @@ -91,4 +92,85 @@ await db.InterviewSessions.Where(p => p.Id == id) return record; } + + public async Task GetFormattedSummaryAsync(Guid id) + { + var record = await db.InterviewSessions.SingleOrDefaultAsync(p => p.Id == id); + if (record is null) + { + return default; + } + + var sb = new StringBuilder(); + sb.AppendLine("# Interview Session Summary"); + sb.AppendLine(); + sb.AppendLine($"**Session ID:** {record.Id}"); + sb.AppendLine($"**Date:** {record.CreatedAt:yyyy-MM-dd HH:mm} UTC"); + sb.AppendLine($"**Status:** {(record.IsCompleted ? "Completed" : "In Progress")}"); + sb.AppendLine(); + + if (!string.IsNullOrWhiteSpace(record.ResumeLink)) + { + sb.AppendLine("---"); + sb.AppendLine(); + sb.AppendLine("## Resume"); + sb.AppendLine(); + sb.AppendLine($"**Source:** {record.ResumeLink}"); + if (!string.IsNullOrWhiteSpace(record.ResumeText)) + { + sb.AppendLine(); + sb.AppendLine(record.ResumeText.Trim()); + } + sb.AppendLine(); + } + else if (record.ProceedWithoutResume) + { + sb.AppendLine("---"); + sb.AppendLine(); + sb.AppendLine("## Resume"); + sb.AppendLine(); + sb.AppendLine("*Proceeded without resume.*"); + sb.AppendLine(); + } + + if (!string.IsNullOrWhiteSpace(record.JobDescriptionLink)) + { + sb.AppendLine("---"); + sb.AppendLine(); + sb.AppendLine("## Job Description"); + sb.AppendLine(); + sb.AppendLine($"**Source:** {record.JobDescriptionLink}"); + if (!string.IsNullOrWhiteSpace(record.JobDescriptionText)) + { + sb.AppendLine(); + sb.AppendLine(record.JobDescriptionText.Trim()); + } + sb.AppendLine(); + } + else if (record.ProceedWithoutJobDescription) + { + sb.AppendLine("---"); + sb.AppendLine(); + sb.AppendLine("## Job Description"); + sb.AppendLine(); + sb.AppendLine("*Proceeded without job description.*"); + sb.AppendLine(); + } + + if (!string.IsNullOrWhiteSpace(record.Transcript)) + { + sb.AppendLine("---"); + sb.AppendLine(); + sb.AppendLine("## Interview Transcript"); + sb.AppendLine(); + sb.AppendLine(record.Transcript.Trim()); + sb.AppendLine(); + } + + sb.AppendLine("---"); + sb.AppendLine(); + sb.AppendLine($"*Generated on {DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm} UTC*"); + + return sb.ToString(); + } } \ No newline at end of file diff --git a/src/InterviewCoach.Mcp.InterviewData/InterviewSessionTool.cs b/src/InterviewCoach.Mcp.InterviewData/InterviewSessionTool.cs index 7243910..204dff2 100644 --- a/src/InterviewCoach.Mcp.InterviewData/InterviewSessionTool.cs +++ b/src/InterviewCoach.Mcp.InterviewData/InterviewSessionTool.cs @@ -11,6 +11,7 @@ public interface IInterviewSessionTool Task GetInterviewSessionAsync(Guid id); Task UpdateInterviewSessionAsync(InterviewSession record); Task CompleteInterviewSessionAsync(Guid id); + Task GetFormattedSummaryAsync(Guid id); } [McpServerToolType] @@ -96,4 +97,23 @@ public async Task> GetAllInterviewSessionsAsync() return completed; } + + [McpServerTool(Name = "get_formatted_summary", Title = "Get a formatted interview summary")] + [Description("Gets a formatted Markdown summary of a completed interview session, including session metadata, resume, job description, and full transcript.")] + public async Task GetFormattedSummaryAsync( + [Description("The ID of the interview session")] Guid id + ) + { + var summary = await repository.GetFormattedSummaryAsync(id); + if (summary is null) + { + logger.LogWarning("Interview session with ID '{id}' not found.", id); + + return default; + } + + logger.LogInformation("Generated formatted summary for interview session '{id}'", id); + + return summary; + } } From 8a987495924e7722ac66d2b846e7c2e03540ef5d Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 00:07:52 +0500 Subject: [PATCH 2/4] Add markdown export endpoint for interview session summary --- src/InterviewCoach.Agent/Program.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/InterviewCoach.Agent/Program.cs b/src/InterviewCoach.Agent/Program.cs index 5a4ade2..3b26b80 100644 --- a/src/InterviewCoach.Agent/Program.cs +++ b/src/InterviewCoach.Agent/Program.cs @@ -1,4 +1,6 @@ using System.Collections.Concurrent; +using System.Text; +using System.Text.Json; using InterviewCoach.Agent; @@ -173,4 +175,23 @@ return Results.File(entry.Content, entry.ContentType, entry.FileName); }); +// --- Export Endpoints --- +app.MapGet("/export/{sessionId}/markdown", async (string sessionId) => +{ + var mcpClient = app.Services.GetRequiredKeyedService("mcp-interview-data"); + var result = await mcpClient.CallToolAsync("get_formatted_summary", new Dictionary + { + ["id"] = sessionId + }); + + var textBlock = result.Content.OfType().FirstOrDefault(); + if (textBlock is null) + return Results.NotFound("Interview session not found."); + + var markdown = textBlock.Text ?? string.Empty; + var bytes = Encoding.UTF8.GetBytes(markdown); + + return Results.File(bytes, "text/markdown", $"interview-summary-{sessionId}.md"); +}); + await app.RunAsync(); From 6c037994979f7e0d24acec1a0498a6ba3e8f428c Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 00:14:51 +0500 Subject: [PATCH 3/4] Add PDF export endpoint using QuestPDF --- .../InterviewCoach.Agent.csproj | 1 + src/InterviewCoach.Agent/Program.cs | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/InterviewCoach.Agent/InterviewCoach.Agent.csproj b/src/InterviewCoach.Agent/InterviewCoach.Agent.csproj index a76d9c1..c784433 100644 --- a/src/InterviewCoach.Agent/InterviewCoach.Agent.csproj +++ b/src/InterviewCoach.Agent/InterviewCoach.Agent.csproj @@ -21,6 +21,7 @@ + diff --git a/src/InterviewCoach.Agent/Program.cs b/src/InterviewCoach.Agent/Program.cs index 3b26b80..fe4ba7a 100644 --- a/src/InterviewCoach.Agent/Program.cs +++ b/src/InterviewCoach.Agent/Program.cs @@ -4,6 +4,10 @@ using InterviewCoach.Agent; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + using Microsoft.Agents.AI; using Microsoft.Agents.AI.DevUI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; @@ -194,4 +198,79 @@ return Results.File(bytes, "text/markdown", $"interview-summary-{sessionId}.md"); }); +app.MapGet("/export/{sessionId}/pdf", async (string sessionId) => +{ + var mcpClient = app.Services.GetRequiredKeyedService("mcp-interview-data"); + var result = await mcpClient.CallToolAsync("get_formatted_summary", new Dictionary + { + ["id"] = sessionId + }); + + var textBlock = result.Content.OfType().FirstOrDefault(); + if (textBlock is null) + return Results.NotFound("Interview session not found."); + + var markdown = textBlock.Text ?? string.Empty; + var lines = markdown.Split('\n'); + + QuestPDF.Settings.License = LicenseType.Community; + + var pdfBytes = Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(40); + page.DefaultTextStyle(x => x.FontSize(11)); + + page.Content().Column(column => + { + foreach (var line in lines) + { + var trimmed = line.TrimEnd('\r'); + + if (trimmed.StartsWith("# ")) + { + column.Item().PaddingBottom(10).Text(trimmed[2..]) + .FontSize(22).Bold(); + } + else if (trimmed.StartsWith("## ")) + { + column.Item().PaddingTop(15).PaddingBottom(5).Text(trimmed[3..]) + .FontSize(16).Bold(); + } + else if (trimmed.StartsWith("**") && trimmed.EndsWith("**")) + { + column.Item().PaddingBottom(3).Text(trimmed.Trim('*')) + .Bold(); + } + else if (trimmed == "---") + { + column.Item().PaddingVertical(8).LineHorizontal(0.5f).LineColor(Colors.Grey.Medium); + } + else if (trimmed.StartsWith("*") && trimmed.EndsWith("*")) + { + column.Item().PaddingBottom(3).Text(trimmed.Trim('*')) + .Italic().FontColor(Colors.Grey.Darken1); + } + else if (!string.IsNullOrWhiteSpace(trimmed)) + { + column.Item().PaddingBottom(3).Text(trimmed); + } + } + }); + + page.Footer().AlignCenter().Text(text => + { + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" of "); + text.TotalPages(); + }); + }); + }).GeneratePdf(); + + return Results.File(pdfBytes, "application/pdf", $"interview-summary-{sessionId}.pdf"); +}); + await app.RunAsync(); From 004fc8eb74895f2d5517267cebf46c55a1602a27 Mon Sep 17 00:00:00 2001 From: KhawarHabibKhan Date: Fri, 13 Mar 2026 00:22:50 +0500 Subject: [PATCH 4/4] Add Download Summary buttons to Chat UI --- .../Components/Pages/Chat/Chat.razor | 65 +++++++++++++++++++ .../Components/Pages/Chat/Chat.razor.js | 12 ++++ 2 files changed, 77 insertions(+) create mode 100644 src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor.js diff --git a/src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor b/src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor index 6977526..bb14334 100644 --- a/src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor +++ b/src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor @@ -4,6 +4,8 @@ @inject IChatClient ChatClient @inject ILogger Logger @inject FileUploadService FileUploadService +@inject IHttpClientFactory HttpClientFactory +@inject IJSRuntime JS @implements IDisposable Chat @@ -17,6 +19,25 @@ + @if (isInterviewComplete) + { +
+ Download your interview summary: + + +
+ } +
@@ -36,6 +57,15 @@ private CancellationTokenSource? currentResponseCancellation; private ChatMessage? currentResponseMessage; private ChatInput? chatInput; + private bool isInterviewComplete; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/Chat.razor.js"); + } + } protected override void OnInitialized() { @@ -92,6 +122,20 @@ currentResponseCancellation.Token); messages.Add(currentResponseMessage!); statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + + var assistantText = currentResponseMessage?.Text; + if (!isInterviewComplete && !string.IsNullOrEmpty(assistantText)) + { + var lowerText = assistantText.ToLower(); + if (lowerText.Contains("interview is complete") || lowerText.Contains("interview session complete") + || lowerText.Contains("completed your interview") || lowerText.Contains("session has been completed") + || lowerText.Contains("interview summary")) + { + isInterviewComplete = true; + StateHasChanged(); + } + } + currentResponseMessage = null; } @@ -144,6 +188,7 @@ ChatMessage responseMessage, CancellationToken cancellationToken) AddSessionSystemMessages(); chatOptions.ConversationId = sessionId; statefulMessageCount = 0; + isInterviewComplete = false; await chatInput!.FocusAsync(); } @@ -176,6 +221,26 @@ ChatMessage responseMessage, CancellationToken cancellationToken) return outboundMessages; } + private async Task DownloadExportAsync(string format) + { + var client = HttpClientFactory.CreateClient("agent"); + var url = $"export/{sessionId}/{format}"; + var response = await client.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + Logger.LogWarning("Export failed with status {StatusCode}", response.StatusCode); + return; + } + + var bytes = await response.Content.ReadAsByteArrayAsync(); + var contentType = format == "pdf" ? "application/pdf" : "text/markdown"; + var fileName = $"interview-summary-{sessionId}.{(format == "pdf" ? "pdf" : "md")}"; + + using var streamRef = new DotNetStreamReference(new MemoryStream(bytes)); + await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef); + } + public void Dispose() => currentResponseCancellation?.Cancel(); } diff --git a/src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor.js b/src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor.js new file mode 100644 index 0000000..401d3d8 --- /dev/null +++ b/src/InterviewCoach.WebUI/Components/Pages/Chat/Chat.razor.js @@ -0,0 +1,12 @@ +window.downloadFileFromStream = async (fileName, dotNetStreamRef) => { + const arrayBuffer = await dotNetStreamRef.arrayBuffer(); + const blob = new Blob([arrayBuffer]); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +};