Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 173 additions & 2 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,42 @@ import SettingsPanel from './components/SettingsPanel'
import PDFPanel from './components/PDFPanel'
import OutputPanel from './components/OutputPanel'

const SIDEBAR_WIDTH_KEY = 'paper2corecode.sidebarWidth'
const UPLOAD_HEIGHT_KEY = 'paper2corecode.uploadHeight'
const SIDEBAR_DEFAULT_WIDTH = 280
const SIDEBAR_MIN_WIDTH = 220
const SIDEBAR_MAX_WIDTH = 420
const MAIN_MIN_WIDTH = 520
const UPLOAD_DEFAULT_HEIGHT = 260
const UPLOAD_MIN_HEIGHT = 230
const UPLOAD_MAX_HEIGHT = 420
const RESULT_MIN_HEIGHT = 260
const RESIZE_HANDLE_SIZE = 16

function clamp(value: number, min: number, max: number): number {
if (max < min) return min
return Math.min(Math.max(value, min), max)
}

function readStoredNumber(key: string, fallback: number): number {
let stored: string | null = null
try {
stored = window.localStorage.getItem(key)
} catch {
return fallback
}

if (!stored) return fallback

const parsed = Number(stored)
return Number.isFinite(parsed) ? parsed : fallback
}

function getContentWidth(element: HTMLElement): number {
const styles = window.getComputedStyle(element)
return element.clientWidth - parseFloat(styles.paddingLeft) - parseFloat(styles.paddingRight)
}

