diff --git a/App/backend-api/Microsoft.GS.DPS.Host/API/KernelMemory/KernelMemory.cs b/App/backend-api/Microsoft.GS.DPS.Host/API/KernelMemory/KernelMemory.cs index 128750a7..fd95c81d 100644 --- a/App/backend-api/Microsoft.GS.DPS.Host/API/KernelMemory/KernelMemory.cs +++ b/App/backend-api/Microsoft.GS.DPS.Host/API/KernelMemory/KernelMemory.cs @@ -49,6 +49,12 @@ ILogger logger try { + if (file == null) + { + logger.LogWarning("[{RequestId}] No file provided in request", requestId); + return Results.BadRequest(new DocumentImportedResult() { DocumentId = string.Empty }); + } + var fileStream = file.OpenReadStream(); //Set Stream Position to 0 fileStream.Seek(0, SeekOrigin.Begin); @@ -457,8 +463,6 @@ ILogger logger //Creating While Loop with 10 mins timeout var timeout = DateTime.UtcNow.AddMinutes(10); - var completeFlag = false; - var status = await kmClient.GetDocumentStatusAsync(documentId); while (DateTime.UtcNow < timeout) @@ -474,7 +478,6 @@ ILogger logger if (status.RemainingSteps.Count == 0) { - completeFlag = true; break; } var totalSteps = status.Steps.Count; diff --git a/App/backend-api/Microsoft.GS.DPS.Host/API/UserInterface/UserInterface.cs b/App/backend-api/Microsoft.GS.DPS.Host/API/UserInterface/UserInterface.cs index d2311fff..e7b55643 100644 --- a/App/backend-api/Microsoft.GS.DPS.Host/API/UserInterface/UserInterface.cs +++ b/App/backend-api/Microsoft.GS.DPS.Host/API/UserInterface/UserInterface.cs @@ -11,7 +11,7 @@ namespace Microsoft.GS.DPSHost.API { public class UserInterface { - private static Dictionary thumbnails = new Dictionary(); + private static readonly Dictionary thumbnails = new Dictionary(); // Static method to register APIs public static void AddAPIs(WebApplication app) diff --git a/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs b/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs index 6a6bd40a..f42eeae4 100644 --- a/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs +++ b/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs @@ -50,10 +50,12 @@ public void TrackEvent(string eventName, Dictionary? properties { _telemetryClient.TrackEvent(eventName, properties, metrics); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to track event: {EventName}", eventName); } + #pragma warning restore CA1031 } /// @@ -73,10 +75,12 @@ public void TrackException(Exception exception, Dictionary? prop { _telemetryClient.TrackException(exception, properties, metrics); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to track exception"); } + #pragma warning restore CA1031 } /// @@ -96,12 +100,16 @@ public void TrackDependency(string dependencyName, string commandName, DateTimeO try { - _telemetryClient.TrackDependency(dependencyName, commandName, startTime, duration, success); + // Overload signature: TrackDependency(dependencyTypeName, target, dependencyName, data, startTime, duration, success) + // Map the wrapper's dependencyName -> telemetry dependencyName (and target), and commandName -> data. + _telemetryClient.TrackDependency("Other", dependencyName, dependencyName, commandName, startTime, duration, success ? "0" : "1", success); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to track dependency: {DependencyName}", dependencyName); } + #pragma warning restore CA1031 } /// @@ -143,10 +151,12 @@ public void SetActivityTag(string key, string value) { Activity.Current?.SetTag(key, value); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to set activity tag: {Key}", key); } + #pragma warning restore CA1031 } /// @@ -163,10 +173,12 @@ public void Flush() { _telemetryClient.Flush(); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to flush telemetry client"); } + #pragma warning restore CA1031 } } } diff --git a/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs b/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs index ee90e8b9..d230fbda 100644 --- a/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs +++ b/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs @@ -28,17 +28,16 @@ internal static class JsonSerializationOptionsCache public class ChatHost(MemoryWebClient kmClient, Kernel kernel, API.KernelMemory kernelMemory, ChatSessionRepository chatSessions) { - private MemoryWebClient _kmClient = kmClient; - private Kernel _kernel = kernel; - private API.KernelMemory _kernelMemory = kernelMemory; - private IChatCompletionService _chatCompletionService = kernel.GetRequiredService(); - private ChatSessionRepository _chatSessions = chatSessions; - private static string s_systemPrompt; - private static string s_assistancePrompt; - private static string s_additionalPrompt; + private readonly MemoryWebClient _kmClient = kmClient; + private readonly Kernel _kernel = kernel; + private readonly API.KernelMemory _kernelMemory = kernelMemory; + private readonly IChatCompletionService _chatCompletionService = kernel.GetRequiredService(); + private readonly ChatSessionRepository _chatSessions = chatSessions; + private static readonly string s_systemPrompt; + private static readonly string s_assistancePrompt; + private static readonly string s_additionalPrompt; - string sessionId = string.Empty; ChatHistory chatHistory = null; ChatSession chatSession = null; @@ -49,7 +48,7 @@ static ChatHost() var assemblyLocation = Assembly.GetExecutingAssembly().Location; var assemblyDirectory = System.IO.Path.GetDirectoryName(assemblyLocation); // binding assembly directory with file path (Prompts/Chat_SystemPrompt.txt) - var systemPromptFilePath = System.IO.Path.Combine(assemblyDirectory, "Prompts", "Chat_SystemPrompt.txt"); + var systemPromptFilePath = System.IO.Path.Join(assemblyDirectory, "Prompts", "Chat_SystemPrompt.txt"); ChatHost.s_systemPrompt = System.IO.File.ReadAllText(systemPromptFilePath); ChatHost.s_assistancePrompt = @" @@ -84,7 +83,7 @@ private async Task makeNewSession(string? chatSessionId) //Create a new ChatSession Entity for Saving into Azure Cosmos return new ChatSession() { - SessionId = this.sessionId, // New Session ID + SessionId = sessionId, // New Session ID StartTime = DateTime.UtcNow // Session Created Time }; @@ -201,16 +200,18 @@ public async Task Chat(ChatRequest chatRequest) Content = "Sorry, your request couldn't be processed as it may contain sensitive or restricted content. Please rephrase your query and try again." }; } + #pragma warning disable CA1031 // Top-level chat-completion safety net: convert any failure to a user-facing fallback response catch(Exception ex) { Console.WriteLine($"unexpected error: {ex.Message}"); returnedChatMessageContent = new ChatMessageContent { - Content = "An error occured while processing request, try again" + Content = "An error occurred while processing request, try again" }; } + #pragma warning restore CA1031 if (returnedChatMessageContent == null) { returnedChatMessageContent = new ChatMessageContent diff --git a/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs b/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs index 425947b7..bcb82e98 100644 --- a/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs +++ b/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs @@ -1,4 +1,4 @@ -using DnsClient.Internal; +using Microsoft.Extensions.Logging; using Microsoft.GS.DPS.Images; using Microsoft.GS.DPS.Model.KernelMemory; using Microsoft.GS.DPS.Storage.Document; @@ -23,11 +23,12 @@ namespace Microsoft.GS.DPS.API { public class KernelMemory { - private MemoryWebClient _kmClient; - private DocumentRepository _documentRepository; - private DataCacheManager _dataCache; - private TagUpdater _tagUpdator; - private static string keywordExtractorPrompt = ""; + private readonly MemoryWebClient _kmClient; + private readonly DocumentRepository _documentRepository; + private readonly DataCacheManager _dataCache; + private readonly TagUpdater _tagUpdator; + private readonly ILogger? _logger; + private static readonly string keywordExtractorPrompt = ""; static KernelMemory() { @@ -35,16 +36,17 @@ static KernelMemory() var assemblyLocation = Assembly.GetExecutingAssembly().Location; var assemblyDirectory = System.IO.Path.GetDirectoryName(assemblyLocation); // binding assembly directory with file path (Prompts/KeywordExtract_SystemPrompt.txt) - var systemPromptFilePath = System.IO.Path.Combine(assemblyDirectory, "Prompts", "KeywordExtract_SystemPrompt.txt"); + var systemPromptFilePath = System.IO.Path.Join(assemblyDirectory, "Prompts", "KeywordExtract_SystemPrompt.txt"); KernelMemory.keywordExtractorPrompt = System.IO.File.ReadAllText(systemPromptFilePath); } - public KernelMemory(MemoryWebClient kmClient, DocumentRepository documentRepository, DataCacheManager dataCache, TagUpdater tagUpdator) + public KernelMemory(MemoryWebClient kmClient, DocumentRepository documentRepository, DataCacheManager dataCache, TagUpdater tagUpdator, ILogger? logger = null) { _kmClient = kmClient; _documentRepository = documentRepository; _dataCache = dataCache; _tagUpdator = tagUpdator; + _logger = logger; } public async Task ImportDocument(Stream documentStream, @@ -139,7 +141,8 @@ private async Task getSummary(string documentId, string fileName) var summaryFile = await _kmClient.ExportFileAsync(documentId, summaryFileName); var summaryFileStream = await summaryFile.GetStreamAsync(); // Read Stream to string - return await new StreamReader(summaryFileStream).ReadToEndAsync(); + using var reader = new StreamReader(summaryFileStream); + return await reader.ReadToEndAsync(); } @@ -151,7 +154,11 @@ private async Task getSummary(string documentId, string fileName) var keywordFile = await _kmClient.ExportFileAsync(documentId, keywordFileName); var keywordFileStream = await keywordFile.GetStreamAsync(); // Read Stream to string - string? keywordContent = await new StreamReader(keywordFileStream).ReadToEndAsync(); + string? keywordContent; + using (var reader = new StreamReader(keywordFileStream)) + { + keywordContent = await reader.ReadToEndAsync(); + } if (string.IsNullOrEmpty(keywordContent)) { @@ -196,10 +203,18 @@ private async Task getSummary(string documentId, string fileName) return keywordDict; } - catch (Exception) + catch (JsonException ex) + { + _logger?.LogWarning(ex, "Failed to parse keyword JSON for document {DocumentId} ({FileName}); returning empty keyword set.", documentId, fileName); + return new Dictionary(); + } + #pragma warning disable CA1031 // LLM keyword-extraction output may be malformed; fall back to empty result rather than failing the import + catch (Exception ex) { + _logger?.LogWarning(ex, "Failed to extract keywords for document {DocumentId} ({FileName}); returning empty keyword set.", documentId, fileName); return new Dictionary(); } + #pragma warning restore CA1031 } } diff --git a/App/backend-api/Microsoft.GS.DPS/API/UserInterface/DataCacheManager.cs b/App/backend-api/Microsoft.GS.DPS/API/UserInterface/DataCacheManager.cs index 379298a3..ec2d52b1 100644 --- a/App/backend-api/Microsoft.GS.DPS/API/UserInterface/DataCacheManager.cs +++ b/App/backend-api/Microsoft.GS.DPS/API/UserInterface/DataCacheManager.cs @@ -53,12 +53,9 @@ public async Task RefreshCacheAsync() var values = keywordDict.Value.Split(',').Select(v => v.Trim()).ToArray(); - foreach (var value in values) + foreach (var value in values.Where(v => !consolidatedKeywords[keywordDict.Key].Contains(v))) { - if (!consolidatedKeywords[keywordDict.Key].Contains(value)) - { - consolidatedKeywords[keywordDict.Key].Add(value); - } + consolidatedKeywords[keywordDict.Key].Add(value); } consolidatedKeywords[keywordDict.Key] = consolidatedKeywords[keywordDict.Key].OrderBy(v => v).ToList(); diff --git a/App/backend-api/Microsoft.GS.DPS/Storage/AISearch/TagUpdater.cs b/App/backend-api/Microsoft.GS.DPS/Storage/AISearch/TagUpdater.cs index 8c7cb4b4..664d4a53 100644 --- a/App/backend-api/Microsoft.GS.DPS/Storage/AISearch/TagUpdater.cs +++ b/App/backend-api/Microsoft.GS.DPS/Storage/AISearch/TagUpdater.cs @@ -49,10 +49,12 @@ public async Task UpdateTags(string documentId, List updatingTags) var response = await _searchClient.MergeOrUploadDocumentsAsync(new[] { updateDocument }); Console.WriteLine($"Document with ID {document["id"]} updated successfully. - {response.GetRawResponse()}"); } + #pragma warning disable CA1031 // Tag update is best-effort; log and continue with the next document catch (Exception ex) { Console.Error.WriteLine($"Error updating document with ID {document["id"]}: {ex.Message}"); } + #pragma warning restore CA1031 } } } diff --git a/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs b/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs index d12b3092..5a38a725 100644 --- a/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs +++ b/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs @@ -50,7 +50,12 @@ public async Task> FindAllAsync(ISpecification spe { var collection = _database.GetCollection(typeof(TEntity).Name.ToLowerInvariant()); - GenericSpecification genericSpecification = specification as GenericSpecification; + if (specification is not GenericSpecification genericSpecification) + { + throw new ArgumentException( + $"Expected specification of type {nameof(GenericSpecification)}.", + nameof(specification)); + } if (genericSpecification.OrderBy == null) { diff --git a/App/backend-api/Microsoft.GS.DPS/Storage/Documents/DocumentRepository.cs b/App/backend-api/Microsoft.GS.DPS/Storage/Documents/DocumentRepository.cs index 413aa26c..e361f114 100644 --- a/App/backend-api/Microsoft.GS.DPS/Storage/Documents/DocumentRepository.cs +++ b/App/backend-api/Microsoft.GS.DPS/Storage/Documents/DocumentRepository.cs @@ -187,9 +187,9 @@ async public Task FindByDocumentIdsAsync(string[] documentIds, if (endDate.HasValue) { - endDate = endDate?.Date.AddHours(23).AddMinutes(59).AddSeconds(59); + var endOfDay = endDate.Value.Date.AddHours(23).AddMinutes(59).AddSeconds(59); var timeFilter = Builders.Filter.Gte(x => x.ImportedTime, startDate ?? DateTime.Now) & - Builders.Filter.Lte(x => x.ImportedTime, endDate.Value); + Builders.Filter.Lte(x => x.ImportedTime, endOfDay); filterDefinition &= timeFilter; } diff --git a/App/frontend-app/public/locales/en/translation.json b/App/frontend-app/public/locales/en/translation.json index 5eacacba..b8a4cb0b 100644 --- a/App/frontend-app/public/locales/en/translation.json +++ b/App/frontend-app/public/locales/en/translation.json @@ -28,6 +28,7 @@ "Search Results": "Search Results", "selected-documents": "Selected Documents", "fetching-answer": "Fetching answer, please wait...", + "error-fetching-answer": "Sorry, something went wrong while fetching the answer. Please try again.", "test-markdown": "# Dog Breed Comparison: Fluffy Golden Dog vs. Beagle\n\n## Fluffy Golden Dog\n- **Appearance**: Small, light-colored with a fluffy golden coat. Notable features include large brown eyes and prominent ears, one perked and one flopped.\n- **Temperament**: Curious and attentive, suggesting a friendly and engaging personality. The dog's posture indicates comfort and familiarity with its environment.\n- **Collar**: Wears a blue and purple collar with a red identification tag, indicating it is a pet with a caring owner.\n- **Environment**: Typically found in cozy indoor settings, reflecting a strong bond with human companions.\n\n## Beagle\n- **Appearance**: Medium-sized dog with a short, smooth coat that can come in various colors, including tri-color (black, white, and brown). Beagles have long ears and a distinctively expressive face.\n- **Temperament**: Known for being friendly, curious, and energetic. Beagles are often social and enjoy being part of family activities.\n- **Collar**: Commonly wear collars for identification, but styles vary widely.\n- **Environment**: Adaptable to both indoor and outdoor settings, Beagles thrive in active households where they can explore and play.\n\n## Summary\nWhile the fluffy golden dog is characterized by its small size, fluffy coat, and cozy indoor demeanor, the Beagle is a medium-sized, energetic breed known for its short coat and love for outdoor activities. Both breeds exhibit friendly and curious temperaments, making them great companions, but they differ in size, coat type, and typical living environments.", "new-topic": "New Topic", "input-placeholder": "Ask a question or request (ctrl + enter to submit)" diff --git a/App/frontend-app/src/AppContext.tsx b/App/frontend-app/src/AppContext.tsx index cf9526f3..19fa6919 100644 --- a/App/frontend-app/src/AppContext.tsx +++ b/App/frontend-app/src/AppContext.tsx @@ -1,12 +1,12 @@ import { ReactNode, createContext, useState } from 'react'; -import { ChatApiResponse } from './api/apiTypes/chatTypes'; +import { ChatUiResponse } from './api/apiTypes/chatTypes'; export interface IAppContext { - conversationAnswers: [prompt: string, response: ChatApiResponse, userTimestamp?: Date, answerTimestamp?: Date][]; + conversationAnswers: [prompt: string, response: ChatUiResponse, userTimestamp?: Date, answerTimestamp?: Date][]; setConversationAnswers: ( value: ( - prevState: [prompt: string, response: ChatApiResponse, userTimestamp?: Date, answerTimestamp?: Date][] - ) => [prompt: string, response: ChatApiResponse, userTimestamp?: Date, answerTimestamp?: Date][] + prevState: [prompt: string, response: ChatUiResponse, userTimestamp?: Date, answerTimestamp?: Date][] + ) => [prompt: string, response: ChatUiResponse, userTimestamp?: Date, answerTimestamp?: Date][] ) => void; query: string; setQuery: (value: string) => void; @@ -17,7 +17,7 @@ export interface IAppContext { export const AppContext = createContext({} as IAppContext); export const AppContextProvider = ({ children }: { children?: ReactNode }) => { - const [conversationAnswers, setConversationAnswers] = useState<[prompt: string, response: ChatApiResponse, userTimestamp?: Date, answerTimestamp?: Date][]>([]); + const [conversationAnswers, setConversationAnswers] = useState<[prompt: string, response: ChatUiResponse, userTimestamp?: Date, answerTimestamp?: Date][]>([]); const [query, setQuery] = useState(""); const [filters, setFilters] = useState<{ [key: string]: string[] }>({}); // Change this line diff --git a/App/frontend-app/src/api/apiTypes/chatTypes.ts b/App/frontend-app/src/api/apiTypes/chatTypes.ts index 78c54af9..49d7d6e1 100644 --- a/App/frontend-app/src/api/apiTypes/chatTypes.ts +++ b/App/frontend-app/src/api/apiTypes/chatTypes.ts @@ -31,6 +31,12 @@ export type ChatApiResponse = { keywords: string[]; } +export type ChatUiResponse = ChatApiResponse & { + requestId?: string; + pending?: boolean; + error?: boolean; +} + export type Reference = { title: string; parent_id: string; diff --git a/App/frontend-app/src/components/chat/chatRoom.tsx b/App/frontend-app/src/components/chat/chatRoom.tsx index 84bcc84b..09fc1c62 100644 --- a/App/frontend-app/src/components/chat/chatRoom.tsx +++ b/App/frontend-app/src/components/chat/chatRoom.tsx @@ -9,7 +9,6 @@ import { DialogSurface, DialogTitle, Tag, - makeStyles, } from "@fluentui/react-components"; import { DocDialog } from "../documentViewer/documentViewer"; import { Textarea } from "@fluentai/textarea"; @@ -19,7 +18,7 @@ import { ChatAdd24Regular } from "@fluentui/react-icons"; import styles from "./chatRoom.module.scss"; import { CopilotProvider, Suggestion } from "@fluentai/react-copilot"; //import { getDocument } from "../../api/documentsService"; -import { Completion, PostFeedback } from "../../api/chatService"; +import { Completion } from "../../api/chatService"; import { FeedbackForm } from "./FeedbackForm"; import { Document } from "../../api/apiTypes/documentResults"; import { AppContext } from "../../AppContext"; @@ -29,12 +28,6 @@ import { renderToStaticMarkup } from "react-dom/server"; import { marked } from 'marked'; const DefaultChatModel = "chat_4o"; -const useStyles = makeStyles({ - tooltipContent: { - maxWidth: "500px", - }, -}); - interface ChatRoomProps { searchResultDocuments: Document[]; disableOptionsPanel?: boolean; @@ -52,7 +45,7 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc const [disableSources, setDisableSources] = useState(false); const [model, setModel] = useState("chat_35"); const [source, setSource] = useState("rag"); - const [temperature, setTemperature] = useState(0.8); + const [temperature] = useState(0.8); const [maxTokens] = useState(750); const [selectedDocument, setSelectedDocument] = useState(chatWithDocument); const [button, setButton] = useState(""); @@ -73,6 +66,10 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc const [allChunkTexts] = useState([]); const { conversationAnswers, setConversationAnswers } = useContext(AppContext); + const hasPendingRequest = conversationAnswers.some( + ([, response]) => response && response.pending === true + ); + const inputDisabled = isLoading || hasPendingRequest; const [isSticky, setIsSticky] = useState(false); const optionsBottom = useRef(null); @@ -136,38 +133,17 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc currentSessionId = newSessionId; // Immediately use the new session ID in this function } - const markdownToHtmlString = (markdown: string) => { - return renderToStaticMarkup({markdown}); - }; - const markdown = `| Data Point | Value | Document Name | Page Number | -|----------------|-----------|-------------------|------------------| -| Households with accessibility needs | 23.1 million | Accessibility in Housing Report | Page 1 | -| Households with mobility-related disabilities | 19% of U.S. households | Accessibility in Housing Report | Page 1 | -| Households without entry-level bedroom or full bathroom planning to add features | 1% | Accessibility in Housing Report | Page 3 | -| Households planning to make homes more accessible | 5% | Accessibility in Housing Report | Page 3 | -| Households with someone using mobility devices | 13% | Accessibility in Housing Report | Page 1 | -| Households with serious difficulty hearing | 12% | Accessibility in Housing Report | Page 24 | -| Households with serious difficulty seeing | 12% | Accessibility in Housing Report | Page 24 | -| Households with difficulty walking or climbing stairs | 12% | Accessibility in Housing Report | Page 24 | -| Households with difficulty dressing or bathing | 12% | Accessibility in Housing Report | Page 24 | -| Households with difficulty doing errands alone | 12% | Accessibility in Housing Report | Page 24 | -| Households with full bathrooms on entry level | 58% | Accessibility in Housing Report | Page 11 | -| Households with bedrooms on entry level | 46% | Accessibility in Housing Report | Page 11 | -| Total single-family loans acquired by Fannie Mae in 2021 | $2.6 trillion | Annual Housing Report 2022 | Page 36 | -| Total single-family loans acquired by Freddie Mac in 2021 | $2.6 trillion | Annual Housing Report 2022 | Page 36 | -| Percentage of loans with LTV > 95% | 13.5% | Annual Housing Report 2022 | Page 44 | -| Percentage of loans with LTV <= 60% | 15.6% | Annual Housing Report 2022 | Page 44 | -| Total NPLs sold by Enterprises through December 2023 | 168,364 | FHFA Non-Performing Loan Sales Report | Page 2 | -| Average delinquency of NPLs sold | 2.8 years | FHFA Non-Performing Loan Sales Report | Page 2 | -| Average current mark-to-market LTV ratio of NPLs | 83% | FHFA Non-Performing Loan Sales Report | Page 2 |`; + const requestId = uuidv4(); setConversationAnswers((prevAnswers) => [ ...prevAnswers, [question, { answer: t('components.chat.fetching-answer'), suggestingQuestions: [], documentIds: [], - keywords: [] + keywords: [], + requestId, + pending: true, }], ]); @@ -212,18 +188,60 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc - // Update the conversation with the formatted answer setConversationAnswers((prevAnswers) => { const newAnswers = [...prevAnswers]; - newAnswers[newAnswers.length - 1] = [question, { ...response, answer: chatResp }, userTimestamp, answerTimestamp]; + const idx = newAnswers.findIndex( + ([, r]) => r && r.requestId === requestId + ); + if (idx === -1) { + return prevAnswers; + } + newAnswers[idx] = [ + question, + { ...response, answer: chatResp, requestId, pending: false }, + userTimestamp, + answerTimestamp, + ]; return newAnswers; }); + } else { + throw new Error( + `Empty response from chat API (received: ${ + response === undefined ? "undefined" : JSON.stringify(response) + })` + ); } } catch (error) { console.error("Error parsing response body:", error); + throw error; } } catch (error) { console.error("Error in makeApiRequest:", error); + const answerTimestamp = new Date(); + setConversationAnswers((prevAnswers) => { + const newAnswers = [...prevAnswers]; + const idx = newAnswers.findIndex( + ([, r]) => r && r.requestId === requestId + ); + if (idx === -1) { + return prevAnswers; + } + newAnswers[idx] = [ + question, + { + answer: t('components.chat.error-fetching-answer'), + suggestingQuestions: [], + documentIds: [], + keywords: [], + requestId, + pending: false, + error: true, + }, + userTimestamp, + answerTimestamp, + ]; + return newAnswers; + }); } finally { setIsLoading(false); setTimeout(() => { @@ -285,11 +303,6 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc // } // }; - const handleOpenFeedbackForm = (sources: Reference[]) => { - setReferencesForFeedbackForm(sources); - setIsFeedbackFormOpen(true); - }; - const handleDialogClose = () => { setIsDialogOpen(false); }; @@ -404,7 +417,7 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc className="mr-auto" progress={{ value: undefined }} // key={`${index}-chat`} - isLoading={index === conversationAnswers.length - 1 && isLoading} + isLoading={!!response?.pending} >
( { - if (!isLoading) { + if (!inputDisabled) { handleFollowUpQuestion(followUp); } }} @@ -564,9 +577,9 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc showCount aria-label="Chat input" placeholder={t('components.chat.input-placeholder')} - disabled={isLoading} + disabled={inputDisabled} onSubmit={handleSend} - disableSend = {textAreaValue.trim().length === 0 || isLoading} + disableSend = {textAreaValue.trim().length === 0 || inputDisabled} contentAfter={undefined} />
diff --git a/App/frontend-app/src/components/chat/modelSwitch.tsx b/App/frontend-app/src/components/chat/modelSwitch.tsx index b494b1e7..60bd00a6 100644 --- a/App/frontend-app/src/components/chat/modelSwitch.tsx +++ b/App/frontend-app/src/components/chat/modelSwitch.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { useTranslation } from "react-i18next"; interface ModelSwitchProps { @@ -6,8 +5,6 @@ interface ModelSwitchProps { } export function ModelSwitch({ onSwitchChange }: ModelSwitchProps) { - const GPT4O = "chat_4o"; - return (
{/*
diff --git a/App/frontend-app/src/components/documentViewer/documentViewer.tsx b/App/frontend-app/src/components/documentViewer/documentViewer.tsx index f656bba6..52d3b280 100644 --- a/App/frontend-app/src/components/documentViewer/documentViewer.tsx +++ b/App/frontend-app/src/components/documentViewer/documentViewer.tsx @@ -72,7 +72,7 @@ export function DocDialog( const [pageMetadata] = useState(null); const [iframeKey, setIframeKey] = useState(0); const [isExpanded, setIsExpanded] = useState(false); - const [clearedChatFlag, setClearChatFlag] = useState(clearChatFlag); + const [, setClearChatFlag] = useState(clearChatFlag); const [iframeSrc, setIframeSrc] = useState(undefined); // const [aiKnowledgeMetadata, setAIKnowledgeMetadata] = useState(null); diff --git a/App/frontend-app/src/components/searchBox/searchBox.tsx b/App/frontend-app/src/components/searchBox/searchBox.tsx index 391344b3..26d0189b 100644 --- a/App/frontend-app/src/components/searchBox/searchBox.tsx +++ b/App/frontend-app/src/components/searchBox/searchBox.tsx @@ -1,15 +1,10 @@ -import React, { forwardRef, useImperativeHandle, ChangeEvent, KeyboardEvent, useRef, useState } from "react"; +import React, { forwardRef, useImperativeHandle, ChangeEvent, KeyboardEvent, useState } from "react"; import { Input } from "@fluentui/react-input"; import { useTranslation } from "react-i18next"; -import { Button, InputOnChangeData, Tooltip, useId } from "@fluentui/react-components"; +import { InputOnChangeData, Tooltip, useId } from "@fluentui/react-components"; import { useDebouncedCallback } from "use-debounce"; -import { - Keyboard24Regular, - SearchVisual24Regular, - Search24Regular -} from "@fluentui/react-icons"; +import { Search24Regular } from "@fluentui/react-icons"; import "./searchInput.scss"; -import { UploadMultipleFiles } from "../../api/storageService"; export interface SearchBoxHandle { setValue(decodedQuery: string): unknown; @@ -26,45 +21,6 @@ interface SearchBoxProps { onKeyDown?: (event: KeyboardEvent) => void; // Include onKeyDown as a prop } -const UploadButton = () => { - const fileInputRef = useRef(null); - - const uploadDocuments = async () => { - if (fileInputRef.current?.files?.length) { - const files = Array.from(fileInputRef.current.files); - const formData = new FormData(); - - files.forEach((file, index) => { - formData.append(`file[${index}]`, file); - }); - - try { - const response = await UploadMultipleFiles(files); - if (!response) { - throw new Error("Error uploading files"); - } - - alert("Files uploaded successfully"); - } catch (error) { - console.error("Error:", error); - alert("Error uploading files"); - } - } - }; - - return ( - <> - {/* -
} size="large" diff --git a/App/frontend-app/src/components/searchResult/old.tsx b/App/frontend-app/src/components/searchResult/old.tsx index b9acbc99..9e58f9d3 100644 --- a/App/frontend-app/src/components/searchResult/old.tsx +++ b/App/frontend-app/src/components/searchResult/old.tsx @@ -5,7 +5,6 @@ import { getFileTypeIconProps } from "@fluentui/react-file-type-icons"; import { DocDialog } from "../documentViewer/documentViewer"; import { useEffect, useState } from "react"; import { Document, Tokens } from "../../api/apiTypes/documentResults"; -import { downloadFile } from "../../api/storageService"; interface SearchResultCardProps { document: Document; diff --git a/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx b/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx index 58a6965c..a4861ce4 100644 --- a/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx +++ b/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx @@ -7,7 +7,7 @@ import { Document } from "../../api/apiTypes/documentResults"; import { Button } from "@fluentui/react-components"; import { ChatAdd24Regular } from "@fluentui/react-icons"; import { Textarea } from "@fluentai/textarea"; -import { ChatApiResponse, ChatOptions, ChatRequest, History } from "../../api/apiTypes/chatTypes"; +import { ChatApiResponse, ChatRequest } from "../../api/apiTypes/chatTypes"; import { Completion } from "../../api/chatService"; import styles from "./sidecar.module.scss"; import { useTranslation } from "react-i18next"; @@ -30,13 +30,11 @@ export function SidecarCopilot({ const [isLoading, setIsLoading] = useState(false); const { conversationAnswers, setConversationAnswers } = useContext(AppContext); - const [model, setModel] = useState("chat_35"); - const [source, setSource] = useState("rag"); - const [button, setButton] = useState(""); - const [temperature] = useState(0.8); - const [maxTokens] = useState(750); + const [, setModel] = useState("chat_35"); + const [, setSource] = useState("rag"); + const [, setButton] = useState(""); const [disableSources, setDisableSources] = useState(false); - const [selectedDocument, setSelectedDocument] = useState([]); + const [, setSelectedDocument] = useState([]); useEffect(() => { setSelectedDocument(chatWithDocument); @@ -108,19 +106,6 @@ export function SidecarCopilot({ } }; - const history: History = conversationAnswers - .map(([prompt, response, userTimestamp, answerTimestamp]) => { - if (response) { - return [ - { role: "user", content: prompt, datetime: userTimestamp }, - { role: "assistant", content: response.answer, datetime: answerTimestamp }, - ]; - } else { - return []; - } - }) - .flat(); - const handleModelChange = (model: string) => { setModel(model); }; diff --git a/App/frontend-app/src/pages/home/home.tsx b/App/frontend-app/src/pages/home/home.tsx index 5b174d36..561b6a40 100644 --- a/App/frontend-app/src/pages/home/home.tsx +++ b/App/frontend-app/src/pages/home/home.tsx @@ -30,7 +30,7 @@ interface HomeProps { isSearchResultsPage?: boolean; } export function Home({ isSearchResultsPage }: HomeProps) { - const [filter, setFilter] = useState({ + const [, setFilter] = useState({ option: null, startDate: null, endDate: null, @@ -68,7 +68,6 @@ export function Home({ isSearchResultsPage }: HomeProps) { const [inOrderBy, setInOrderBy] = useState(""); const [searchResultDocuments, setSearchResultDocuments] = useState([]); - const [selectedDocument, setSelectedDocument] = useState([]); const { query, setQuery, filters: persistedFilters, setFilters: setPersistedFilters } = useContext(AppContext); const [selectedDateFilter] = useState(null); // const tempFilter = new SearchFacet @@ -247,14 +246,8 @@ export function Home({ isSearchResultsPage }: HomeProps) { setChatWidth(rightWidth); }; - const handleSortSelected = (sort: string) => { - setInOrderBy(sort); - }; - const headerMenuTabsRef = useRef(null); - const [widthClass, setWidthClass] = useState(window.innerWidth > 2000 ? "w-[165%]" : "w-[125%]"); - useEffect(() => { const filtersFromUrl = searchParams.get("filters"); @@ -300,18 +293,6 @@ export function Home({ isSearchResultsPage }: HomeProps) { } }, []); - useEffect(() => { - const handleResize = () => { - setWidthClass(window.innerWidth > 2000 ? "w-[165%]" : "w-[125%]"); - }; - - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); - //API call useEffect(() => { loadDataAsync(); @@ -458,8 +439,6 @@ export function Home({ isSearchResultsPage }: HomeProps) { }, [data]); function handleChatWithDocument(document: Document) { - setSelectedDocument([document]); - setShowCopilot(true); if (headerMenuTabsRef.current && showCopilot === false) { headerMenuTabsRef.current.scrollIntoView({ behavior: "smooth" }); diff --git a/Deployment/validate_bicep_params.py b/Deployment/validate_bicep_params.py index 467478ba..fcd0b206 100644 --- a/Deployment/validate_bicep_params.py +++ b/Deployment/validate_bicep_params.py @@ -111,6 +111,8 @@ def parse_parameters_env_vars(json_path: Path) -> dict[str, list[str]]: data = json.loads(sanitized) params = data.get("parameters", {}) except json.JSONDecodeError: + # Fall back to an empty params map if JSON parsing fails so the + # caller can still proceed with the regex-based scan over raw text. pass # Walk each top-level parameter and scan its entire serialized value