mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
revert top controls, increate dropdown width
This commit is contained in:
parent
b1de286b02
commit
3afb6f7ac7
@ -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>
|
||||
);
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user