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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.250.209"
VERSION = "0.250.210"
IS_DEVELOPMENT = is_development_env_enabled()

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
24 changes: 24 additions & 0 deletions application/single_app/static/css/chats.css
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,30 @@
white-space: nowrap;
}

/* Document rows stack the title above the file name when the two differ */
#document-dropdown-items .dropdown-item .chat-document-option-text {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-width: 0;
overflow: hidden;

Check warning on line 818 in application/single_app/static/css/chats.css

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
}

#document-dropdown-items .dropdown-item .chat-document-option-title,
#document-dropdown-items .dropdown-item .chat-document-option-filename {
display: block;
min-width: 0;
overflow: hidden;

Check warning on line 825 in application/single_app/static/css/chats.css

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
text-overflow: ellipsis;
white-space: nowrap;
}

#document-dropdown-items .dropdown-item .chat-document-option-filename {
font-size: 0.75rem;
line-height: 1.2;
opacity: 0.85;
}

/* Document dropdown items must be explicitly displayed */
#document-dropdown .dropdown-item {
align-items: center;
Expand Down
44 changes: 33 additions & 11 deletions application/single_app/static/js/chat/chat-documents.js
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,10 @@
return (documentItem.title || documentItem.file_name || 'Untitled Document').trim() || 'Untitled Document';
}

function getDocumentFileName(documentItem) {
return String((documentItem || {}).file_name || '').trim();
}

function createDropdownHeader(label) {
const header = document.createElement('div');
header.classList.add('dropdown-header', 'small', 'text-muted', 'px-2', 'pt-2', 'pb-1');
Expand All @@ -1449,10 +1453,16 @@
}

