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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user