export default function App() {
const [pdfPath, setPdfPath] = useState<string | null>(null)
const [analyzing, setAnalyzing] = useState(false)
Expand All @@ -19,7 +55,19 @@ export default function App() {
const [apiKeyConfigured, setApiKeyConfigured] = useState(false)
const [language, setLanguage] = useState<Language>('zh-CN')
const [showCompletionDialog, setShowCompletionDialog] = useState(false)
const [sidebarWidth, setSidebarWidth] = useState(() => clamp(
readStoredNumber(SIDEBAR_WIDTH_KEY, SIDEBAR_DEFAULT_WIDTH),
SIDEBAR_MIN_WIDTH,
SIDEBAR_MAX_WIDTH
))
const [uploadHeight, setUploadHeight] = useState(() => clamp(
readStoredNumber(UPLOAD_HEIGHT_KEY, UPLOAD_DEFAULT_HEIGHT),
UPLOAD_MIN_HEIGHT,
UPLOAD_MAX_HEIGHT
))
const cleanupRef = useRef<(() => void) | null>(null)
const appBodyRef = useRef<HTMLDivElement | null>(null)
const mainContentRef = useRef<HTMLDivElement | null>(null)

useEffect(() => {
window.electronAPI.getSettings().then((s) => {
Expand All @@ -42,6 +90,33 @@ export default function App() {
return () => window.clearInterval(timer)
}, [startedAt, finishedAt])

useEffect(() => {
const clampLayoutToViewport = () => {
const appBody = appBodyRef.current
if (appBody) {
const maxSidebarWidth = Math.max(
SIDEBAR_MIN_WIDTH,
Math.min(SIDEBAR_MAX_WIDTH, getContentWidth(appBody) - RESIZE_HANDLE_SIZE - MAIN_MIN_WIDTH)
)
setSidebarWidth((width) => clamp(width, SIDEBAR_MIN_WIDTH, maxSidebarWidth))
}

const mainContent = mainContentRef.current
if (mainContent) {
const maxUploadHeight = Math.max(
UPLOAD_MIN_HEIGHT,
Math.min(UPLOAD_MAX_HEIGHT, mainContent.clientHeight - RESIZE_HANDLE_SIZE - RESULT_MIN_HEIGHT)
)
setUploadHeight((height) => clamp(height, UPLOAD_MIN_HEIGHT, maxUploadHeight))
}
}

clampLayoutToViewport()
window.addEventListener('resize', clampLayoutToViewport)

return () => window.removeEventListener('resize', clampLayoutToViewport)
}, [])

const resetAnalysisMeta = () => {
setAnalysisStatus('idle')
setElapsedTime(0)
Expand Down Expand Up @@ -158,6 +233,78 @@ export default function App() {
await window.electronAPI.saveSettings({ language: lang })
}

const handleSidebarResizeStart = (event: React.PointerEvent<HTMLDivElement>) => {
const appBody = appBodyRef.current
if (!appBody) return

event.preventDefault()
const startX = event.clientX
const startWidth = sidebarWidth
const maxSidebarWidth = Math.max(
SIDEBAR_MIN_WIDTH,
Math.min(SIDEBAR_MAX_WIDTH, getContentWidth(appBody) - RESIZE_HANDLE_SIZE - MAIN_MIN_WIDTH)
)

document.body.classList.add('is-resizing-layout', 'is-resizing-column')

const handlePointerMove = (moveEvent: PointerEvent) => {
const nextWidth = clamp(startWidth + moveEvent.clientX - startX, SIDEBAR_MIN_WIDTH, maxSidebarWidth)
setSidebarWidth(nextWidth)
}

const handlePointerUp = (upEvent: PointerEvent) => {
const nextWidth = clamp(startWidth + upEvent.clientX - startX, SIDEBAR_MIN_WIDTH, maxSidebarWidth)
setSidebarWidth(nextWidth)
try {
window.localStorage.setItem(SIDEBAR_WIDTH_KEY, String(Math.round(nextWidth)))
} catch {}
document.body.classList.remove('is-resizing-layout', 'is-resizing-column')
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', handlePointerUp)
window.removeEventListener('pointercancel', handlePointerUp)
}

window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', handlePointerUp)
window.addEventListener('pointercancel', handlePointerUp)
}

const handleUploadResizeStart = (event: React.PointerEvent<HTMLDivElement>) => {
const mainContent = mainContentRef.current
if (!mainContent) return

event.preventDefault()
const startY = event.clientY
const startHeight = uploadHeight
const maxUploadHeight = Math.max(
UPLOAD_MIN_HEIGHT,
Math.min(UPLOAD_MAX_HEIGHT, mainContent.clientHeight - RESIZE_HANDLE_SIZE - RESULT_MIN_HEIGHT)
)

document.body.classList.add('is-resizing-layout', 'is-resizing-row')

const handlePointerMove = (moveEvent: PointerEvent) => {
const nextHeight = clamp(startHeight + moveEvent.clientY - startY, UPLOAD_MIN_HEIGHT, maxUploadHeight)
setUploadHeight(nextHeight)
}

const handlePointerUp = (upEvent: PointerEvent) => {
const nextHeight = clamp(startHeight + upEvent.clientY - startY, UPLOAD_MIN_HEIGHT, maxUploadHeight)
setUploadHeight(nextHeight)
try {
window.localStorage.setItem(UPLOAD_HEIGHT_KEY, String(Math.round(nextHeight)))
} catch {}
document.body.classList.remove('is-resizing-layout', 'is-resizing-row')
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', handlePointerUp)
window.removeEventListener('pointercancel', handlePointerUp)
}

window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', handlePointerUp)
window.addEventListener('pointercancel', handlePointerUp)
}

const progressMessages = progress.map((p) => p.message)

return (
Expand Down Expand Up @@ -196,15 +343,31 @@ export default function App() {
</div>
</header>

<div className='app-body'>
<div
ref={appBodyRef}
className='app-body'
style={{ gridTemplateColumns: `${sidebarWidth}px ${RESIZE_HANDLE_SIZE}px minmax(0, 1fr)` }}
>
<aside className='sidebar'>
<SettingsPanel
language={language}
onApiKeyConfiguredChange={setApiKeyConfigured}
/>
</aside>

<div className='main-content'>
<div
className='resize-handle resize-handle-vertical'
role='separator'
aria-orientation='vertical'
aria-label='Resize sidebar'
onPointerDown={handleSidebarResizeStart}
/>

<div
ref={mainContentRef}
className='main-content'
style={{ gridTemplateRows: `${uploadHeight}px ${RESIZE_HANDLE_SIZE}px minmax(0, 1fr)` }}
>
<div className='workspace'>
<PDFPanel
pdfPath={pdfPath}
Expand All @@ -220,6 +383,14 @@ export default function App() {
/>
</div>

<div
className='resize-handle resize-handle-horizontal'
role='separator'
aria-orientation='horizontal'
aria-label='Resize upload and summary panels'
onPointerDown={handleUploadResizeStart}
/>

<div className='result-viewer'>
<OutputPanel
result={result}
Expand Down
7 changes: 6 additions & 1 deletion src/renderer/components/PDFPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const cardStyle: React.CSSProperties = {
background: 'var(--color-surface)',
borderRadius: 'var(--radius-md)',
padding: 18,
height: '100%',
minHeight: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
gap: 14,
Expand All @@ -33,6 +36,8 @@ const dropBase: React.CSSProperties = {
border: '2px dashed var(--color-border)',
background: 'transparent',
minHeight: 108,
flex: 1,
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
Expand Down Expand Up @@ -181,7 +186,7 @@ export default function PDFPanel({ pdfPath, analyzing, progress, apiKeyConfigure
)}
</div>

<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', flexShrink: 0 }}>
{!apiKeyConfigured && pdfPath && (
<span style={{ fontSize: 13, color: 'var(--color-warning)', marginRight: 'auto' }}>
{t(language, 'upload.setApiKeyHint')}
Expand Down
78 changes: 70 additions & 8 deletions src/renderer/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,7 @@ body {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
align-items: stretch;
gap: 16px;
padding: 18px;
overflow: hidden;
}
Expand All @@ -115,26 +113,90 @@ body {
}

.main-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 0;
display: grid;
overflow: hidden;
}

.workspace {
flex-shrink: 0;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}

.result-viewer {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}

.resize-handle {
position: relative;
flex-shrink: 0;
touch-action: none;
user-select: none;
-webkit-app-region: no-drag;
}

.resize-handle::after {
content: '';
position: absolute;
border-radius: 999px;
background: var(--color-border-strong);
opacity: 0;
transition: opacity 0.15s, background 0.15s;
}

.resize-handle:hover::after,
.is-resizing-column .resize-handle-vertical::after,
.is-resizing-row .resize-handle-horizontal::after {
opacity: 1;
background: var(--color-accent);
}

.resize-handle-vertical {
cursor: col-resize;
}

.resize-handle-vertical::after {
top: 8px;
bottom: 8px;
left: 50%;
width: 2px;
transform: translateX(-50%);
}

.resize-handle-horizontal {
cursor: row-resize;
}

.resize-handle-horizontal::after {
left: 8px;
right: 8px;
top: 50%;
height: 2px;
transform: translateY(-50%);
}

.is-resizing-layout {
user-select: none;
}

.is-resizing-column {
cursor: col-resize;
}

.is-resizing-row {
cursor: row-resize;
}

.is-resizing-layout * {
user-select: none !important;
}

.markdown-table-frame {
display: flex;
justify-content: center;
Expand Down