From 8a7b8554ceb1abe997a02b452e76f26f0096c3fb Mon Sep 17 00:00:00 2001 From: Seshan Pillay Date: Fri, 31 Oct 2025 05:20:54 +0200 Subject: [PATCH 1/2] feat: general touch ups - Enhance dark mode support and improve UI responsiveness - Updated border colors and text styles in TextInputPanel for dark mode compatibility. - Removed planned demo cards from DemosPage to streamline content. - Refactored WorkspacePage to improve state management and UI transitions using framer-motion. - Added animations for history panel and info section to enhance user experience. - Improved accessibility and visual feedback for buttons and inputs. --- src/components/input/ExampleContentMenu.tsx | 117 ++-- src/components/input/TextInputPanel.tsx | 14 +- src/pages/DemosPage.tsx | 30 - src/pages/workspace/WorkspacePage.tsx | 704 +++++++++++--------- 4 files changed, 451 insertions(+), 414 deletions(-) diff --git a/src/components/input/ExampleContentMenu.tsx b/src/components/input/ExampleContentMenu.tsx index 9ca95b4..477a25a 100644 --- a/src/components/input/ExampleContentMenu.tsx +++ b/src/components/input/ExampleContentMenu.tsx @@ -1,4 +1,5 @@ import { useEffect, useId, useRef, useState } from 'react' +import { AnimatePresence, motion } from 'framer-motion' import { exampleContents, type ExampleContent } from '@/constants/exampleContent' interface ExampleContentMenuProps { @@ -21,10 +22,10 @@ export const ExampleContentMenu = ({ const getCategoryColor = (category: ExampleContent['category']) => { const colors = { - technical: 'bg-blue-100 text-blue-700', - academic: 'bg-purple-100 text-purple-700', - news: 'bg-green-100 text-green-700', - legal: 'bg-amber-100 text-amber-700', + technical: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-200', + academic: 'bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-200', + news: 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-200', + legal: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-200', } return colors[category] } @@ -143,7 +144,7 @@ export const ExampleContentMenu = ({ type="button" onClick={() => setIsOpen((prev) => !prev)} disabled={disabled} - className="flex items-center gap-2 rounded-xl border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 transition hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 focus-visible:ring-2 focus-visible:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-neutral-300 disabled:hover:bg-white disabled:hover:text-neutral-700" + className="flex items-center gap-2 rounded-xl border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 focus-visible:ring-2 focus-visible:ring-primary-500 dark:border-neutral-600 dark:bg-neutral-900/70 dark:text-neutral-200 dark:hover:border-primary-400 dark:hover:bg-primary-500/10 dark:hover:text-primary-200 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:translate-y-0" aria-haspopup="menu" aria-expanded={isOpen} aria-controls={isOpen ? menuId : undefined} @@ -164,7 +165,7 @@ export const ExampleContentMenu = ({ Load example - {isOpen && ( - - )} + + ))} + + + )} + ) } diff --git a/src/components/input/TextInputPanel.tsx b/src/components/input/TextInputPanel.tsx index 58ed73b..5fef5e6 100644 --- a/src/components/input/TextInputPanel.tsx +++ b/src/components/input/TextInputPanel.tsx @@ -284,12 +284,12 @@ export const TextInputPanel = ({ // Get border color based on validation state const getBorderColor = () => { if (validation && !validation.isValid && text.length > 0) { - return 'border-red-300 focus:border-red-500 focus:ring-red-500' + return 'border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-500/70 dark:focus:border-red-400 dark:focus:ring-red-400' } if (warningMessage) { - return 'border-orange-300 focus:border-orange-500 focus:ring-orange-500' + return 'border-orange-300 focus:border-orange-500 focus:ring-orange-500 dark:border-orange-500/60 dark:focus:border-orange-400 dark:focus:ring-orange-400' } - return 'border-neutral-300 focus:border-primary-500 focus:ring-primary-500' + return 'border-neutral-300 focus:border-primary-500 focus:ring-primary-500 dark:border-neutral-700 dark:focus:border-primary-400 dark:focus:ring-primary-400' } return ( @@ -298,14 +298,16 @@ export const TextInputPanel = ({
-

Input text

+

+ Input text +

Paste or type text that you want to rewrite or translate. Use ⌘/Ctrl + Enter to process and ⌘/Ctrl + K to clear. Synapse sanitizes pasted content to remove stray control characters automatically.
-

+

Paste or type the text you want to process. Maximum 10,000 words.

@@ -388,7 +390,7 @@ export const TextInputPanel = ({ onChange={(e) => setText(e.target.value)} disabled={isDisabled} placeholder="Paste or type your text here..." - className={`w-full rounded-xl border bg-white px-4 py-3 text-neutral-900 placeholder-neutral-400 transition focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:bg-neutral-50 disabled:text-neutral-500 ${getBorderColor()}`} + className={`w-full rounded-xl border bg-white px-4 py-3 text-neutral-900 placeholder-neutral-400 transition focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:bg-neutral-50 disabled:text-neutral-500 dark:bg-neutral-900/70 dark:text-neutral-100 dark:placeholder-neutral-500 dark:disabled:bg-neutral-900/40 dark:disabled:text-neutral-500 ${getBorderColor()}`} rows={12} aria-label="Text input" aria-invalid={validation && !validation.isValid ? 'true' : 'false'} diff --git a/src/pages/DemosPage.tsx b/src/pages/DemosPage.tsx index 0ec3aaa..1fccfb4 100644 --- a/src/pages/DemosPage.tsx +++ b/src/pages/DemosPage.tsx @@ -68,21 +68,6 @@ export const DemosPage = () => { delay={0.05} /> - - { ]} delay={0.2} /> - -
{ const [inputMode, setInputMode] = useState('text') @@ -366,8 +367,10 @@ export const WorkspacePage = () => { setViewMode('output') setLastSuccessMeta(null) setProcessingStartedAt(null) + setProcessingError(null) setStreamingResults({}) setCompletedFormats(new Set()) + setIsStreaming(false) streamingResultsRef.current = {} completedFormatsRef.current = new Set() } @@ -623,6 +626,9 @@ export const WorkspacePage = () => { setProcessingStartedAt(null) setLastSuccessMeta(null) setViewMode('output') + setIsStreaming(false) + streamingResultsRef.current = {} + completedFormatsRef.current = new Set() } // Clear extracted text when switching back to file mode @@ -634,12 +640,26 @@ export const WorkspacePage = () => { const hasFileInput = fileUpload.files.length > 0 const hasUrlContent = Boolean(content) || isLoading + + const hasExtractedFileText = fileUpload.files.some((file) => { + const extracted = file.result?.text + return file.status === 'complete' && typeof extracted === 'string' && extracted.trim().length > 0 + }) + const urlTextContent = content?.textContent + const hasUrlTextContent = + typeof urlTextContent === 'string' && urlTextContent.trim().length > 0 + const hasProcessableInput = + hasTextInput || + extractedText.trim().length > 0 || + hasExtractedFileText || + hasUrlTextContent + const hasGeneratedOutput = Boolean(processedResult) || Boolean(processedResults && Object.keys(processedResults).length > 0) || Object.keys(streamingResults).length > 0 const hasAnyInput = - hasTextInput || extractedText.trim().length > 0 || hasFileInput || hasUrlContent + hasProcessableInput || hasFileInput || hasUrlContent const activeStep = hasGeneratedOutput ? 3 : hasAnyInput ? 2 : 1 const formatSummary = @@ -799,6 +819,7 @@ export const WorkspacePage = () => { initial={{ opacity: 0, transform: 'translateY(18px)' }} animate={{ opacity: 1, transform: 'translateY(0)' }} transition={{ duration: 0.5, ease: 'easeOut', delay: 0.12 + index * 0.08 }} + layout >
{ Next: Add content )} - {step.id === 2 && !hasTextInput && ( + {step.id === 2 && !hasAnyInput && (

⚠️ Please add content in Step 1 first

)} - {step.id === 2 && hasTextInput && selectedFormats.length === 0 && ( + {step.id === 2 && hasAnyInput && selectedFormats.length === 0 && ( )} - {step.id === 2 && hasTextInput && selectedFormats.length > 0 && ( + {step.id === 2 && hasProcessableInput && selectedFormats.length > 0 && (

@@ -884,12 +905,12 @@ export const WorkspacePage = () => { Ready to process!

)} - {step.id === 3 && !hasTextInput && ( + {step.id === 3 && !hasProcessableInput && (

⚠️ Complete Steps 1 & 2 first

)} - {step.id === 3 && hasTextInput && ( + {step.id === 3 && inputMode === 'text' && hasTextInput && (
- + - {fileUpload.files.length > 0 && ( - <> - + {fileUpload.files.length > 0 && ( + <> + -
- {!fileUpload.files.some((f) => f.status === 'complete') ? ( - - ) : ( - <> -
+ + Extract text + + ) : ( + <> +
+ + {selectedFormats.length === 0 && ( +

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

+ )} +
+ - {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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> -
-

- 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. -

+ Clear all + +
+ + {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) => ( -
+

+ 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}

+
+ ))} +
+
+
- ))} -
-
- - -
+ + Try the sample workflow + + +
) : (
-
-

- Extract from URL -

-

- Enter a URL to extract and process article content -

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

- What we extract +

+

+ Extract from URL +

+

+ Enter a URL to extract and process article content

-
    -
  • - - 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 && ( +
+ +
+ )} + + {!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 +
  • +
+
+ )}
)}
{/* Output panel */} -
+
@@ -1393,58 +1417,54 @@ export const WorkspacePage = () => { )}
- {processingError ? ( - - ) : shouldShowProcessingState ? ( -
- 0 ? processingProgress : undefined} - showProgress={selectedFormats.length === 1} - operationProgress={selectedFormats.length === 1 ? operationProgress : null} - showStage={selectedFormats.length === 1} - showTimeRemaining={selectedFormats.length === 1} - > - {selectedFormats.length > 1 && ( -

- {completedFormats.size} of {selectedFormats.length} formats finished. -

+
+ {processingError ? ( + + ) : shouldShowProcessingState ? ( + <> + 0 ? processingProgress : undefined} + showProgress={selectedFormats.length === 1} + operationProgress={selectedFormats.length === 1 ? operationProgress : null} + showStage={selectedFormats.length === 1} + showTimeRemaining={selectedFormats.length === 1} + > + {selectedFormats.length > 1 && ( +

+ {completedFormats.size} of {selectedFormats.length} formats finished. +

+ )} +
+ + {selectedFormats.length > 1 && multiFormatProgressMap.size > 0 && ( + )} -
- - {/* Show multi-format progress when processing multiple formats */} - {selectedFormats.length > 1 && multiFormatProgressMap.size > 0 && ( - - )} -
- ) : null} - - {!processingError && !shouldShowProcessingState && lastSuccessMeta && ( - - - - } - message="Content transformed successfully!" - metrics={lastSuccessMeta} - /> - )} + + ) : !processingError && !shouldShowProcessingState && lastSuccessMeta ? ( + + + + } + message="Content transformed successfully!" + metrics={lastSuccessMeta} + /> + ) : null} +
- {(() => { - return null - })()} {hasStreamingContent ? ( { )}
-
+ {/* History Section with Toggle */} -
+

@@ -1570,19 +1590,35 @@ export const WorkspacePage = () => {

- {historyExpanded && ( - - )} -
+ + {historyExpanded && ( + + + + )} + +
{/* Info section */} -
+
{
-

- {inputMode === 'text' ? 'Keyboard shortcuts' : inputMode === 'file' ? 'Supported formats' : 'Note about CORS'} -

- {inputMode === 'text' ? ( -
-
- - ⌘ + Enter - - Process text -
-
- - ⌘ + K - - Clear input -
-
- ) : inputMode === 'file' ? ( -

- Supported: TXT, PDF, DOCX files up to 10MB. Files are processed securely with content validation and automatic retry for failed extractions. -

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

- Writing tools seed the text workspace with guided prompts so you can run a full Chrome AI transformation. - Load the sample workflow or return to the editor to start with your own content. -

- ) : ( -

- Some websites may block cross-origin requests due to CORS policies. If you - encounter errors, you can use a CORS proxy for development or install a browser - extension that enables CORS. -

- )} + + +

+ {inputMode === 'text' + ? 'Keyboard shortcuts' + : inputMode === 'file' + ? 'Supported formats' + : inputMode === 'writing-tools' + ? 'Writing tools tips' + : 'Note about CORS'} +

+ {inputMode === 'text' ? ( +
+
+ + ⌘ + Enter + + Process text +
+
+ + ⌘ + K + + Clear input +
+
+ ) : inputMode === 'file' ? ( +

+ Supported: TXT, PDF, DOCX files up to 10MB. Files are processed securely with content validation + and automatic retry for failed extractions. +

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

+ Writing tools seed the text workspace with guided prompts so you can run a full Chrome AI transformation. + Load the sample workflow or return to the editor to start with your own content. +

+ ) : ( +

+ Some websites may block cross-origin requests due to CORS policies. If you encounter errors, you can + use a CORS proxy for development or install a browser extension that enables CORS. +

+ )} +
+
-
+
) } From 08852b99c12acb5c437b1e5219a20dd9acbba7ed Mon Sep 17 00:00:00 2001 From: Seshan Pillay Date: Fri, 31 Oct 2025 05:29:26 +0200 Subject: [PATCH 2/2] refactor: update motion properties for improved animations and transitions --- src/components/input/ExampleContentMenu.tsx | 10 ++++------ src/lib/motion.tsx | 20 +++++++++++++++----- src/pages/workspace/WorkspacePage.tsx | 15 +++++---------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/components/input/ExampleContentMenu.tsx b/src/components/input/ExampleContentMenu.tsx index 477a25a..b1e0e41 100644 --- a/src/components/input/ExampleContentMenu.tsx +++ b/src/components/input/ExampleContentMenu.tsx @@ -183,9 +183,8 @@ export const ExampleContentMenu = ({ role="menu" aria-orientation="vertical" onKeyDown={handleMenuKeyDown} - initial={{ opacity: 0, y: -8, scale: 0.98 }} - animate={{ opacity: 1, y: 0, scale: 1 }} - exit={{ opacity: 0, y: -8, scale: 0.98 }} + initial={{ opacity: 0, transform: 'translateY(-8px) scale(0.98)' }} + animate={{ opacity: 1, transform: 'translateY(0) scale(1)' }} transition={{ duration: 0.16, ease: 'easeOut' }} >
@@ -207,9 +206,8 @@ export const ExampleContentMenu = ({ onFocus={() => setFocusedIndex(index)} className="w-full px-4 py-3 text-left transition-all duration-150 hover:bg-neutral-50 focus:bg-neutral-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 dark:hover:bg-neutral-800/70 dark:focus:bg-neutral-800/70" role="menuitem" - initial={{ opacity: 0, y: 4 }} - animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: 4 }} + initial={{ opacity: 0, transform: 'translateY(4px)' }} + animate={{ opacity: 1, transform: 'translateY(0)' }} transition={{ duration: 0.14, ease: 'easeOut' }} >
diff --git a/src/lib/motion.tsx b/src/lib/motion.tsx index fa4b355..7e1662e 100644 --- a/src/lib/motion.tsx +++ b/src/lib/motion.tsx @@ -5,8 +5,10 @@ import { useEffect, useMemo, useState, + type ButtonHTMLAttributes, type CSSProperties, type HTMLAttributes, + type MouseEvent as ReactMouseEvent, type ReactNode, } from 'react' import type { JSX } from 'react' @@ -20,7 +22,11 @@ type TransitionConfig = { type VariantStyles = CSSProperties type Variants = Record -type MotionProps = HTMLAttributes & { +type BaseElementProps = T extends HTMLButtonElement + ? ButtonHTMLAttributes + : HTMLAttributes + +type MotionProps = BaseElementProps & { initial?: VariantStyles | string animate?: VariantStyles | string whileHover?: VariantStyles @@ -97,8 +103,10 @@ const createMotionComponent = (element: keyof JSX.Intrins } as CSSProperties }, [transition?.duration, transition?.delay, transition?.ease]) - const handleMouseEnter: typeof onMouseEnter = (event) => { - onMouseEnter?.(event) + const handleMouseEnter = (event: ReactMouseEvent) => { + if (onMouseEnter) { + onMouseEnter(event as never) + } if (whileHover) { setCurrentStyle((prev) => ({ ...prev, @@ -107,8 +115,10 @@ const createMotionComponent = (element: keyof JSX.Intrins } } - const handleMouseLeave: typeof onMouseLeave = (event) => { - onMouseLeave?.(event) + const handleMouseLeave = (event: ReactMouseEvent) => { + if (onMouseLeave) { + onMouseLeave(event as never) + } if (whileHover && resolvedAnimate) { setCurrentStyle((prev) => ({ ...prev, diff --git a/src/pages/workspace/WorkspacePage.tsx b/src/pages/workspace/WorkspacePage.tsx index 0390386..4fdaf2b 100644 --- a/src/pages/workspace/WorkspacePage.tsx +++ b/src/pages/workspace/WorkspacePage.tsx @@ -819,7 +819,6 @@ export const WorkspacePage = () => { initial={{ opacity: 0, transform: 'translateY(18px)' }} animate={{ opacity: 1, transform: 'translateY(0)' }} transition={{ duration: 0.5, ease: 'easeOut', delay: 0.12 + index * 0.08 }} - layout >
{
{/* History Section with Toggle */} - +

@@ -1590,16 +1589,14 @@ export const WorkspacePage = () => {

- + {historyExpanded && ( { {/* Info section */}
@@ -1637,12 +1633,11 @@ export const WorkspacePage = () => {
- +