diff --git a/src/__tests__/integration/component-integration.test.tsx b/src/__tests__/integration/component-integration.test.tsx index c600f03..1e18295 100644 --- a/src/__tests__/integration/component-integration.test.tsx +++ b/src/__tests__/integration/component-integration.test.tsx @@ -96,12 +96,11 @@ describe('Component Integration Tests', () => { /> ) - // Should show progress indicator - expect(screen.getByText(/Generating Formats \(/)).toBeInTheDocument() - expect(screen.getByText(/1\/3/)).toBeInTheDocument() + // Should show progress indicator (bullets complete, so showing format 2 of 3) + expect(screen.getByText(/Generating Formats \(Format 2 of 3\)/)).toBeInTheDocument() // Should show currently processing format - expect(screen.getByText(/Currently processing: paragraphs/)).toBeInTheDocument() + expect(screen.getByText(/Currently processing:/)).toBeInTheDocument() }) it('should handle clear functionality', async () => { diff --git a/src/components/input/ExampleContentMenu.tsx b/src/components/input/ExampleContentMenu.tsx index b1e0e41..bda1d98 100644 --- a/src/components/input/ExampleContentMenu.tsx +++ b/src/components/input/ExampleContentMenu.tsx @@ -150,7 +150,7 @@ export const ExampleContentMenu = ({ aria-controls={isOpen ? menuId : undefined} > - Load example + Load example void initialText?: string selectedFormats?: FormatType[] + targetLanguage?: string onInputStateChange?: (hasText: boolean) => void onProcessingStart?: () => void onOperationProgress?: (progress: OperationProgress | null) => void @@ -41,6 +42,7 @@ export const TextInputPanel = ({ onProcessingError, initialText = '', selectedFormats: propSelectedFormats, + targetLanguage, onInputStateChange, onProcessingStart, onOperationProgress, @@ -97,9 +99,12 @@ export const TextInputPanel = ({ onInputStateChange?.(text.trim().length > 0) }, [text, onInputStateChange]) + const preferenceSnapshot = loadPreferences() + // Format transform hook - auto-initialize with reading level const formatTransform = useFormatTransform(true, { - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: preferenceSnapshot.readingLevel || undefined, + targetLanguage: targetLanguage || preferenceSnapshot.targetLanguage || undefined, }) // Pass progress updates to parent @@ -203,13 +208,18 @@ export const TextInputPanel = ({ // Store the original text before processing const originalTextSnapshot = text + const preferences = loadPreferences() + const readingLevelPreference = preferences.readingLevel || undefined + const effectiveTargetLanguage = + targetLanguage || preferences.targetLanguage || undefined try { // Check if format transform service is ready if (!formatTransform.isReady) { setStatus('initializing') await formatTransform.initialize({ - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: readingLevelPreference, + targetLanguage: effectiveTargetLanguage, }) } @@ -223,7 +233,8 @@ export const TextInputPanel = ({ text, selectedFormats[0], { - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: readingLevelPreference, + targetLanguage: effectiveTargetLanguage, } ) @@ -238,18 +249,15 @@ export const TextInputPanel = ({ text, selectedFormats, (format, content, isComplete) => { - console.log('[TextInputPanel] Streaming callback:', format, 'isComplete:', isComplete, 'content length:', content.length) if (isComplete) { aggregatedResults[format] = content - console.log('[TextInputPanel] ✅ Format SAVED to aggregatedResults:', format, 'Content length:', content.length) - } else { - console.log('[TextInputPanel] ⏳ Format streaming (not saved yet):', format) } // Pass streaming updates to parent onStreamingUpdate?.(format, content, isComplete) }, { - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: readingLevelPreference, + targetLanguage: effectiveTargetLanguage, } ) @@ -506,13 +514,12 @@ export const TextInputPanel = ({
) : ( { isLoading={isLoading} /> ) - expect(screen.getByText(/1\/3/)).toBeInTheDocument() + // 1 complete, so showing Format 2 of 3 + expect(screen.getByText(/Format 2 of 3/)).toBeInTheDocument() }) it('should show currently processing format', () => { diff --git a/src/components/output/FormattedOutput.tsx b/src/components/output/FormattedOutput.tsx index 3079be2..aa9b1f6 100644 --- a/src/components/output/FormattedOutput.tsx +++ b/src/components/output/FormattedOutput.tsx @@ -161,13 +161,18 @@ export const FormattedOutput = ({ const hasLoadingInfo = Boolean(isLoading && isLoading.size > 0) const isInProgress = hasLoadingInfo && completed < total - const percent = total > 0 ? (completed / total) * 100 : 0 + const partialStep = isInProgress && active ? 0.5 : 0 + const percent = total > 0 ? ((completed + partialStep) / total) * 100 : 0 + const currentPosition = isInProgress + ? Math.min(completed + 1, total) + : total return { total, completed, active: isInProgress ? active : undefined, isInProgress, + currentPosition, percent, } }, [formats, isLoading, results]) @@ -175,7 +180,7 @@ export const FormattedOutput = ({ const formatCountLabel = useMemo(() => { const suffix = progress.total !== 1 ? 's' : '' if (progress.isInProgress) { - return `${progress.completed} of ${progress.total} format${suffix} complete` + return `Currently processing format ${progress.currentPosition} of ${progress.total}` } return `${progress.total} format${suffix} generated` }, [progress]) @@ -207,7 +212,7 @@ export const FormattedOutput = ({

- Generating Formats ({progress.completed}/{progress.total}) + Generating Formats (Format {progress.currentPosition} of {progress.total})

{progress.active && `Currently processing: ${getFormatLabels([progress.active])}`} @@ -404,8 +409,6 @@ export const FormattedOutput = ({ const isFormatLoading = isLoading?.get(format) ?? false const isFormatExpanded = expandedFormats.has(format) - console.log(`[FormattedOutput RENDER FormatSection] format: ${format}, content length: ${content.length}, isLoading: ${isFormatLoading}, isExpanded: ${isFormatExpanded}`) - return (

-

Quick Presets

-

Common format combinations

+

Quick Presets

+

Common format combinations

{selectedPresetId && ( @@ -78,8 +78,8 @@ export const FormatPresetSelector = ({ disabled={disabled} className={`rounded-xl border px-4 py-3 text-left transition hc-surface ${ isSelected - ? 'border-synapse-500 bg-synapse-50 shadow-soft hc-surface--active' - : 'border-neutral-200 hover:border-neutral-300 hover:bg-neutral-50' + ? 'border-synapse-500 bg-synapse-50 shadow-soft hc-surface--active dark:border-synapse-400 dark:bg-synapse-900/30' + : 'border-neutral-200 hover:border-neutral-300 hover:bg-neutral-50 dark:border-neutral-700 dark:hover:border-neutral-500 dark:hover:bg-neutral-800/60' } ${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`} aria-pressed={isSelected} > @@ -88,7 +88,9 @@ export const FormatPresetSelector = ({
-

{preset.label}

+

{preset.label}

{isSelected && ( - + Active )}
-

{preset.description}

+

{preset.description}

diff --git a/src/components/preferences/FormatPreviewCard.tsx b/src/components/preferences/FormatPreviewCard.tsx index 7abe4b5..0f13fd6 100644 --- a/src/components/preferences/FormatPreviewCard.tsx +++ b/src/components/preferences/FormatPreviewCard.tsx @@ -64,8 +64,8 @@ export const FormatPreviewCard = ({ disabled={disabled} className={`flex h-full flex-col items-start rounded-2xl border px-5 py-4 text-left transition hc-surface ${ isSelected - ? 'border-primary-600 bg-primary-50 shadow-[0_0_0_4px_rgba(59,130,246,0.15)] hc-surface--active' - : 'border-neutral-200 hover:border-neutral-300 hover:bg-neutral-50' + ? 'border-primary-600 bg-primary-50 shadow-[0_0_0_4px_rgba(59,130,246,0.15)] hc-surface--active dark:border-primary-400 dark:bg-primary-900/30 dark:shadow-[0_0_0_4px_rgba(56,189,248,0.25)]' + : 'border-neutral-200 hover:border-neutral-300 hover:bg-neutral-50 dark:border-neutral-700 dark:hover:border-neutral-500 dark:hover:bg-neutral-800/60' } ${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`} aria-pressed={isSelected} > @@ -76,13 +76,15 @@ export const FormatPreviewCard = ({
-

{config.label}

+

{config.label}

{isSelected && ( - + Selected )} @@ -119,14 +121,14 @@ export const FormatPreviewCard = ({
{/* Description */} -

{config.description}

+

{config.description}

{/* Preview */} -
-

+

+

Preview

-
+        
           {config.preview}
         
diff --git a/src/components/preferences/FormatSelector.tsx b/src/components/preferences/FormatSelector.tsx index 8612808..130ff0c 100644 --- a/src/components/preferences/FormatSelector.tsx +++ b/src/components/preferences/FormatSelector.tsx @@ -25,32 +25,23 @@ export const FormatSelector = ({ const handleFormatToggle = (type: FormatType) => { const isSelected = selectedFormats.includes(type) - // If toggling summary - if (type === 'summary') { - if (isSelected) { - // Deselecting summary - onChangeFormats(selectedFormats.filter((f) => f !== type)) - } else { - // Selecting summary - replace all other formats - onChangeFormats(['summary']) + let updatedFormats: FormatType[] + + if (isSelected) { + const next = selectedFormats.filter((format) => format !== type) + + // Prevent empty selection – keep at least one format + if (next.length === 0) { + return } + + updatedFormats = next } else { - // Toggling a non-summary format - if (isSelected) { - // Deselecting this format - prevent empty selection - const newFormats = selectedFormats.filter((f) => f !== type) - if (newFormats.length > 0) { - onChangeFormats(newFormats) - } - // If this would result in empty selection, do nothing (keep at least one format) - } else { - // Selecting this format - remove summary if present - const newFormats = selectedFormats.filter((f) => f !== 'summary') - onChangeFormats([...newFormats, type]) - } + updatedFormats = [...selectedFormats, type] } - // Clear preset when manual selection changes + onChangeFormats(updatedFormats) + if (selectedPreset) { onChangePreset(null) } @@ -76,10 +67,12 @@ export const FormatSelector = ({ {/* Divider */}
-
+
- or choose individual formats + + or choose individual formats +
@@ -97,10 +90,10 @@ export const FormatSelector = ({ {/* Selected formats summary */} {selectedFormats.length > 0 && isValidFormatCombination(selectedFormats) && ( -
+
-

+

{selectedFormats.length} format{selectedFormats.length > 1 ? 's' : ''} selected

-

+

Your content will be restructured into: {getFormatLabels(selectedFormats)}

@@ -127,10 +120,10 @@ export const FormatSelector = ({ {/* Warning for invalid combinations */} {!isValidFormatCombination(selectedFormats) && ( -
+
-

Invalid format combination

-

+

Invalid format combination

+

Please select at least one format.

diff --git a/src/components/ui/MultiFormatProgress.tsx b/src/components/ui/MultiFormatProgress.tsx index a0dd1cc..bf81bc5 100644 --- a/src/components/ui/MultiFormatProgress.tsx +++ b/src/components/ui/MultiFormatProgress.tsx @@ -111,7 +111,9 @@ export const MultiFormatProgress = ({ Overall Progress - {aggregate.completedCount} of {aggregate.totalCount} formats + {aggregate.currentFormat + ? `Format ${Math.min(aggregate.completedCount + 1, aggregate.totalCount)} of ${aggregate.totalCount}` + : `${aggregate.completedCount} of ${aggregate.totalCount} formats complete`}
{ render() expect(screen.getByText('Overall Progress')).toBeInTheDocument() - expect(screen.getByText('1 of 3 formats')).toBeInTheDocument() + expect(screen.getByText('Format 2 of 3')).toBeInTheDocument() expect(screen.getByText(/currently processing: bullet points/i)).toBeInTheDocument() }) diff --git a/src/components/workspace/FormatQuickSelector.tsx b/src/components/workspace/FormatQuickSelector.tsx index 692eaac..9cec947 100644 --- a/src/components/workspace/FormatQuickSelector.tsx +++ b/src/components/workspace/FormatQuickSelector.tsx @@ -1,4 +1,5 @@ import { useId, useState } from 'react' +import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent } from 'react' import { InlineHelp } from '@/components/common/InlineHelp' import type { FormatType } from '@/lib/chrome-ai/types' import { FORMAT_OPTIONS, FORMAT_PRESETS, getFormatLabels } from '@/constants/formats' @@ -11,18 +12,34 @@ interface PreviewTooltipProps { const PreviewTooltip = ({ preview, label }: PreviewTooltipProps) => { const [show, setShow] = useState(false) + const handleOpen = () => setShow(true) + const handleClose = () => setShow(false) + const handleToggle = (event?: ReactMouseEvent | ReactKeyboardEvent) => { + if (event) { + event.stopPropagation() + event.preventDefault() + } + setShow((prev) => !prev) + } + return (
- +
{show && (
@@ -77,29 +94,22 @@ export const FormatQuickSelector = ({ const handleFormatToggle = (type: FormatType) => { const isSelected = selectedFormats.includes(type) - // If toggling summary - if (type === 'summary') { - if (isSelected) { - // Deselecting summary - default to bullets - onChangeFormats(['bullets']) - } else { - // Selecting summary - replace all other formats - onChangeFormats(['summary']) + let updatedFormats: FormatType[] + + if (isSelected) { + const next = selectedFormats.filter((format) => format !== type) + + // Keep at least one format selected + if (next.length === 0) { + return } + + updatedFormats = next } else { - // Toggling a non-summary format - if (isSelected) { - // Deselecting this format - prevent empty selection - const newFormats = selectedFormats.filter((f) => f !== type) - if (newFormats.length > 0) { - onChangeFormats(newFormats) - } - } else { - // Selecting this format - remove summary if present - const newFormats = selectedFormats.filter((f) => f !== 'summary') - onChangeFormats([...newFormats, type]) - } + updatedFormats = [...selectedFormats, type] } + + onChangeFormats(updatedFormats) } const handlePresetClick = (formats: FormatType[]) => { @@ -214,23 +224,15 @@ export const FormatQuickSelector = ({
{Object.values(FORMAT_OPTIONS).map((config) => { const isSelected = selectedFormats.includes(config.type) - const isDisabled = - config.type !== 'summary' && - selectedFormats.includes('summary') && - !isSelected - return ( diff --git a/src/pages/workspace/WorkspacePage.tsx b/src/pages/workspace/WorkspacePage.tsx index 4fdaf2b..1843b43 100644 --- a/src/pages/workspace/WorkspacePage.tsx +++ b/src/pages/workspace/WorkspacePage.tsx @@ -14,8 +14,15 @@ import { useFileUpload } from '@/hooks/useFileUpload' import { useContentExtraction } from '@/hooks/useContentExtraction' import { useProcessingHistory } from '@/hooks/useProcessingHistory' import { useToast } from '@/hooks/useToast' -import { loadPreferences, persistPreferences, PreferenceKey } from '@/lib/storage/preferences' +import { + loadPreferences, + persistPreferences, + PreferenceKey, + preferencesStorage, +} from '@/lib/storage/preferences' import { FORMAT_OPTIONS, getFormatLabels } from '@/constants/formats' +import { LanguageSelector, type LanguageSelectorValue } from '@/components/input/LanguageSelector' +import { LANGUAGE_OPTIONS, findLanguageByCode } from '@/constants/languages' import type { FormatType } from '@/lib/chrome-ai/types' import type { ProcessingHistoryEntry, ProcessingHistorySource } from '@/lib/storage/history' import type { @@ -41,6 +48,8 @@ export const WorkspacePage = () => { const [completedFormats, setCompletedFormats] = useState>(new Set()) const [isStreaming, setIsStreaming] = useState(false) const [selectedFormats, setSelectedFormats] = useState(['paragraphs']) + const [targetLanguage, setTargetLanguage] = useState('en') + const [favoriteLanguages, setFavoriteLanguages] = useState([]) const [processingError, setProcessingError] = useState(null) const [extractedText, setExtractedText] = useState('') const [textInputKey, setTextInputKey] = useState(0) // Key to force re-render @@ -103,16 +112,44 @@ export const WorkspacePage = () => { const { showSuccess, showError } = useToast() - // Loaded but not yet used - will be used when language selector is added to workspace - const [, setTargetLanguage] = useState('en') const streamingResultsRef = useRef>>({}) const completedFormatsRef = useRef>(new Set()) + const isStreamingRef = useRef(false) // Load preferences on mount useEffect(() => { const prefs = loadPreferences() setSelectedFormats(prefs.formatSelection || ['paragraphs']) setTargetLanguage(prefs.targetLanguage || 'en') + setFavoriteLanguages(prefs.favoriteLanguages || []) + if (prefs.showComparison) { + setViewMode('comparison') + } + }, []) + + useEffect(() => { + const unsubscribeTarget = preferencesStorage.subscribe( + PreferenceKey.TargetLanguage, + ({ value }) => { + if (typeof value === 'string') { + setTargetLanguage(value) + } + } + ) + + const unsubscribeFavorites = preferencesStorage.subscribe( + PreferenceKey.FavoriteLanguages, + ({ value }) => { + if (Array.isArray(value)) { + setFavoriteLanguages(value) + } + } + ) + + return () => { + unsubscribeTarget?.() + unsubscribeFavorites?.() + } }, []) // File upload hook @@ -154,6 +191,7 @@ export const WorkspacePage = () => { setOriginalText('') setViewMode('output') setProcessingError(null) + isStreamingRef.current = false setIsStreaming(false) setStreamingResults({}) setCompletedFormats(new Set()) @@ -176,6 +214,7 @@ export const WorkspacePage = () => { setProcessedResults(null) setStreamingResults({}) setCompletedFormats(new Set()) + isStreamingRef.current = false setIsStreaming(false) setOperationProgress(null) setMultiFormatProgressMap(new Map()) @@ -198,14 +237,12 @@ export const WorkspacePage = () => { ) const updateStreamingResults = useCallback((format: FormatType, content: string) => { - console.log(`[WorkspacePage] updateStreamingResults: ${format}, content length=${content.length}`) streamingResultsRef.current = { ...streamingResultsRef.current, [format]: content, } - console.log(`[WorkspacePage] streamingResultsRef.current after update:`, Object.keys(streamingResultsRef.current), 'Lengths:', Object.fromEntries(Object.entries(streamingResultsRef.current).map(([k, v]) => [k, v?.length || 0]))) setStreamingResults((prev) => { if (prev[format] === content) { @@ -229,6 +266,7 @@ export const WorkspacePage = () => { const formats = selectedFormats.length > 0 ? selectedFormats : [] if (formats.length === 0) { + isStreamingRef.current = false setIsStreaming(false) setStreamingResults({}) setCompletedFormats(new Set()) @@ -240,13 +278,16 @@ export const WorkspacePage = () => { [formats[0]]: result, } + const preferences = loadPreferences() + addHistoryEntry({ source: lastProcessingSource, inputText: original, formats, results: resultsRecord, metadata: { - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: preferences.readingLevel || undefined, + targetLanguage, }, }) @@ -265,18 +306,14 @@ export const WorkspacePage = () => { results: Partial>, original: string ) => { - console.log('[WorkspacePage] Multi-format complete. Results:', Object.keys(results), 'Lengths:', Object.fromEntries(Object.entries(results).map(([k, v]) => [k, v?.length || 0]))) - console.log('[WorkspacePage] streamingResults state:', Object.keys(streamingResults), 'Lengths:', Object.fromEntries(Object.entries(streamingResults).map(([k, v]) => [k, v?.length || 0]))) - console.log('[WorkspacePage] streamingResultsRef.current:', Object.keys(streamingResultsRef.current), 'Lengths:', Object.fromEntries(Object.entries(streamingResultsRef.current).map(([k, v]) => [k, v?.length || 0]))) - // Merge streaming results ref with final results to avoid losing any content // Use ref instead of state because state might already be cleared const mergedResults = { ...streamingResultsRef.current, ...results } - console.log('[WorkspacePage] Merged results:', Object.keys(mergedResults), 'Lengths:', Object.fromEntries(Object.entries(mergedResults).map(([k, v]) => [k, v?.length || 0]))) setProcessedResults(mergedResults) setProcessedResult(null) setProcessingError(null) + isStreamingRef.current = false setIsStreaming(false) setStreamingResults({}) streamingResultsRef.current = {} @@ -294,13 +331,16 @@ export const WorkspacePage = () => { return } + const preferences = loadPreferences() + addHistoryEntry({ source: lastProcessingSource, inputText: original, formats, results: mergedResults, metadata: { - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: preferences.readingLevel || undefined, + targetLanguage, }, }) @@ -320,17 +360,18 @@ export const WorkspacePage = () => { const handleStreamingUpdate = useCallback( (format: FormatType, content: string, isComplete: boolean) => { - console.log(`[WorkspacePage] handleStreamingUpdate: format=${format}, isComplete=${isComplete}, content length=${content.length}`) - setIsStreaming(true) + // Use ref to prevent unnecessary state updates + if (!isStreamingRef.current) { + isStreamingRef.current = true + setIsStreaming(true) + } updateStreamingResults(format, content) if (isComplete) { - console.log(`[WorkspacePage] Format ${format} marked complete`) setCompletedFormats((prev) => { const next = new Set(prev) next.add(format) completedFormatsRef.current = next - console.log(`[WorkspacePage] Completed formats:`, Array.from(next)) return next }) } @@ -342,6 +383,7 @@ export const WorkspacePage = () => { setProcessingError(error) setProcessedResult(null) setProcessedResults(null) + isStreamingRef.current = false setIsStreaming(false) setStreamingResults({}) setCompletedFormats(new Set()) @@ -370,6 +412,7 @@ export const WorkspacePage = () => { setProcessingError(null) setStreamingResults({}) setCompletedFormats(new Set()) + isStreamingRef.current = false setIsStreaming(false) streamingResultsRef.current = {} completedFormatsRef.current = new Set() @@ -394,6 +437,27 @@ export const WorkspacePage = () => { }) } + const handleTargetLanguageChange = (language: LanguageSelectorValue) => { + if (language === 'auto') { + return + } + + setTargetLanguage(language) + persistPreferences({ + [PreferenceKey.TargetLanguage]: language, + }) + } + + const handleToggleFavoriteLanguage = (code: string) => { + setFavoriteLanguages((prev) => { + const next = prev.includes(code) ? prev.filter((item) => item !== code) : [...prev, code] + persistPreferences({ + [PreferenceKey.FavoriteLanguages]: next, + }) + return next + }) + } + const handleHistoryRestore = (entry: ProcessingHistoryEntry) => { const restoredFormats = entry.formats.length > 0 ? entry.formats : selectedFormats if (restoredFormats.length > 0) { @@ -403,6 +467,13 @@ export const WorkspacePage = () => { }) } + if (entry.metadata?.targetLanguage && entry.metadata.targetLanguage !== targetLanguage) { + setTargetLanguage(entry.metadata.targetLanguage) + persistPreferences({ + [PreferenceKey.TargetLanguage]: entry.metadata.targetLanguage, + }) + } + setExtractedText(entry.input.text) setTextInputKey((prev) => prev + 1) setInputMode('text') @@ -415,6 +486,7 @@ export const WorkspacePage = () => { setStreamingResults({}) setCompletedFormats(new Set()) setProcessingError(null) + isStreamingRef.current = false setIsStreaming(false) setLastProcessingSource(entry.source === 'unknown' ? 'text' : entry.source) if (hasSavedResults) { @@ -487,9 +559,11 @@ export const WorkspacePage = () => { handleProcessingStart() try { + const preferences = loadPreferences() const { FormatTransformService } = await import('@/lib/chrome-ai/services/FormatTransformService') const service = new FormatTransformService({ - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: preferences.readingLevel || undefined, + targetLanguage, }) // Initialize service @@ -498,7 +572,8 @@ export const WorkspacePage = () => { if (selectedFormats.length === 1) { // Single format processing const result = await service.transformToFormat(textContent, selectedFormats[0], { - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: preferences.readingLevel || undefined, + targetLanguage, }) handleProcessingComplete(result, textContent) @@ -510,7 +585,8 @@ export const WorkspacePage = () => { textContent, selectedFormats, { - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: preferences.readingLevel || undefined, + targetLanguage, } )) { if (update.isComplete) { @@ -577,9 +653,11 @@ export const WorkspacePage = () => { handleProcessingStart() try { + const preferences = loadPreferences() const { FormatTransformService } = await import('@/lib/chrome-ai/services/FormatTransformService') const service = new FormatTransformService({ - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: preferences.readingLevel || undefined, + targetLanguage, }) // Initialize service @@ -588,7 +666,8 @@ export const WorkspacePage = () => { if (selectedFormats.length === 1) { // Single format processing const result = await service.transformToFormat(textContent, selectedFormats[0], { - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: preferences.readingLevel || undefined, + targetLanguage, }) handleProcessingComplete(result, textContent) @@ -600,7 +679,8 @@ export const WorkspacePage = () => { textContent, selectedFormats, { - readingLevel: loadPreferences().readingLevel || undefined, + readingLevel: preferences.readingLevel || undefined, + targetLanguage, } )) { if (update.isComplete) { @@ -626,6 +706,7 @@ export const WorkspacePage = () => { setProcessingStartedAt(null) setLastSuccessMeta(null) setViewMode('output') + isStreamingRef.current = false setIsStreaming(false) streamingResultsRef.current = {} completedFormatsRef.current = new Set() @@ -713,10 +794,10 @@ export const WorkspacePage = () => { return (
{
    - {stepItems.map((step, index) => ( + {stepItems.map((step) => ( { ? 'border-synapse-200 bg-synapse-50/70 shadow-sm dark:border-synapse-500/40 dark:bg-synapse-500/10' : 'border-neutral-200 bg-white/70 dark:border-neutral-700 dark:bg-neutral-900/50' }`} - initial={{ opacity: 0, transform: 'translateY(18px)' }} + initial={{ opacity: 1, transform: 'translateY(0)' }} animate={{ opacity: 1, transform: 'translateY(0)' }} - transition={{ duration: 0.5, ease: 'easeOut', delay: 0.12 + index * 0.08 }} >
    {
    +
    +
    +
    +
    +

    Output language

    +

    + Applies to future transformations and updates your saved preference. +

    +
    + + {findLanguageByCode(targetLanguage)?.code ?? targetLanguage} + +
    + +
    +
    + {inputMode === 'text' ? ( { onProcessingError={handleProcessingError} initialText={extractedText} selectedFormats={selectedFormats} + targetLanguage={targetLanguage} onInputStateChange={setHasTextInput} onProcessingStart={handleProcessingStart} onOperationProgress={handleOperationProgress} @@ -1093,84 +1198,56 @@ export const WorkspacePage = () => {

- + - {fileUpload.files.length > 0 && ( - <> - + {fileUpload.files.length > 0 && ( + <> + -
- {!fileUpload.files.some((f) => f.status === 'complete') ? ( - - ) : ( - <> -
- - {selectedFormats.length === 0 && ( -

- ⚠️ Please select at least one output format above -

- )} -
- + + + Extract text + + ) : ( + <> +
- - )} + {selectedFormats.length === 0 && ( +

+ ⚠️ Please select at least one output format above +

+ )} +
- -
- - {fileUpload.files.some((f) => f.status === 'complete') && ( -
-
{ strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} - d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> -
-

- Text extraction complete -

-

- Extracted {fileUpload.getAllExtractedText().length.toLocaleString()} characters from {fileUpload.files.filter((f) => f.status === 'complete').length} file(s). Click "Use extracted text" to process. -

-
+ Edit as text + + + )} + + +
+ + {fileUpload.files.some((f) => f.status === 'complete') && ( +
+
+ +
+

+ Text extraction complete +

+

+ Extracted {fileUpload.getAllExtractedText().length.toLocaleString()} characters from {fileUpload.files.filter((f) => f.status === 'complete').length} file(s). Click "Use extracted text" to process. +

- )} - - )} +
+ )} + + )}
) : inputMode === 'writing-tools' ? (
-
-

- Chrome writing tools (preview) -

-

- Proofread drafts, adjust tone, or expand copy with Chrome's on-device AI. Pick a template or jump back into the text workspace to run a full transformation. -

-
-
- {writingToolHighlights.map((item) => ( -
-

- {item.title} -

-

{item.description}

-
- ))} -
-
- - -
+

+ {item.title} +

+

{item.description}

+
+ ))} +
+
+ + +
) : (
-
-

- Extract from URL -

-

- Enter a URL to extract and process article content -

-
- +
+

+ Extract from URL +

+

+ Enter a URL to extract and process article content +

+
+ - {content && ( -
- -
- )} + {content && ( +
+ +
+ )} - {!content && ( -
-

- What we extract -

-
    -
  • - - Main article text without ads or navigation -
  • -
  • - - Images with alt text and captions -
  • -
  • - - Article metadata (author, reading time, word count) -
  • -
  • - - Cleaned and sanitized HTML for security -
  • -
-
- )} + {!content && ( +
+

+ What we extract +

+
    +
  • + + Main article text without ads or navigation +
  • +
  • + + Images with alt text and captions +
  • +
  • + + Article metadata (author, reading time, word count) +
  • +
  • + + Cleaned and sanitized HTML for security +
  • +
+
+ )}
)}