From 11b7d9b7a407f3017bdb9b4a9aa0c8ce399409f5 Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 31 May 2026 18:50:46 -0700 Subject: [PATCH] [eric] perf+ui: shrink runs chunks in parallel (5min -> ~1min) and shows a live pulsing label so users know its working --- backend/apps/settings/settings.py | 13 +++++-- .../ChatInput/view/ChatInputOverlays.tsx | 38 +++++++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/backend/apps/settings/settings.py b/backend/apps/settings/settings.py index acaac7b6..da747077 100644 --- a/backend/apps/settings/settings.py +++ b/backend/apps/settings/settings.py @@ -528,10 +528,15 @@ async def summarize_file(req: _SummarizeRequest): else: chunks = [raw[i:i + CHUNK_CHARS] for i in range(0, len(raw), CHUNK_CHARS)] per_chunk_budget = max(800, req.target_tokens // len(chunks) + 600) - partials = [] - for i, ch in enumerate(chunks): - label = f"{os.path.basename(src)} (part {i + 1} of {len(chunks)})" - partials.append(await _summarize_block(ch, per_chunk_budget, label)) + # Parallel summarization. Sequential was N chunks * ~60s each + # (5+ min wall time for a 4-chunk PDF on Haiku). Aux providers + # all handle parallel requests fine; the only ceiling is the + # provider's per-key rate limit, and a single user summarizing + # one file will never hit that. + partials = await asyncio.gather(*[ + _summarize_block(ch, per_chunk_budget, f"{os.path.basename(src)} (part {i + 1} of {len(chunks)})") + for i, ch in enumerate(chunks) + ]) merge_input = "\n\n".join(f"## Part {i + 1}\n{p}" for i, p in enumerate(partials)) summary = await _summarize_block(merge_input, req.target_tokens, f"merged summary of {os.path.basename(src)}") except Exception as e: diff --git a/frontend/src/app/pages/AgentChat/ChatInput/view/ChatInputOverlays.tsx b/frontend/src/app/pages/AgentChat/ChatInput/view/ChatInputOverlays.tsx index b7e22e08..7d80e680 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/view/ChatInputOverlays.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput/view/ChatInputOverlays.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; import Modal from '@mui/material/Modal'; @@ -6,6 +6,37 @@ import Snackbar from '@mui/material/Snackbar'; import CloseIcon from '@mui/icons-material/Close'; import { ClaudeTokens } from '@/shared/styles/claudeTokens'; +const SHRINK_PROGRESS_MESSAGES = [ + 'Reading the file', + 'Pulling out the key bits', + 'Compressing', + 'Almost done', +]; + +function ShrinkingLabel() { + const [idx, setIdx] = useState(0); + useEffect(() => { + const t = setInterval(() => setIdx((i) => (i + 1) % SHRINK_PROGRESS_MESSAGES.length), 4000); + return () => clearInterval(t); + }, []); + return ( + + + + {SHRINK_PROGRESS_MESSAGES[idx]}… + + + ); +} + interface Props { c: ClaudeTokens; lightboxSrc: string | null; @@ -100,11 +131,12 @@ export const ChatInputOverlays: React.FC = ({ border: 'none', borderRadius: '6px', px: 1.5, py: 0.7, fontSize: '0.82rem', fontWeight: 500, cursor: 'pointer', whiteSpace: 'nowrap', + transition: 'background 0.15s ease, opacity 0.15s ease', '&:hover': { bgcolor: c.accent.hover }, - '&:disabled': { opacity: 0.6, cursor: 'wait' }, + '&:disabled': { opacity: 0.85, cursor: 'wait', bgcolor: c.accent.primary }, }} > - {summarizingPath === oversizeQueue[0]?.path ? 'Shrinking…' : 'Shrink it'} + {summarizingPath === oversizeQueue[0]?.path ? : 'Shrink it'}