revert top controls, increate dropdown width

This commit is contained in:
EthanHealy01 2025-10-27 19:29:19 +00:00
parent b1de286b02
commit 3afb6f7ac7
6 changed files with 107 additions and 66 deletions

View File

@ -1,5 +1,5 @@
import React, { useState, useCallback } from "react";
import { SegmentedControl, Loader } from "@mantine/core";
import React, { useState, useCallback, useMemo } from "react";
import { SegmentedControl, Loader, Menu, Button } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import { useRainbowThemeContext } from "./RainbowThemeProvider";
import rainbowStyles from '../../styles/rainbow.module.css';
@ -22,15 +22,14 @@ const viewOptionStyle: React.CSSProperties = {
};
// Build view options; in compact mode, only the selected shows text, others show icon only
// Build view options showing text always
const createViewOptions = (
currentView: WorkbenchType,
switchingTo: WorkbenchType | null,
activeFiles: Array<{ fileId: string; name: string; versionNumber?: number }>,
currentFileIndex: number,
onFileSelect?: (index: number) => void,
customViews?: CustomWorkbenchViewInstance[],
compactNonSelected?: boolean
customViews?: CustomWorkbenchViewInstance[]
) => {
const currentFile = activeFiles[currentFileIndex];
const isInViewer = currentView === 'viewer';
@ -56,9 +55,7 @@ const createViewOptions = (
) : (
<VisibilityIcon fontSize="small" />
)}
{compactNonSelected && currentView !== ("viewer" as WorkbenchType) ? null : (
<span className="ph-no-capture">{displayName}</span>
)}
<span className="ph-no-capture">{displayName}</span>
</div>
),
value: "viewer",
@ -70,12 +67,12 @@ const createViewOptions = (
{currentView === "pageEditor" ? (
<>
{switchingTo === "pageEditor" ? <Loader size="xs" /> : <EditNoteIcon fontSize="small" />}
{compactNonSelected && currentView !== ("pageEditor" as WorkbenchType) ? null : <span>Page Editor</span>}
<span>Page Editor</span>
</>
) : (
<>
{switchingTo === "pageEditor" ? <Loader size="xs" /> : <EditNoteIcon fontSize="small" />}
{compactNonSelected && currentView !== ("pageEditor" as WorkbenchType) ? null : <span>Page Editor</span>}
<span>Page Editor</span>
</>
)}
</div>
@ -89,12 +86,12 @@ const createViewOptions = (
{currentView === "fileEditor" ? (
<>
{switchingTo === "fileEditor" ? <Loader size="xs" /> : <FolderIcon fontSize="small" />}
{compactNonSelected && currentView !== ("fileEditor" as WorkbenchType) ? null : <span>Active Files</span>}
<span>Active Files</span>
</>
) : (
<>
{switchingTo === "fileEditor" ? <Loader size="xs" /> : <FolderIcon fontSize="small" />}
{compactNonSelected && currentView !== ("fileEditor" as WorkbenchType) ? null : <span>Active Files</span>}
<span>Active Files</span>
</>
)}
</div>
@ -118,7 +115,7 @@ const createViewOptions = (
) : (
view.icon || <PictureAsPdfIcon fontSize="small" />
)}
{compactNonSelected && currentView !== view.workbenchId ? null : <span>{view.label}</span>}
<span>{view.label}</span>
</div>
),
value: view.workbenchId,
@ -146,7 +143,6 @@ const TopControls = ({
}: TopControlsProps) => {
const { isRainbowMode } = useRainbowThemeContext();
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
const isMobile = useMediaQuery('(max-width: 768px)') ?? false;
const handleViewChange = useCallback((view: string) => {
if (!isValidWorkbench(view)) {
@ -170,42 +166,40 @@ const TopControls = ({
});
}, [setCurrentView]);
const totalOptions = 3 + (customViews?.filter((v) => v.data != null).length ?? 0);
const compactNonSelected = isMobile && totalOptions > 3;
return (
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
<div className="flex justify-center mt-[0.5rem]" style={{ pointerEvents: 'auto' }}>
<SegmentedControl
data-tour="view-switcher"
data={createViewOptions(currentView, switchingTo, activeFiles, currentFileIndex, onFileSelect, customViews, compactNonSelected)}
value={currentView}
onChange={handleViewChange}
color="blue"
fullWidth
className={isRainbowMode ? rainbowStyles.rainbowSegmentedControl : ''}
style={{
transition: 'all 0.2s ease',
opacity: switchingTo ? 0.8 : 1,
pointerEvents: 'auto'
}}
styles={{
root: {
borderRadius: 9999,
maxHeight: '2.6rem',
},
control: {
borderRadius: 9999,
},
indicator: {
borderRadius: 9999,
maxHeight: '2rem',
},
label: {
paddingTop: '0rem',
}
}}
/>
<SegmentedControl
data-tour="view-switcher"
data={createViewOptions(currentView, switchingTo, activeFiles, currentFileIndex, onFileSelect, customViews)}
value={currentView}
onChange={handleViewChange}
color="blue"
fullWidth
className={isRainbowMode ? rainbowStyles.rainbowSegmentedControl : ''}
style={{
transition: 'all 0.2s ease',
opacity: switchingTo ? 0.8 : 1,
pointerEvents: 'auto'
}}
styles={{
root: {
borderRadius: 9999,
maxHeight: '2.6rem',
},
control: {
borderRadius: 9999,
},
indicator: {
borderRadius: 9999,
maxHeight: '2rem',
},
label: {
paddingTop: '0rem',
}
}}
/>
</div>
</div>
);

View File

@ -164,7 +164,7 @@ const CompareNavigationDropdown = ({
</div>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Dropdown className="compare-changes-dropdown">
<div className="compare-dropdown-scrollwrap">
<ScrollArea.Autosize mah={300} viewportRef={viewportRef} onScrollPositionChange={handleScrollPos}>
<div ref={searchRef}>

View File

@ -122,6 +122,17 @@
font-weight: 500 !important;
}
/* Wider dropdown menu for long block text */
.compare-changes-dropdown {
min-width: 520px !important;
max-width: 70vw !important;
}
/* Ensure options text uses full width inside wider dropdown */
.compare-dropdown-option__text {
max-width: 100%;
}
/* Style the dropdown container */
.compare-changes-select .mantine-Combobox-dropdown {
border: 1px solid var(--mantine-color-gray-3) !important;

View File

@ -1,5 +1,6 @@
import { pdfWorkerManager } from '../../../services/pdfWorkerManager';
import { appendWord as sharedAppendWord } from '../../../utils/textDiff';
import { PARAGRAPH_SENTINEL } from '../../../types/compare';
import type { StirlingFile } from '../../../types/fileContext';
import type { PDFPageProxy, TextContent, TextItem } from 'pdfjs-dist/types/src/display/api';
import type {
@ -266,6 +267,27 @@ export const getWorkerErrorCode = (value: unknown): 'EMPTY_TEXT' | 'TOO_LARGE' |
return undefined;
};
// Produce a filtered view of tokens/metadata that excludes paragraph sentinel markers,
// returning a mapping to original indices for potential future use.
export const filterTokensForDiff = (
tokens: string[],
metadata: TokenMetadata[],
): { tokens: string[]; metadata: TokenMetadata[]; filteredToOriginal: number[] } => {
const outTokens: string[] = [];
const outMeta: TokenMetadata[] = [];
const map: number[] = [];
for (let i = 0; i < tokens.length; i += 1) {
const t = tokens[i];
const isPara = t === PARAGRAPH_SENTINEL || t.startsWith('\uE000') || t.includes('PARA');
if (!isPara) {
outTokens.push(t);
if (metadata[i]) outMeta.push(metadata[i]);
map.push(i);
}
}
return { tokens: outTokens, metadata: outMeta, filteredToOriginal: map };
};
export const extractContentFromPdf = async (file: StirlingFile): Promise<ExtractedContent> => {
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await pdfWorkerManager.createDocument(arrayBuffer, {
@ -299,16 +321,23 @@ export const extractContentFromPdf = async (file: StirlingFile): Promise<Extract
.replace(/[“”]/g, '"')
.replace(/[]/g, "'")
.replace(/[–—]/g, '-')
.replace(/\u00A0/g, ' ')
.replace(/\s+/g, ' ')
.trim();
const isParagraphBreak = (curr: TextItem, prev: TextItem | null, yJumpThreshold = 6) => {
const isParagraphBreak = (curr: TextItem, prev: TextItem | null) => {
const hasHardBreak = 'hasEOL' in curr && (curr as TextItem).hasEOL;
if (hasHardBreak) return true;
if (!prev) return false;
const prevY = prev.transform[5];
const currY = curr.transform[5];
return Math.abs(currY - prevY) > yJumpThreshold;
const dy = Math.abs(currY - prevY);
const currX = curr.transform[4];
const prevX = prev.transform[4];
const approxLine = Math.max(10, Math.abs((curr as any).height ?? 0) * 0.9);
const looksLikeParagraph = dy > approxLine * 1.8;
const likelySoftWrap = currX < prevX && dy < approxLine * 0.6;
return looksLikeParagraph && !likelySoftWrap;
};
const adjustBoundingBox = (left: number, top: number, width: number, height: number): TokenBoundingBox | null => {

View File

@ -19,6 +19,7 @@ import {
createSummaryFile,
extractContentFromPdf,
getWorkerErrorCode,
filterTokensForDiff,
} from './operationUtils';
export interface CompareOperationHook extends ToolOperationHook<CompareParameters> {
@ -207,17 +208,21 @@ export const useCompareOperation = (): CompareOperationHook => {
setStatus(t('compare.status.processing', 'Analyzing differences...'));
// Filter out paragraph sentinels before diffing to avoid large false-positive runs
const baseFiltered = filterTokensForDiff(baseContent.tokens, baseContent.metadata);
const comparisonFiltered = filterTokensForDiff(comparisonContent.tokens, comparisonContent.metadata);
const { tokens, stats, warnings: workerWarnings } = await runCompareWorker(
baseContent.tokens,
comparisonContent.tokens,
baseFiltered.tokens,
comparisonFiltered.tokens,
warningMessages
);
const totals = aggregateTotals(tokens);
const processedAt = Date.now();
const baseMetadata = baseContent.metadata;
const comparisonMetadata = comparisonContent.metadata;
const baseMetadata = baseFiltered.metadata;
const comparisonMetadata = comparisonFiltered.metadata;
const changes = buildChanges(tokens, baseMetadata, comparisonMetadata);

View File

@ -80,11 +80,14 @@ const chunkedDiff = (
const tokens: CompareDiffToken[] = [];
let start1 = 0;
let start2 = 0;
const overlap = Math.max(0, Math.min(500, Math.floor(chunkSize * 0.1)));
// Advance by the actual number of tokens consumed per chunk to maintain alignment
while (start1 < words1.length || start2 < words2.length) {
const slice1 = words1.slice(start1, Math.min(start1 + chunkSize, words1.length));
const slice2 = words2.slice(start2, Math.min(start2 + chunkSize, words2.length));
const end1 = Math.min(start1 + chunkSize, words1.length);
const end2 = Math.min(start2 + chunkSize, words2.length);
const slice1 = words1.slice(start1, end1);
const slice2 = words2.slice(start2, end2);
const chunkTokens = diff(slice1, slice2);
tokens.push(...chunkTokens);
@ -93,23 +96,22 @@ const chunkedDiff = (
let consumed1 = 0;
let consumed2 = 0;
for (const t of chunkTokens) {
if (t.type === 'unchanged') {
consumed1 += 1; consumed2 += 1;
} else if (t.type === 'removed') {
consumed1 += 1;
} else if (t.type === 'added') {
consumed2 += 1;
}
if (t.type === 'unchanged') { consumed1 += 1; consumed2 += 1; }
else if (t.type === 'removed') { consumed1 += 1; }
else if (t.type === 'added') { consumed2 += 1; }
}
// Fallback to progress by a small step if diff returned nothing (shouldn't happen)
// Fallback to ensure forward progress
if (consumed1 === 0 && consumed2 === 0) {
consumed1 = Math.min(chunkSize, words1.length - start1);
consumed2 = Math.min(chunkSize, words2.length - start2);
}
start1 += consumed1;
start2 += consumed2;
// Advance with overlap to allow re-synchronization across chunk boundaries
const nextStart1 = Math.min(words1.length, Math.max(start1 + consumed1 - overlap, start1 + 1));
const nextStart2 = Math.min(words2.length, Math.max(start2 + consumed2 - overlap, start2 + 1));
start1 = nextStart1;
start2 = nextStart2;
}
return tokens;