Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/CrestApps.Core.Docs/docs/changelog/2.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,14 @@ microphone, desk speakers):

## Change Logs

- **Added: uploaded knowledge files can now be downloaded again.** Files attached to an AI Profile or a
Chat Interaction are stored on the server, but there was previously no way to retrieve the original
file after uploading it. Each attached document now shows a download button next to its remove button
in both the MVC and Blazor hosts. Downloads are served by the existing
`ai/documents/{documentId}/download` endpoint, which now authorizes AI Profile documents in addition to
Chat Interaction and chat-session documents. Hosts authorize profile downloads through the same
resource-based `AIChatDocumentOperations.ManageDocuments` requirement; the sample hosts register a
`SampleAIProfileDocumentAuthorizationHandler` that grants it to administrators.
- **Fixed: totals from an uploaded spreadsheet could come back several times too large.** A worksheet
that prints its own subtotals — a per-group `... Total` line beneath each group, then a grand total
beneath those — used to import every one of those rows alongside the rows they summarize. `SUM` over
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ private static async Task<IResult> AuthorizeAsync(
new AIChatSessionDocumentAuthorizationContext(profile, session),
[AIChatDocumentOperations.ManageDocuments]);

return authorization.Succeeded ? null : CreateUnauthorizedResult(httpContext);
}
case AIReferenceTypes.Document.Profile:
{
var profile = await profileManager.FindByIdAsync(document.ReferenceId);

if (profile is null)
{
return Results.NotFound();
}

var authorization = await authorizationService.AuthorizeAsync(
httpContext.User,
profile,
[AIChatDocumentOperations.ManageDocuments]);

return authorization.Succeeded ? null : CreateUnauthorizedResult(httpContext);
}
default:
Expand Down
39 changes: 27 additions & 12 deletions src/Resources/CrestApps.AI.Resources/Assets/js/chat-interaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -2758,15 +2758,38 @@ window.chatInteractionDocumentManager = function () {

interactionDocuments.forEach(documentInfo => {
const row = window.document.createElement('div');
row.className = 'd-flex justify-content-between align-items-start gap-2 border rounded px-2 py-2 bg-white chat-document-row';
row.className = 'd-flex align-items-center gap-2 border rounded px-2 py-2 bg-white chat-document-row';
row.dataset.chatDocumentId = documentInfo.documentId;
row.dataset.chatDocumentName = documentInfo.fileName;
row.dataset.chatDocumentSize = documentInfo.fileSize;

const actions = window.document.createElement('div');
actions.className = 'd-flex gap-1';

const downloadBaseUrl = config.downloadDocumentBaseUrl || '/ai/documents/';
const downloadLink = createTextElement('a', 'btn btn-sm btn-outline-secondary', '');
downloadLink.title = 'Download';
downloadLink.href = downloadBaseUrl + encodeURIComponent(documentInfo.documentId) + '/download';
const downloadIcon = window.document.createElement('i');
downloadIcon.className = 'fa-solid fa-download';
downloadLink.appendChild(downloadIcon);

const removeButton = createTextElement('button', 'btn btn-sm btn-outline-danger remove-chat-document-btn', '');
removeButton.type = 'button';
removeButton.title = 'Remove';
removeButton.dataset.documentId = documentInfo.documentId;
const removeIcon = window.document.createElement('i');
removeIcon.className = 'fa-solid fa-trash';
removeButton.appendChild(removeIcon);
removeButton.addEventListener('click', () => removeDocument(documentInfo.documentId));

actions.appendChild(downloadLink);
actions.appendChild(removeButton);

const details = window.document.createElement('div');
details.className = 'me-2 min-w-0';
details.className = 'min-w-0';

const name = createTextElement('div', 'fw-semibold small', documentInfo.fileName || 'Document');
const name = createTextElement('div', 'fw-semibold small text-truncate', documentInfo.fileName || 'Document');
const icon = window.document.createElement('i');
icon.className = 'fa-solid fa-file-lines me-1';
name.prepend(icon);
Expand All @@ -2776,16 +2799,8 @@ window.chatInteractionDocumentManager = function () {
details.appendChild(name);
details.appendChild(size);

const removeButton = createTextElement('button', 'btn btn-sm btn-outline-danger remove-chat-document-btn', ' Remove');
removeButton.type = 'button';
removeButton.dataset.documentId = documentInfo.documentId;
const removeIcon = window.document.createElement('i');
removeIcon.className = 'fa-solid fa-trash';
removeButton.prepend(removeIcon);
removeButton.addEventListener('click', () => removeDocument(documentInfo.documentId));

row.appendChild(actions);
row.appendChild(details);
row.appendChild(removeButton);
documentsList.appendChild(row);
});
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -865,14 +865,19 @@ else if (_model != null)
@foreach (var doc in _model.AttachedDocuments)
{
<div class="attached-document-card">
<div class="d-flex gap-1 me-2">
<a class="btn btn-sm btn-outline-secondary" title="Download" href="@($"/ai/documents/{doc.DocumentId}/download")">
<i class="fa-solid fa-download"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" title="Remove" @onclick="() => RemoveDocument(doc.DocumentId)">
<i class="fa-solid fa-trash"></i>
</button>
</div>
<span class="attached-document-icon"><i class="fa-solid fa-file-lines"></i></span>
<div class="attached-document-meta flex-grow-1">
<div class="attached-document-name">@doc.FileName</div>
<span class="attached-document-size">@FormatFileSize(doc.FileSize)</span>
</div>
<button type="button" class="btn btn-sm btn-outline-danger" title="Remove" @onclick="() => RemoveDocument(doc.DocumentId)">
<i class="fa-solid fa-trash"></i>
</button>
</div>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,19 +355,27 @@ else
{
@foreach (var document in _model.Documents)
{
<div class="d-flex justify-content-between align-items-start gap-2 border rounded px-2 py-2 bg-white chat-document-row"
<div class="d-flex align-items-center gap-2 border rounded px-2 py-2 bg-white chat-document-row"
data-chat-document-id="@document.DocumentId"
data-chat-document-name="@document.FileName"
data-chat-document-size="@document.FileSize">
<div class="me-2 min-w-0">
<div class="fw-semibold small"><i class="fa-solid fa-file-lines me-1"></i>@document.FileName</div>
<div class="d-flex gap-1">
<a class="btn btn-sm btn-outline-secondary"
title="Download"
href="@($"/ai/documents/{document.DocumentId}/download")">
<i class="fa-solid fa-download"></i>
</a>
<button type="button"
class="btn btn-sm btn-outline-danger remove-chat-document-btn"
title="Remove"
data-document-id="@document.DocumentId">
<i class="fa-solid fa-trash"></i>
</button>
</div>
<div class="min-w-0">
<div class="fw-semibold small text-truncate"><i class="fa-solid fa-file-lines me-1"></i>@document.FileName</div>
<div class="text-muted small">@FormatFileSize(document.FileSize)</div>
</div>
<button type="button"
class="btn btn-sm btn-outline-danger remove-chat-document-btn"
data-document-id="@document.DocumentId">
<i class="fa-solid fa-trash"></i> Remove
</button>
</div>
}
}
Expand Down Expand Up @@ -775,6 +783,7 @@ else
}).ToArray(),
uploadDocumentUrl = "/ai/chat-interactions/upload-document",
removeDocumentUrl = "/ai/chat-interactions/remove-document",
downloadDocumentBaseUrl = "/ai/documents/",
});

