feat: add task cancellation feature (task-management)

- Backend: Add API endpoint to cancel tasks and mark status as cancelled
- Frontend: Add cancel button to task management UI (visible only for cancellable tasks)
- Process: Implement cancellation check during generation to abort in-progress tasks
- UI: Update status display styles with a visual indicator for the cancelled state
This commit is contained in:
p
2026-01-17 18:47:54 +08:00
parent 0b47770683
commit de6fe4b294
3 changed files with 66 additions and 2 deletions
+23
View File
@@ -164,6 +164,11 @@ def generate_storyboard_task(task_id: str, project_id: str, user_input: str):
story_blocks = [b for b in json_blocks if b.get("type") not in ["character_sheet", "comic_config"]] story_blocks = [b for b in json_blocks if b.get("type") not in ["character_sheet", "comic_config"]]
# --- Missing Character Check & Fix --- # --- Missing Character Check & Fix ---
session.refresh(task)
if task.status == "cancelled":
log_task_event(session, task_id, "Task execution cancelled by user.")
return
story_char_names = set() story_char_names = set()
for block in story_blocks: for block in story_blocks:
chars = block.get("characters", []) chars = block.get("characters", [])
@@ -303,6 +308,12 @@ def generate_all_images_task(task_id: str, project_id: str):
total_chars = len(project.characters) total_chars = len(project.characters)
log_task_event(session, task_id, f"Generating {total_chars} characters...") log_task_event(session, task_id, f"Generating {total_chars} characters...")
for i, char in enumerate(project.characters): for i, char in enumerate(project.characters):
# Check for cancellation
session.refresh(task)
if task.status == "cancelled":
log_task_event(session, task_id, "Task execution cancelled by user.")
return
if char.image_url: if char.image_url:
log_task_event(session, task_id, f"Character {char.name} already has image, skipping.") log_task_event(session, task_id, f"Character {char.name} already has image, skipping.")
continue continue
@@ -353,6 +364,12 @@ def generate_all_images_task(task_id: str, project_id: str):
# For "one click", let's assume we scan all items. # For "one click", let's assume we scan all items.
for i, item in enumerate(items): for i, item in enumerate(items):
# Check for cancellation
session.refresh(task)
if task.status == "cancelled":
log_task_event(session, task_id, "Task execution cancelled by user.")
return
# Update progress at start of loop # Update progress at start of loop
# task.progress = int((i / total_items) * 100) # task.progress = int((i / total_items) * 100)
# session.add(task) # session.add(task)
@@ -479,6 +496,12 @@ def generate_all_characters_task(task_id: str, project_id: str):
log_task_event(session, task_id, f"Generating {total_chars} characters...") log_task_event(session, task_id, f"Generating {total_chars} characters...")
for i, char in enumerate(project.characters): for i, char in enumerate(project.characters):
# Check for cancellation
session.refresh(task)
if task.status == "cancelled":
log_task_event(session, task_id, "Task execution cancelled by user.")
return
# if char.image_url: # if char.image_url:
# logger.info(f"Character {char.name} already has image, skipping.") # logger.info(f"Character {char.name} already has image, skipping.")
# continue # continue
+16
View File
@@ -21,3 +21,19 @@ def get_project_tasks(project_id: str, session: Session = Depends(get_session)):
# Filter only recent or active tasks if list is too long? # Filter only recent or active tasks if list is too long?
# For now return all, maybe limit 20 # For now return all, maybe limit 20
return tasks[:20] return tasks[:20]
@router.post("/{task_id}/cancel", response_model=TaskRead)
def cancel_task(task_id: str, session: Session = Depends(get_session)):
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
if task.status in ["completed", "failed", "cancelled"]:
return task
task.status = "cancelled"
task.message = "Task cancelled by user"
session.add(task)
session.commit()
session.refresh(task)
return task
+27 -2
View File
@@ -15,6 +15,16 @@
</span> </span>
</div> </div>
<div class="status-group"> <div class="status-group">
<el-button
v-if="['pending', 'processing'].includes(task.status)"
link
size="small"
type="danger"
@click.stop="cancelTask(task.id)"
title="Cancel Task"
>
<el-icon><CircleClose /></el-icon>
</el-button>
<el-button link size="small" @click.stop="openTerminal(task.id)" title="View Logs"> <el-button link size="small" @click.stop="openTerminal(task.id)" title="View Logs">
<el-icon><Monitor /></el-icon> <el-icon><Monitor /></el-icon>
</el-button> </el-button>
@@ -29,7 +39,9 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { ArrowUp, ArrowDown, Monitor } from '@element-plus/icons-vue' import { ArrowUp, ArrowDown, Monitor, CircleClose } from '@element-plus/icons-vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'
const props = defineProps({ const props = defineProps({
tasks: { tasks: {
@@ -56,6 +68,16 @@ const openTerminal = (taskId) => {
emit('open-terminal', taskId) emit('open-terminal', taskId)
} }
const cancelTask = async (taskId) => {
try {
await axios.post(`/api/v1/tasks/${taskId}/cancel`)
ElMessage.warning('Task cancellation requested')
} catch (e) {
console.error(e)
ElMessage.error('Failed to cancel task')
}
}
const getTaskTypeName = (type) => { const getTaskTypeName = (type) => {
const map = { const map = {
'storyboard': 'Storyboard Generation', 'storyboard': 'Storyboard Generation',
@@ -70,7 +92,8 @@ const getTaskStatusText = (status) => {
'pending': 'Pending', 'pending': 'Pending',
'processing': 'Processing', 'processing': 'Processing',
'completed': 'Completed', 'completed': 'Completed',
'failed': 'Failed' 'failed': 'Failed',
'cancelled': 'Cancelled'
} }
return map[status] || status return map[status] || status
} }
@@ -78,6 +101,7 @@ const getTaskStatusText = (status) => {
const getTaskProgressStatus = (status) => { const getTaskProgressStatus = (status) => {
if (status === 'completed') return 'success' if (status === 'completed') return 'success'
if (status === 'failed') return 'exception' if (status === 'failed') return 'exception'
if (status === 'cancelled') return 'warning'
return '' return ''
} }
</script> </script>
@@ -173,4 +197,5 @@ const getTaskProgressStatus = (status) => {
.task-status.processing { color: #409EFF; } .task-status.processing { color: #409EFF; }
.task-status.completed { color: #67C23A; } .task-status.completed { color: #67C23A; }
.task-status.failed { color: #F56C6C; } .task-status.failed { color: #F56C6C; }
.task-status.cancelled { color: #E6A23C; }
</style> </style>