feat: Add Stop/Cancel buttons for long-running AI operations

Resolved 💬 3 comments Opened Dec 30, 2025 by smartwhale8 Closed Feb 15, 2026

Summary

Add the ability for users to cancel/stop long-running operations in the chat interface. This improves UX when operations get stuck, take too long, or when the user changes their mind.

Problem

Currently, when a user initiates an operation (document upload, LLM query, entity extraction), there's no way to cancel it:

  1. Stuck operations: If processing fails silently (e.g., database error), the UI shows a spinner indefinitely
  2. Wrong input: User uploads wrong document but must wait for full processing
  3. Slow LLM responses: No way to abort a slow or stuck LLM call
  4. Changed mind: User wants to try a different approach but can't cancel current operation

Real-world example: Document intake analysis failed due to missing database column, but the frontend kept polling indefinitely with no way for the user to cancel or retry.

Proposed Solution

1. Cancel Button for Document Processing

┌─────────────────────────────────────────────────┐
│  📄 Analyzing document...                       │
│                                                 │
│  ✓ Uploading document                           │
│  ✓ Extracting text content                      │
│  ◐ Analyzing document type                      │
│  ○ Extracting basic case info                   │
│                                                 │
│  [Cancel]                                       │
└─────────────────────────────────────────────────┘

Behavior:

  • Stops frontend polling
  • Marks document as processing_cancelled in database
  • Shows "Processing cancelled" message with retry option
  • Document remains uploaded (text extraction may be complete)

2. Stop Button for LLM Streaming

┌─────────────────────────────────────────────────┐
│  🤖 Lexinox is typing...                        │
│                                                 │
│  Based on the document you provided, I can see  │
│  that this appears to be a criminal case        │
│  involving...█                                  │
│                                                 │
│  [■ Stop generating]                            │
└─────────────────────────────────────────────────┘

Behavior:

  • Aborts the streaming response
  • Keeps partial response visible
  • Shows "[Response stopped]" indicator
  • User can send new message

3. Timeout with Auto-Cancel

┌─────────────────────────────────────────────────┐
│  ⚠️ This is taking longer than expected         │
│                                                 │
│  Document analysis has been running for 60s.    │
│  This might indicate a problem.                 │
│                                                 │
│  [Keep waiting]  [Cancel and retry]             │
└─────────────────────────────────────────────────┘

Behavior:

  • Shows after configurable timeout (e.g., 60s)
  • User chooses to wait or cancel
  • Provides context about what might be wrong

Technical Implementation

Frontend Changes

  1. New state management:

``javascript
const [processingState, setProcessingState] = useState({
isProcessing: false,
canCancel: true,
operationType: null, // 'document_upload', 'llm_stream', 'entity_extraction'
startTime: null,
})
``

  1. Cancel handler:

```javascript
const handleCancel = async () => {
// Stop polling/streaming
abortControllerRef.current?.abort()

// Notify backend (optional)
if (processingState.taskId) {
await api.post(/tasks/${processingState.taskId}/cancel)
}

// Reset UI
setProcessingState({ isProcessing: false, ... })
showToast('Operation cancelled')
}
```

  1. AbortController integration:

```javascript
const abortControllerRef = useRef(null)

const startOperation = async () => {
abortControllerRef.current = new AbortController()

try {
await fetch(url, { signal: abortControllerRef.current.signal })
} catch (e) {
if (e.name === 'AbortError') {
// User cancelled - handle gracefully
}
}
}
```

Backend Changes (Optional, for full cancellation)

  1. Task tracking endpoint:

``python
@router.post("/tasks/{task_id}/cancel")
async def cancel_task(task_id: str):
# Mark task as cancelled in database
# Stop any background processing if possible
``

  1. Document status update:

``python
class DocumentProcessingStatus:
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled" # New status
``

Affected Components

Frontend

  • frontend/src/components/ChatInterface.jsx - Main chat with stop button
  • frontend/src/components/DocumentUploadProgress.jsx - Cancel during upload
  • frontend/src/components/IntakeProgressTracker.jsx - Cancel intake analysis
  • frontend/src/hooks/useLLMStream.js - Abort streaming (new hook)

Backend

  • app/api/routes/documents.py - Cancel endpoint
  • app/services/document_service.py - Handle cancellation
  • app/models/database.py - Add CANCELLED status

UI/UX Considerations

  1. Button placement: Stop button should be prominent but not accidentally clickable
  2. Confirmation: No confirmation needed for stop (reversible action)
  3. Feedback: Clear indication that operation was cancelled
  4. Recovery: Easy path to retry or try something else
  5. Partial results: Show any partial results (e.g., extracted text even if analysis cancelled)

Acceptance Criteria

  • [ ] Cancel button visible during document processing
  • [ ] Stop button visible during LLM streaming responses
  • [ ] Clicking cancel/stop immediately stops the operation
  • [ ] UI shows clear feedback that operation was cancelled
  • [ ] User can retry or proceed with different action
  • [ ] Timeout warning appears for operations exceeding threshold
  • [ ] Partial results preserved when possible
  • [ ] No orphaned background processes

Priority

Medium - This is a quality-of-life improvement that significantly improves UX for edge cases and error scenarios.

Related

  • Document intake processing flow
  • LLM streaming responses
  • Error handling and recovery

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