From bf9e0f80ff25c535463bef97f29438142721314d Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Tue, 30 Dec 2025 14:52:46 +0500 Subject: [PATCH 1/8] 78564: Pass Page Context to Concierge from Side Panel --- .../AddCommentOrAttachmentParams.ts | 1 + src/libs/PageHTMLCapture/index.native.ts | 11 ++ src/libs/PageHTMLCapture/index.ts | 130 ++++++++++++++++++ src/libs/actions/Report.ts | 24 +++- .../ReportActionCompose.tsx | 6 +- src/pages/home/report/ReportFooter.tsx | 5 +- 6 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 src/libs/PageHTMLCapture/index.native.ts create mode 100644 src/libs/PageHTMLCapture/index.ts diff --git a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts index 8815e5c85eb6..6f87172def2d 100644 --- a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts +++ b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts @@ -10,6 +10,7 @@ type AddCommentOrAttachmentParams = { clientCreatedTime?: string; isOldDotConciergeChat?: boolean; idempotencyKey?: string; + pageHTML?: string; }; export default AddCommentOrAttachmentParams; diff --git a/src/libs/PageHTMLCapture/index.native.ts b/src/libs/PageHTMLCapture/index.native.ts new file mode 100644 index 000000000000..2242e295f131 --- /dev/null +++ b/src/libs/PageHTMLCapture/index.native.ts @@ -0,0 +1,11 @@ +/** + * Captures simplified HTML content from the main app window. + * On native platforms, this is not applicable, so we return an empty string. + * @returns Empty string on native platforms + */ +function capturePageHTML(): string { + return ''; +} + +export default capturePageHTML; + diff --git a/src/libs/PageHTMLCapture/index.ts b/src/libs/PageHTMLCapture/index.ts new file mode 100644 index 000000000000..c3abe46c5d9d --- /dev/null +++ b/src/libs/PageHTMLCapture/index.ts @@ -0,0 +1,130 @@ +import Navigation from '@libs/Navigation/Navigation'; + +/** + * Captures simplified HTML content from the main app window. + * Extracts semantic HTML elements, visible text, and useful attributes. + * @returns Simplified HTML string wrapped with page URL + */ +function capturePageHTML(): string { + try { + // Get the current page URL + const currentPath = Navigation.getActiveRoute(); + const pageURL = currentPath || '/'; + + // Get the main content container (excluding the side panel) + const mainContent = document.querySelector('#root') as HTMLElement; + if (!mainContent) { + return ''; + } + + // Extract simplified HTML + const simplifiedHTML = extractSimplifiedHTML(mainContent); + + // Wrap with page URL + return `\n${simplifiedHTML}\n`; + } catch (error) { + console.error('[PageHTMLCapture] Error capturing page HTML:', error); + return ''; + } +} + +/** + * Recursively extracts simplified HTML from an element. + * Includes semantic elements, visible text, and useful attributes. + */ +function extractSimplifiedHTML(element: HTMLElement): string { + const result: string[] = []; + + // Skip side panel and modal content + if ( + element.classList.contains('side-panel') || + element.getAttribute('data-testid') === 'side-panel' || + element.getAttribute('role') === 'dialog' || + element.classList.contains('modal') + ) { + return ''; + } + + // Process child nodes + const childNodes = Array.from(element.childNodes); + for (const node of childNodes) { + if (node.nodeType === Node.TEXT_NODE) { + // Include visible text content + const text = node.textContent?.trim(); + if (text) { + result.push(text); + } + } else if (node.nodeType === Node.ELEMENT_NODE) { + const childElement = node as HTMLElement; + + // Skip hidden elements + const style = window.getComputedStyle(childElement); + if (style.display === 'none' || style.visibility === 'hidden') { + continue; + } + + const tagName = childElement.tagName.toLowerCase(); + + // Check if this is a semantic element we want to capture + const semanticElements = ['button', 'a', 'input', 'textarea', 'select', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'nav', 'header', 'footer', 'article', 'section', 'aside', 'main', 'form', 'label', 'img']; + + if (semanticElements.includes(tagName)) { + // Build simplified element representation + const attrs: string[] = []; + + // Include useful attributes + const ariaLabel = childElement.getAttribute('aria-label'); + if (ariaLabel) { + attrs.push(`aria-label="${ariaLabel}"`); + } + + const href = childElement.getAttribute('href'); + if (href && tagName === 'a') { + attrs.push(`href="${href}"`); + } + + const alt = childElement.getAttribute('alt'); + if (alt && tagName === 'img') { + attrs.push(`alt="${alt}"`); + } + + const placeholder = childElement.getAttribute('placeholder'); + if (placeholder && (tagName === 'input' || tagName === 'textarea')) { + attrs.push(`placeholder="${placeholder}"`); + } + + const type = childElement.getAttribute('type'); + if (type && tagName === 'input') { + attrs.push(`type="${type}"`); + } + + const value = (childElement as HTMLInputElement).value; + if (value && (tagName === 'input' || tagName === 'textarea')) { + attrs.push(`value="${value}"`); + } + + // Get inner content + const innerContent = extractSimplifiedHTML(childElement); + + // Build element string + const attrString = attrs.length > 0 ? ` ${attrs.join(' ')}` : ''; + if (innerContent) { + result.push(`<${tagName}${attrString}>${innerContent}`); + } else { + result.push(`<${tagName}${attrString} />`); + } + } else { + // For non-semantic elements, just extract their content + const innerContent = extractSimplifiedHTML(childElement); + if (innerContent) { + result.push(innerContent); + } + } + } + } + + return result.join(' '); +} + +export default capturePageHTML; + diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 82eecae2bf9d..07da4585410b 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -83,6 +83,7 @@ import NetworkConnection from '@libs/NetworkConnection'; import {buildNextStepNew, buildOptimisticNextStep} from '@libs/NextStepUtils'; import LocalNotification from '@libs/Notification/LocalNotification'; import {rand64} from '@libs/NumberUtils'; +import capturePageHTML from '@libs/PageHTMLCapture'; import Parser from '@libs/Parser'; import {getParsedMessageWithShortMentions} from '@libs/ParsingUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -147,6 +148,7 @@ import { getReportOrDraftReport, getReportPreviewMessage, getReportTransactions, + isAdminRoom, getReportViolations, getTitleReportField, hasOutstandingChildRequest, @@ -550,8 +552,9 @@ function notifyNewAction(reportID: string | undefined, accountID: number | undef * * @param reportID - The report ID where the comment should be added * @param notifyReportID - The report ID we should notify for new actions. This is usually the same as reportID, except when adding a comment to an expense report with a single transaction thread, in which case we want to notify the parent expense report. + * @param isInSidePanel - Whether the comment is being added from the side panel */ -function addActions(reportID: string, notifyReportID: string, ancestors: Ancestor[], timezoneParam: Timezone, text = '', file?: FileObject) { +function addActions(reportID: string, notifyReportID: string, ancestors: Ancestor[], timezoneParam: Timezone, text = '', file?: FileObject, isInSidePanel = false) { let reportCommentText = ''; let reportCommentAction: OptimisticAddCommentReportAction | undefined; let attachmentAction: OptimisticAddCommentReportAction | undefined; @@ -625,6 +628,14 @@ function addActions(reportID: string, notifyReportID: string, ancestors: Ancesto parameters.isOldDotConciergeChat = true; } + // Capture page HTML when sending from side panel to Concierge or admins room + if (isInSidePanel && (isConciergeChatReport(report) || isAdminRoom(report))) { + const pageHTML = capturePageHTML(); + if (pageHTML) { + parameters.pageHTML = pageHTML; + } + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -722,6 +733,7 @@ function addAttachmentWithComment( text = '', timezone: Timezone = CONST.DEFAULT_TIME_ZONE, shouldPlaySound = false, + isInSidePanel = false, ) { if (!reportID) { return; @@ -736,17 +748,17 @@ function addAttachmentWithComment( // Single attachment if (!Array.isArray(attachments)) { - addActions(reportID, notifyReportID, ancestors, timezone, text, attachments); + addActions(reportID, notifyReportID, ancestors, timezone, text, attachments, isInSidePanel); handlePlaySound(); return; } // Multiple attachments - first: combine text + first attachment as a single action - addActions(reportID, notifyReportID, ancestors, timezone, text, attachments?.at(0)); + addActions(reportID, notifyReportID, ancestors, timezone, text, attachments?.at(0), isInSidePanel); // Remaining: attachment-only actions (no text duplication) for (let i = 1; i < attachments?.length; i += 1) { - addActions(reportID, notifyReportID, ancestors, timezone, '', attachments?.at(i)); + addActions(reportID, notifyReportID, ancestors, timezone, '', attachments?.at(i), isInSidePanel); } // Play sound once @@ -754,11 +766,11 @@ function addAttachmentWithComment( } /** Add a single comment to a report */ -function addComment(reportID: string, notifyReportID: string, ancestors: Ancestor[], text: string, timezoneParam: Timezone, shouldPlaySound?: boolean) { +function addComment(reportID: string, notifyReportID: string, ancestors: Ancestor[], text: string, timezoneParam: Timezone, shouldPlaySound?: boolean, isInSidePanel?: boolean) { if (shouldPlaySound) { playSound(SOUNDS.DONE); } - addActions(reportID, notifyReportID, ancestors, timezoneParam, text); + addActions(reportID, notifyReportID, ancestors, timezoneParam, text, undefined, isInSidePanel); } function reportActionsExist(reportID: string): boolean { diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 1223ac0fd27c..3e2f63a913bb 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -111,6 +111,9 @@ type ReportActionComposeProps = Pick void; + /** Whether the report screen is being displayed in the side panel */ + isInSidePanel?: boolean; + /** Whether the main composer was hidden */ didHideComposerInput?: boolean; }; @@ -136,6 +139,7 @@ function ReportActionCompose({ didHideComposerInput, reportTransactions, transactionThreadReportID, + isInSidePanel = false, }: ReportActionComposeProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -332,7 +336,7 @@ function ReportActionCompose({ } if (attachmentFileRef.current) { - addAttachmentWithComment(transactionThreadReportID ?? reportID, reportID, ancestors, attachmentFileRef.current, newCommentTrimmed, personalDetail.timezone, true); + addAttachmentWithComment(transactionThreadReportID ?? reportID, reportID, ancestors, attachmentFileRef.current, newCommentTrimmed, personalDetail.timezone, true, isInSidePanel); attachmentFileRef.current = null; } else { Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed}); diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index f125a0d0e667..5f89cb20dd6e 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -195,10 +195,10 @@ function ReportFooter({ // If we are adding an action on an expense report that only has a single transaction thread child report, we need to add the action to the transaction thread instead. // This is because we need it to be associated with the transaction thread and not the expense report in order for conversational corrections to work as expected. const targetReportID = transactionThreadReportID ?? report.reportID; - addComment(targetReportID, report.reportID, targetReportAncestors, text, personalDetail.timezone ?? CONST.DEFAULT_TIME_ZONE, true); + addComment(targetReportID, report.reportID, targetReportAncestors, text, personalDetail.timezone ?? CONST.DEFAULT_TIME_ZONE, true, isInSidePanel); }, // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [report.reportID, handleCreateTask, transactionThreadReportID, targetReportAncestors], + [report.reportID, handleCreateTask, transactionThreadReportID, targetReportAncestors, isInSidePanel], ); const [didHideComposerInput, setDidHideComposerInput] = useState(!shouldShowComposeInput); @@ -257,6 +257,7 @@ function ReportFooter({ didHideComposerInput={didHideComposerInput} reportTransactions={reportTransactions} transactionThreadReportID={transactionThreadReportID} + isInSidePanel={isInSidePanel} /> From dc6472bad550ac46cf03306736a70c45409b7c3a Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 31 Dec 2025 03:26:02 +0500 Subject: [PATCH 2/8] Updated Code --- src/libs/actions/Report.ts | 1 + .../home/report/ReportActionCompose/ReportActionCompose.tsx | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 07da4585410b..b0e6b017bfb7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -152,6 +152,7 @@ import { getReportViolations, getTitleReportField, hasOutstandingChildRequest, + isAdminRoom, isChatThread as isChatThreadReportUtils, isConciergeChatReport, isCurrentUserSubmitter, diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 3e2f63a913bb..a0709b86f169 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -116,6 +116,9 @@ type ReportActionComposeProps = Pick