diff --git a/src/components/input/ExampleContentMenu.tsx b/src/components/input/ExampleContentMenu.tsx
index 9ca95b4..b1e0e41 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/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/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 =
@@ -848,12 +868,12 @@ export const WorkspacePage = () => {
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 && (
{
@@ -876,7 +896,7 @@ export const WorkspacePage = () => {
Next: Select formats
)}
- {step.id === 2 && hasTextInput && selectedFormats.length > 0 && (
+ {step.id === 2 && hasProcessableInput && selectedFormats.length > 0 && (
@@ -884,12 +904,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 && (
{
@@ -1073,56 +1093,84 @@ export const WorkspacePage = () => {
-
+
- {fileUpload.files.length > 0 && (
- <>
-
+ {fileUpload.files.length > 0 && (
+ <>
+
-
- {!fileUpload.files.some((f) => f.status === 'complete') ? (
-
f.status !== 'pending')
- }
- className="flex items-center gap-2 rounded-xl bg-synapse-600 px-6 py-3 text-sm font-semibold text-white shadow-brand transition hover:bg-synapse-700 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-synapse-600"
- >
-
+ {!fileUpload.files.some((f) => f.status === 'complete') ? (
+ f.status !== 'pending')
+ }
+ className="flex items-center gap-2 rounded-xl bg-synapse-600 px-6 py-3 text-sm font-semibold text-white shadow-brand transition hover:bg-synapse-700 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-synapse-600"
>
-
-
- Extract text
-
- ) : (
- <>
-
+
+
+
+
Extract text
+
+ ) : (
+ <>
+
+
+
+
+
+ Transform with Chrome AI
+
+ {selectedFormats.length === 0 && (
+
+ ⚠️ Please select at least one output format above
+
+ )}
+
+
{
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
- d="M13 10V3L4 14h7v7l9-11h-7z"
+ 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"
/>
- Transform with Chrome AI
+ Edit as text
- {selectedFormats.length === 0 && (
-
- ⚠️ Please select at least one output format above
-
- )}
-
+ >
+ )}
-
-
-
-
- Edit as text
-
- >
- )}
-
-
-
-
-
- Clear all
-
-
-
- {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}
+
+ ))}
+
+
+
{
+ resetWorkspaceOutputs()
+ setInputMode('text')
+ setExtractedText(DEFAULT_SAMPLE_TEXT)
+ setHasTextInput(true)
+ setTextInputKey((prev) => prev + 1)
+ }}
+ className="inline-flex items-center gap-2 rounded-xl bg-synapse-600 px-4 py-2 text-sm font-semibold text-white shadow-brand transition hover:-translate-y-0.5 hover:bg-synapse-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-synapse-500"
>
-
- {item.title}
-
- {item.description}
-
- ))}
-
-
-
{
- resetWorkspaceOutputs()
- setInputMode('text')
- setExtractedText(DEFAULT_SAMPLE_TEXT)
- setHasTextInput(true)
- setTextInputKey((prev) => prev + 1)
- }}
- className="inline-flex items-center gap-2 rounded-xl bg-synapse-600 px-4 py-2 text-sm font-semibold text-white shadow-brand transition hover:-translate-y-0.5 hover:bg-synapse-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-synapse-500"
- >
-
-
-
- Try the sample workflow
-
-
{
- resetWorkspaceOutputs()
- setInputMode('text')
- setOriginalText('')
- setHasTextInput(false)
- }}
- className="inline-flex items-center gap-2 rounded-xl border border-synapse-200 bg-white/80 px-4 py-2 text-sm font-semibold text-synapse-700 transition hover:-translate-y-0.5 hover:border-synapse-300 hover:bg-synapse-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-synapse-500 dark:border-synapse-500/40 dark:text-synapse-200 dark:hover:border-synapse-400/60 dark:hover:bg-neutral-900/60"
- >
- Return to workspace editor
-
-
+
+
+
+ Try the sample workflow
+
+
{
+ resetWorkspaceOutputs()
+ setInputMode('text')
+ setOriginalText('')
+ setHasTextInput(false)
+ }}
+ className="inline-flex items-center gap-2 rounded-xl border border-synapse-200 bg-white/80 px-4 py-2 text-sm font-semibold text-synapse-700 transition hover:-translate-y-0.5 hover:border-synapse-300 hover:bg-synapse-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-synapse-500 dark:border-synapse-500/40 dark:text-synapse-200 dark:hover:border-synapse-400/60 dark:hover:bg-neutral-900/60"
+ >
+ Return to workspace editor
+
+
) : (
-
-
- 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 +1416,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 +1589,32 @@ 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.
+
+ )}
+
+
-
+
)
}