V2 results flow (#4196)

Better tool flow for reusability
Pinning 
Styling of tool flow
consumption of files after tooling

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
ConnorYoh
2025-08-15 14:43:30 +01:00
committed by GitHub
parent 1468df3e21
commit 4c17c520d7
40 changed files with 1474 additions and 1274 deletions

View File

@@ -35,6 +35,7 @@ const initialViewerConfig: ViewerConfig = {
const initialState: FileContextState = {
activeFiles: [],
processedFiles: new Map(),
pinnedFiles: new Set(),
currentMode: 'pageEditor',
currentView: 'fileEditor', // Legacy field
currentTool: null, // Legacy field
@@ -77,6 +78,9 @@ type FileContextAction =
| { type: 'SET_UNSAVED_CHANGES'; payload: boolean }
| { type: 'SET_PENDING_NAVIGATION'; payload: (() => void) | null }
| { type: 'SHOW_NAVIGATION_WARNING'; payload: boolean }
| { type: 'PIN_FILE'; payload: File }
| { type: 'UNPIN_FILE'; payload: File }
| { type: 'CONSUME_FILES'; payload: { inputFiles: File[]; outputFiles: File[] } }
| { type: 'RESET_CONTEXT' }
| { type: 'LOAD_STATE'; payload: Partial<FileContextState> };
@@ -317,6 +321,43 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
showNavigationWarning: action.payload
};
case 'PIN_FILE':
return {
...state,
pinnedFiles: new Set([...state.pinnedFiles, action.payload])
};
case 'UNPIN_FILE':
const newPinnedFiles = new Set(state.pinnedFiles);
newPinnedFiles.delete(action.payload);
return {
...state,
pinnedFiles: newPinnedFiles
};
case 'CONSUME_FILES': {
const { inputFiles, outputFiles } = action.payload;
const unpinnedInputFiles = inputFiles.filter(file => !state.pinnedFiles.has(file));
// Remove unpinned input files and add output files
const newActiveFiles = [
...state.activeFiles.filter(file => !unpinnedInputFiles.includes(file)),
...outputFiles
];
// Update processed files map - remove consumed files, keep pinned ones
const newProcessedFiles = new Map(state.processedFiles);
unpinnedInputFiles.forEach(file => {
newProcessedFiles.delete(file);
});
return {
...state,
activeFiles: newActiveFiles,
processedFiles: newProcessedFiles
};
}
case 'RESET_CONTEXT':
return {
...initialState
@@ -560,6 +601,46 @@ export function FileContextProvider({
dispatch({ type: 'CLEAR_SELECTIONS' });
}, [cleanupAllFiles]);
// File pinning functions
const pinFile = useCallback((file: File) => {
dispatch({ type: 'PIN_FILE', payload: file });
}, []);
const unpinFile = useCallback((file: File) => {
dispatch({ type: 'UNPIN_FILE', payload: file });
}, []);
const isFilePinned = useCallback((file: File): boolean => {
return state.pinnedFiles.has(file);
}, [state.pinnedFiles]);
// File consumption function
const consumeFiles = useCallback(async (inputFiles: File[], outputFiles: File[]): Promise<void> => {
dispatch({ type: 'CONSUME_FILES', payload: { inputFiles, outputFiles } });
// Store new output files if persistence is enabled
if (enablePersistence) {
for (const file of outputFiles) {
try {
const fileId = getFileId(file);
if (!fileId) {
try {
const thumbnail = await (thumbnailGenerationService as any).generateThumbnail(file);
const storedFile = await fileStorage.storeFile(file, thumbnail);
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
} catch (thumbnailError) {
console.warn('Failed to generate thumbnail, storing without:', thumbnailError);
const storedFile = await fileStorage.storeFile(file);
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
}
}
} catch (error) {
console.error('Failed to store output file:', error);
}
}
}
}, [enablePersistence, state.pinnedFiles]);
// Navigation guard system functions
const setHasUnsavedChanges = useCallback((hasChanges: boolean) => {
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: hasChanges });
@@ -783,6 +864,10 @@ export function FileContextProvider({
removeFiles,
replaceFile,
clearAllFiles,
pinFile,
unpinFile,
isFilePinned,
consumeFiles,
setCurrentMode,
setCurrentView,
setCurrentTool,

View File

@@ -1,8 +1,9 @@
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
import {
MaxFiles,
FileSelectionContextValue
} from '../types/tool';
import { useFileContext } from './FileContext';
interface FileSelectionProviderProps {
children: ReactNode;
@@ -11,10 +12,23 @@ interface FileSelectionProviderProps {
const FileSelectionContext = createContext<FileSelectionContextValue | undefined>(undefined);
export function FileSelectionProvider({ children }: FileSelectionProviderProps) {
const { activeFiles } = useFileContext();
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [maxFiles, setMaxFiles] = useState<MaxFiles>(-1);
const [isToolMode, setIsToolMode] = useState<boolean>(false);
// Sync selected files with active files - remove any selected files that are no longer active
useEffect(() => {
if (selectedFiles.length > 0) {
const activeFileSet = new Set(activeFiles);
const validSelectedFiles = selectedFiles.filter(file => activeFileSet.has(file));
if (validSelectedFiles.length !== selectedFiles.length) {
setSelectedFiles(validSelectedFiles);
}
}
}, [activeFiles, selectedFiles]);
const clearSelection = useCallback(() => {
setSelectedFiles([]);
}, []);

View File

@@ -15,11 +15,11 @@ interface ToolWorkflowState {
sidebarsVisible: boolean;
leftPanelView: 'toolPicker' | 'toolContent';
readerMode: boolean;
// File/Preview State
previewFile: File | null;
pageEditorFunctions: PageEditorFunctions | null;
// Search State
searchQuery: string;
}
@@ -72,7 +72,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
selectedToolKey: string | null;
selectedTool: Tool | null;
toolRegistry: any; // From useToolManagement
// UI Actions
setSidebarsVisible: (visible: boolean) => void;
setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void;
@@ -80,16 +80,16 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
setPreviewFile: (file: File | null) => void;
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
setSearchQuery: (query: string) => void;
// Tool Actions
selectTool: (toolId: string) => void;
clearToolSelection: () => void;
// Workflow Actions (compound actions)
handleToolSelect: (toolId: string) => void;
handleBackToTools: () => void;
handleReaderToggle: () => void;
// Computed values
filteredTools: [string, any][]; // Filtered by search
isPanelVisible: boolean;
@@ -106,7 +106,7 @@ interface ToolWorkflowProviderProps {
export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowProviderProps) {
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
// Tool management hook
const {
selectedToolKey,
@@ -181,7 +181,7 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro
);
}, [toolRegistry, state.searchQuery]);
const isPanelVisible = useMemo(() =>
const isPanelVisible = useMemo(() =>
state.sidebarsVisible && !state.readerMode,
[state.sidebarsVisible, state.readerMode]
);
@@ -193,7 +193,7 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro
selectedToolKey,
selectedTool,
toolRegistry,
// Actions
setSidebarsVisible,
setLeftPanelView,
@@ -203,12 +203,12 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro
setSearchQuery,
selectTool,
clearToolSelection,
// Workflow Actions
handleToolSelect,
handleBackToTools,
handleReaderToggle,
// Computed
filteredTools,
isPanelVisible,
@@ -232,5 +232,5 @@ export function useToolWorkflow(): ToolWorkflowContextValue {
// Convenience exports for specific use cases (optional - components can use useToolWorkflow directly)
export const useToolSelection = useToolWorkflow;
export const useToolPanelState = useToolWorkflow;
export const useWorkbenchState = useToolWorkflow;
export const useToolPanelState = useToolWorkflow;
export const useWorkbenchState = useToolWorkflow;