function buildDocumentDescriptor(documentItem, sectionLabel) {
const displayName = getDocumentDisplayName(documentItem);
const fileName = getDocumentFileName(documentItem);
const showsFileName = Boolean(fileName) && fileName.toLowerCase() !== displayName.toLowerCase();

return {
id: documentItem.id,
label: getDocumentDisplayName(documentItem),
searchLabel: `${getDocumentDisplayName(documentItem)} ${sectionLabel}`.trim(),
label: displayName,
fileName: fileName,
secondaryLabel: showsFileName ? fileName : '',
searchLabel: [displayName, fileName, sectionLabel].filter(Boolean).join(' '),

Check warning on line 1465 in application/single_app/static/js/chat/chat-documents.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
tags: documentItem.tags || [],
classification: documentItem.document_classification || '',
};
Expand Down Expand Up @@ -1484,7 +1494,7 @@
dropdownItem.classList.add('dropdown-item', 'd-flex', 'align-items-center');
dropdownItem.setAttribute('data-document-id', doc.id);
dropdownItem.setAttribute('data-search-role', 'item');
dropdownItem.setAttribute('title', doc.label);
dropdownItem.setAttribute('title', doc.secondaryLabel ? `${doc.label}\n${doc.secondaryLabel}` : doc.label);
dropdownItem.dataset.searchLabel = doc.searchLabel;
dropdownItem.dataset.tags = JSON.stringify(doc.tags || []);
dropdownItem.dataset.classification = doc.classification || '';
Expand All @@ -1498,14 +1508,23 @@
checkbox.style.pointerEvents = 'none';
checkbox.style.minWidth = '16px';

const label = document.createElement('span');
label.textContent = doc.label;
label.style.overflow = 'hidden';
label.style.textOverflow = 'ellipsis';
label.style.whiteSpace = 'nowrap';
const labelWrapper = document.createElement('span');
labelWrapper.classList.add('chat-document-option-text');

Check warning on line 1512 in application/single_app/static/js/chat/chat-documents.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

const primaryLabel = document.createElement('span');
primaryLabel.classList.add('chat-document-option-title');

Check warning on line 1515 in application/single_app/static/js/chat/chat-documents.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
primaryLabel.textContent = doc.label;
labelWrapper.appendChild(primaryLabel);

if (doc.secondaryLabel) {
const secondaryLabel = document.createElement('span');
secondaryLabel.classList.add('chat-document-option-filename', 'text-muted');

Check warning on line 1521 in application/single_app/static/js/chat/chat-documents.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
secondaryLabel.textContent = doc.secondaryLabel;
labelWrapper.appendChild(secondaryLabel);
}

dropdownItem.appendChild(checkbox);
dropdownItem.appendChild(label);
dropdownItem.appendChild(labelWrapper);
docDropdownItems.appendChild(dropdownItem);
});
}
Expand Down Expand Up @@ -2605,8 +2624,11 @@
textEl.textContent = isExplicitDocumentSelectionMode() ? "Select Documents" : "All Documents";
} else if (count === 1) {
const selectedDocumentId = selectedDocumentOptions[0].value;
const labelSpan = docDropdownItems
? docDropdownItems.querySelector(`.dropdown-item[data-document-id="${selectedDocumentId}"] span`)
const selectedItem = docDropdownItems
? docDropdownItems.querySelector(`.dropdown-item[data-document-id="${selectedDocumentId}"]`)
: null;
const labelSpan = selectedItem
? (selectedItem.querySelector('.chat-document-option-title') || selectedItem.querySelector('span'))
: null;
textEl.textContent = labelSpan ? labelSpan.textContent : "1 document selected";
} else {
Expand Down
7 changes: 5 additions & 2 deletions application/single_app/static/js/chat/chat-onload.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,11 @@ window.addEventListener('DOMContentLoaded', async () => {
if (textEl) {
if (docIdsToSelect.length === 1) {
// Find the label from the dropdown item
const matchItem = docDropdownItems
? docDropdownItems.querySelector(`.dropdown-item[data-document-id="${docIdsToSelect[0]}"] span`)
const matchRow = docDropdownItems
? docDropdownItems.querySelector(`.dropdown-item[data-document-id="${docIdsToSelect[0]}"]`)
: null;
const matchItem = matchRow
? (matchRow.querySelector('.chat-document-option-title') || matchRow.querySelector('span'))
: null;
textEl.textContent = matchItem ? matchItem.textContent : "1 document selected";
} else {
Expand Down
157 changes: 125 additions & 32 deletions application/single_app/static/js/chat/chat-searchable-select.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
// chat-searchable-select.js

const SEARCH_ROLE_ACTION = 'action';

Check warning on line 3 in application/single_app/static/js/chat/chat-searchable-select.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

Check warning on line 3 in application/single_app/static/js/chat/chat-searchable-select.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
const SEARCH_SEPARATOR_PATTERN = /[_\-.]+/g;

Check warning on line 4 in application/single_app/static/js/chat/chat-searchable-select.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
const SEARCH_WHITESPACE_PATTERN = /\s+/g;

Check warning on line 5 in application/single_app/static/js/chat/chat-searchable-select.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.

export function normalizeSearchText(value) {
return String(value ?? '')
.toLowerCase()
.replace(SEARCH_SEPARATOR_PATTERN, ' ')
.replace(SEARCH_WHITESPACE_PATTERN, ' ')
.trim();
}

export function matchesSearchTokens(searchText, searchTerm) {
const normalizedTerm = normalizeSearchText(searchTerm);

if (!normalizedTerm) {
return true;
}

const normalizedText = normalizeSearchText(searchText);
return normalizedTerm.split(' ').every(token => normalizedText.includes(token));
}

function createNoMatchesElement(message) {
const noMatchesEl = document.createElement('div');
noMatchesEl.className = 'no-matches text-center text-muted py-2';
Expand All @@ -14,12 +37,89 @@
}
}

function isHiddenElement(el) {
return Boolean(el && el.classList.contains('d-none'));
}

function isDividerElement(el) {
return Boolean(el && el.classList.contains('dropdown-divider'));
}

function isHeaderElement(el) {
return Boolean(el && el.classList.contains('dropdown-header'));
}

function isVisibleItem(el) {
return Boolean(
el &&
!el.classList.contains('d-none') &&
!el.classList.contains('dropdown-divider')
);
return Boolean(el && !isHiddenElement(el) && !isDividerElement(el));
}

// Section content is what a divider is allowed to separate. Always-visible action rows
// are excluded so an orphaned divider cannot anchor itself to the "Select All" row.
function isVisibleSectionContent(el) {
if (!el || isHiddenElement(el) || isDividerElement(el)) {
return false;
}

if (isHeaderElement(el)) {
return true;
}

return el.classList.contains('dropdown-item')
&& el.getAttribute('data-search-role') !== SEARCH_ROLE_ACTION;
}

// A divider is "bound" when it introduces a section header, so its visibility follows
// that header instead of whatever unrelated row happens to still be visible above it.
function findDividerBoundHeader(children, dividerIndex) {
for (let index = dividerIndex + 1; index < children.length; index += 1) {
const candidate = children[index];

if (isDividerElement(candidate)) {
continue;
}

return isHeaderElement(candidate) ? candidate : null;
}

return null;
}

function collapseRedundantDividers(children) {
let sawVisibleContent = false;

children.forEach(child => {
if (isDividerElement(child)) {
if (isHiddenElement(child)) {
return;
}

if (!sawVisibleContent) {
child.classList.add('d-none');
return;
}

sawVisibleContent = false;
return;
}

if (!isHiddenElement(child)) {
sawVisibleContent = true;
}
});

for (let index = children.length - 1; index >= 0; index -= 1) {
const child = children[index];

if (isHiddenElement(child)) {
continue;
}

if (!isDividerElement(child)) {
break;
}

child.classList.add('d-none');
}
}

function updateDropdownStructure(itemsContainerEl) {
Expand All @@ -30,14 +130,14 @@
const children = Array.from(itemsContainerEl.children).filter(child => !child.classList.contains('no-matches'));

children.forEach(child => {
if (!child.classList.contains('dropdown-header')) {
if (!isHeaderElement(child)) {
return;
}

let hasVisibleItem = false;
let next = child.nextElementSibling;

while (next && !next.classList.contains('dropdown-header')) {
while (next && !isHeaderElement(next)) {
if (next.classList.contains('dropdown-item') && isVisibleItem(next)) {
hasVisibleItem = true;
break;
Expand All @@ -48,33 +148,27 @@
child.classList.toggle('d-none', !hasVisibleItem);
});

children.forEach(child => {
if (!child.classList.contains('dropdown-divider')) {
children.forEach((child, index) => {
if (!isDividerElement(child)) {
return;
}

let previousVisible = null;
let previous = child.previousElementSibling;
while (previous) {
if (!previous.classList.contains('no-matches') && isVisibleItem(previous)) {
previousVisible = previous;
break;
}
previous = previous.previousElementSibling;
}
const boundHeader = findDividerBoundHeader(children, index);
const hasSectionContentBefore = children.slice(0, index).some(sibling => isVisibleSectionContent(sibling));
let keepDivider;

let nextVisible = null;
let next = child.nextElementSibling;
while (next) {
if (!next.classList.contains('no-matches') && isVisibleItem(next)) {
nextVisible = next;
break;
}
next = next.nextElementSibling;
if (boundHeader) {
keepDivider = !isHiddenElement(boundHeader) && hasSectionContentBefore;
} else {
const hasVisibleContentBefore = children.slice(0, index).some(sibling => isVisibleItem(sibling));
const hasSectionContentAfter = children.slice(index + 1).some(sibling => isVisibleSectionContent(sibling));
keepDivider = hasVisibleContentBefore && hasSectionContentAfter;
}

child.classList.toggle('d-none', !(previousVisible && nextVisible));
child.classList.toggle('d-none', !keepDivider);
});

collapseRedundantDividers(children);
}

function createDropdownHeader(label) {
Expand Down Expand Up @@ -160,14 +254,13 @@
const isAlwaysVisible = isAlwaysVisibleItem || (() => false);

const applyFilter = (rawSearchTerm = '') => {
const searchTerm = rawSearchTerm.toLowerCase().trim();
const searchTerm = normalizeSearchText(rawSearchTerm);
const items = Array.from(itemsContainerEl.querySelectorAll(itemSelector));
let visibleMatchCount = 0;

items.forEach(item => {
const keepVisible = isAlwaysVisible(item);
const searchText = String(readSearchText(item) || '').toLowerCase();
const matches = !searchTerm || keepVisible || searchText.includes(searchTerm);
const matches = keepVisible || matchesSearchTokens(readSearchText(item), searchTerm);

item.classList.toggle('d-none', !matches);

Expand Down Expand Up @@ -289,7 +382,7 @@
};

const renderOptions = () => {
const searchTerm = searchInputEl.value.toLowerCase().trim();
const searchTerm = normalizeSearchText(searchInputEl.value);
const options = Array.from(selectEl.options);
const optionIndexMap = new Map(options.map((option, index) => [option, index]));
const selectedIndex = selectEl.selectedIndex;
Expand All @@ -311,7 +404,7 @@
const index = optionIndexMap.get(option);
const optionLabel = readOptionLabel(option);
const optionSearchText = String(readOptionSearchText(option) || optionLabel).toLowerCase();
const matches = !searchTerm || optionSearchText.includes(searchTerm);
const matches = matchesSearchTokens(optionSearchText, searchTerm);

const item = document.createElement('button');
item.type = 'button';
Expand Down
Loading
Loading