_chatInteractionConfigJson = JsonSerializer.Serialize(new
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public static IServiceCollection AddBlazorSampleHostServices(this IServiceCollec
.AddScoped<ICatalogEntryHandler<AIMemoryEntry>, AIMemoryEntryHandler>()
.AddScoped<IAuthorizationHandler, SampleChatInteractionDocumentAuthorizationHandler>()
.AddScoped<IAuthorizationHandler, SampleAIChatSessionDocumentAuthorizationHandler>()
.AddScoped<IAuthorizationHandler, SampleAIProfileDocumentAuthorizationHandler>()
.AddScoped<IAIChatDocumentEventHandler, SampleAIChatDocumentEventHandler>()
.AddScoped<ICatalogEntryHandler<Article>, ArticleHandler>()
.AddScoped<ICopilotCredentialStore, JsonFileCopilotCredentialStore>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -743,14 +743,19 @@
@foreach (var doc in Model.AttachedDocuments)
{
<div class="attached-document-card existing-doc-row" data-doc-id="@doc.DocumentId">
<div class="d-flex gap-1 me-2">
<a class="btn btn-sm btn-outline-secondary" title="Download" href="@Url.Content($"~/ai/documents/{doc.DocumentId}/download")">
<i class="fa-solid fa-download"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger remove-existing-doc-btn" title="Remove">
<i class="fa-solid fa-trash"></i>
</button>
</div>
<span class="attached-document-icon"><i class="fa-solid fa-file-lines"></i></span>
<div class="attached-document-meta flex-grow-1">
<div class="attached-document-name">@doc.FileName</div>
<span class="attached-document-size">@(doc.FileSize < 1024 ? doc.FileSize + " B" : doc.FileSize < 1048576 ? (doc.FileSize / 1024.0).ToString("F1") + " KB" : (doc.FileSize / 1048576.0).ToString("F1") + " MB")</span>
</div>
<button type="button" class="btn btn-sm btn-outline-danger remove-existing-doc-btn" title="Remove">
<i class="fa-solid fa-trash"></i>
</button>
</div>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,7 @@
var interactionDocuments = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Documents));
var uploadDocumentUrl = '@Url.Content("~/ai/chat-interactions/upload-document")';
var removeDocumentUrl = '@Url.Content("~/ai/chat-interactions/remove-document")';
var downloadDocumentBaseUrl = '@Url.Content("~/ai/documents/")';
var audioChunkIntervalMs = 250;
var speechToTextEnabled = @Model.SpeechToTextEnabled.ToString().ToLowerInvariant();
var conversationModeEnabled = @Model.ConversationModeEnabled.ToString().ToLowerInvariant();
Expand Down Expand Up @@ -1218,15 +1219,21 @@

interactionDocuments.forEach(function (doc) {
var row = document.createElement('div');
row.className = 'd-flex justify-content-between align-items-start border rounded px-2 py-2 bg-white';
row.className = 'd-flex align-items-center border rounded px-2 py-2 bg-white';
var downloadUrl = downloadDocumentBaseUrl + encodeURIComponent(doc.documentId) + '/download';
row.innerHTML =
'<div class="me-2">' +
'<div class="fw-semibold small"><i class="fa-solid fa-file-lines me-1"></i>' + DOMPurify.sanitize(doc.fileName || 'Document') + '</div>' +
'<div class="text-muted small">' + DOMPurify.sanitize(formatFileSize(doc.fileSize)) + '</div>' +
'<div class="d-flex gap-1 me-2">' +
'<a class="btn btn-sm btn-outline-secondary" title="Download" href="' + DOMPurify.sanitize(downloadUrl) + '">' +
'<i class="fa-solid fa-download"></i>' +
'</a>' +
'<button type="button" class="btn btn-sm btn-outline-danger remove-chat-document-btn" title="Remove" data-document-id="' + DOMPurify.sanitize(doc.documentId) + '">' +
'<i class="fa-solid fa-trash"></i>' +
'</button>' +
'</div>' +
'<button type="button" class="btn btn-sm btn-outline-danger remove-chat-document-btn" data-document-id="' + DOMPurify.sanitize(doc.documentId) + '">' +
'<i class="fa-solid fa-trash"></i> Remove' +
'</button>';
'<div class="min-w-0">' +
'<div class="fw-semibold small text-truncate"><i class="fa-solid fa-file-lines me-1"></i>' + DOMPurify.sanitize(doc.fileName || 'Document') + '</div>' +
'<div class="text-muted small">' + DOMPurify.sanitize(formatFileSize(doc.fileSize)) + '</div>' +
'</div>';

container.appendChild(row);
});
Expand Down
Loading
Loading