diff --git a/review_agent/file_summary/views.py b/review_agent/file_summary/views.py index 8be64f3..680d4a3 100644 --- a/review_agent/file_summary/views.py +++ b/review_agent/file_summary/views.py @@ -229,6 +229,7 @@ def batch_status(request, batch_id: int): { "batch": { "id": batch.pk, + "workflow_type": "file_summary", "batch_no": batch.batch_no, "status": batch.status, "product_name": batch.product_name, diff --git a/review_agent/regulatory_review/views.py b/review_agent/regulatory_review/views.py index 1842487..d51a249 100644 --- a/review_agent/regulatory_review/views.py +++ b/review_agent/regulatory_review/views.py @@ -26,6 +26,7 @@ def batch_status(request, batch_id: int): "status": batch.status, "source_summary_batch_id": batch.source_summary_batch_id, "risk_summary": batch.risk_summary, + "risk_summary_text": _format_risk_summary(batch.risk_summary or {}), "error_message": batch.error_message, }, "nodes": [ @@ -40,3 +41,18 @@ def batch_status(request, batch_id: int): ], } ) + + +def _format_risk_summary(risk_summary: dict) -> str: + labels = [ + ("blocking", "阻断项"), + ("high", "高风险"), + ("medium", "中风险"), + ("low", "低风险"), + ("info", "提示"), + ] + return " · ".join( + f"{label} {int(risk_summary.get(key) or 0)}" + for key, label in labels + if int(risk_summary.get(key) or 0) + ) diff --git a/review_agent/views.py b/review_agent/views.py index 43dbded..b85b86c 100644 --- a/review_agent/views.py +++ b/review_agent/views.py @@ -11,7 +11,7 @@ from .services import ( send_message, stream_message, ) -from .models import Conversation, FileAttachment, FileSummaryBatch +from .models import Conversation, FileAttachment, FileSummaryBatch, RegulatoryReviewBatch, WorkflowNodeRun @login_required @@ -42,6 +42,8 @@ def workspace(request: HttpRequest) -> HttpResponse: if current is None and conversations.exists(): current = conversations.first() + workflow_cards = build_workflow_cards(current) if current else [] + return render( request, "home.html", @@ -52,7 +54,7 @@ def workspace(request: HttpRequest) -> HttpResponse: "current_conversation": current, "messages": current.messages.all() if current else [], "attachments": FileAttachment.objects.filter(conversation=current).order_by("original_name", "-version_no") if current else [], - "summary_batches": FileSummaryBatch.objects.filter(conversation=current).prefetch_related("node_runs").order_by("-created_at")[:5] if current else [], + "workflow_cards": workflow_cards, }, ) @@ -109,3 +111,56 @@ def stream_chat(request: HttpRequest) -> HttpResponse: response["Cache-Control"] = "no-cache" response["X-Accel-Buffering"] = "no" return response + + +def build_workflow_cards(conversation: Conversation) -> list[dict[str, object]]: + cards: list[dict[str, object]] = [] + for batch in FileSummaryBatch.objects.filter(conversation=conversation).prefetch_related("node_runs"): + cards.append( + { + "id": batch.pk, + "workflow_type": "file_summary", + "batch_no": batch.batch_no, + "status": batch.status, + "error_message": batch.error_message, + "risk_label": "", + "created_at": batch.created_at, + "nodes": list(batch.node_runs.order_by("id")), + } + ) + regulatory_batches = RegulatoryReviewBatch.objects.filter(conversation=conversation) + for batch in regulatory_batches: + cards.append( + { + "id": batch.pk, + "workflow_type": "regulatory_review", + "batch_no": batch.batch_no, + "status": batch.status, + "error_message": batch.error_message, + "risk_label": _format_risk_label(batch.risk_summary or {}), + "created_at": batch.created_at, + "nodes": list( + WorkflowNodeRun.objects.filter( + workflow_type="regulatory_review", + workflow_batch_id=batch.pk, + ).order_by("id") + ), + } + ) + return sorted(cards, key=lambda item: item["created_at"], reverse=True)[:5] + + +def _format_risk_label(risk_summary: dict) -> str: + parts = [] + labels = [ + ("blocking", "阻断项"), + ("high", "高风险"), + ("medium", "中风险"), + ("low", "低风险"), + ("info", "提示"), + ] + for key, label in labels: + count = int(risk_summary.get(key) or 0) + if count: + parts.append(f"{label} {count}") + return " · ".join(parts) diff --git a/static/js/app.js b/static/js/app.js index cf4c6dd..f1d27bb 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -455,6 +455,12 @@ return summaryPanel.getAttribute(attributeName).replace(token, value); } + function statusUrlForWorkflow(workflow_type, batchId) { + var attributeName = + workflow_type === "regulatory_review" ? "data-regulatory-status-url-template" : "data-status-url-template"; + return templateUrl(attributeName, "__batch_id__", batchId); + } + function renderAttachments(attachments) { if (!attachmentList) { return; @@ -542,13 +548,17 @@ if (empty) { empty.remove(); } - var card = workflowCardList.querySelector('[data-batch-id="' + batch.batch_id + '"]'); + var workflow_type = batch.workflow_type || "file_summary"; + var card = workflowCardList.querySelector( + '[data-batch-id="' + batch.batch_id + '"][data-workflow-type="' + workflow_type + '"]' + ); if (card) { return card; } card = document.createElement("article"); card.className = "workflow-card"; card.setAttribute("data-batch-id", batch.batch_id); + card.setAttribute("data-workflow-type", workflow_type); card.innerHTML = "
" + escapeHtml(batch.batch_no || "文件汇总") + @@ -634,13 +644,13 @@ selectWorkflowBatchIndex(activeIndex); } - async function refreshWorkflowCard(batchId) { + async function refreshWorkflowCard(batchId, workflow_type) { if (!summaryPanel || !batchId) { return ""; } var response; try { - response = await fetch(templateUrl("data-status-url-template", "__batch_id__", batchId), { + response = await fetch(statusUrlForWorkflow(workflow_type || "file_summary", batchId), { cache: "no-store", }); } catch (error) { @@ -655,6 +665,7 @@ var card = ensureWorkflowCard({ batch_id: payload.batch.id, batch_no: payload.batch.batch_no, + workflow_type: payload.batch.workflow_type || workflow_type || "file_summary", }); if (!card) { return payload.batch.status || ""; @@ -673,6 +684,17 @@ } else if (batchError) { batchError.remove(); } + var riskSummary = card.querySelector(".workflow-risk-summary"); + if (payload.batch.risk_summary_text) { + if (!riskSummary) { + riskSummary = document.createElement("p"); + riskSummary.className = "workflow-risk-summary"; + card.insertBefore(riskSummary, card.querySelector("ol")); + } + riskSummary.textContent = payload.batch.risk_summary_text; + } else if (riskSummary) { + riskSummary.remove(); + } var list = card.querySelector("ol"); list.innerHTML = ""; (payload.nodes || []).forEach(function (node) { @@ -724,29 +746,37 @@ return status === "success" || status === "failed"; } - function stopWorkflowPolling(batchId) { - if (!workflowPollingTimers[batchId]) { + function workflowTimerKey(batchId, workflow_type) { + return (workflow_type || "file_summary") + ":" + batchId; + } + + function stopWorkflowPolling(batchId, workflow_type) { + var key = workflowTimerKey(batchId, workflow_type); + if (!workflowPollingTimers[key]) { return; } - window.clearInterval(workflowPollingTimers[batchId]); - delete workflowPollingTimers[batchId]; + window.clearInterval(workflowPollingTimers[key]); + delete workflowPollingTimers[key]; } function startWorkflowPolling(batchId) { - if (!batchId || workflowPollingTimers[batchId]) { + var card = workflowCardList ? workflowCardList.querySelector('[data-batch-id="' + batchId + '"]') : null; + var workflow_type = card ? card.getAttribute("data-workflow-type") || "file_summary" : "file_summary"; + var key = workflowTimerKey(batchId, workflow_type); + if (!batchId || workflowPollingTimers[key]) { return; } - workflowPollingTimers[batchId] = window.setInterval(async function () { - var status = await refreshWorkflowCard(batchId); + workflowPollingTimers[key] = window.setInterval(async function () { + var status = await refreshWorkflowCard(batchId, workflow_type); if (isWorkflowTerminalStatus(status)) { refreshConversationMessages(); - stopWorkflowPolling(batchId); + stopWorkflowPolling(batchId, workflow_type); } }, WORKFLOW_POLL_INTERVAL_MS); - refreshWorkflowCard(batchId).then(function (status) { + refreshWorkflowCard(batchId, workflow_type).then(function (status) { if (isWorkflowTerminalStatus(status)) { refreshConversationMessages(); - stopWorkflowPolling(batchId); + stopWorkflowPolling(batchId, workflow_type); } }); } @@ -757,10 +787,11 @@ } workflowCardList.querySelectorAll(".workflow-card").forEach(function (card) { var batchId = card.getAttribute("data-batch-id"); + var workflow_type = card.getAttribute("data-workflow-type") || "file_summary"; var status = card.querySelector(".workflow-status"); var statusText = status ? status.textContent.trim() : ""; if (!isWorkflowTerminalStatus(statusText)) { - startWorkflowPolling(batchId); + startWorkflowPolling(batchId, workflow_type); } }); } diff --git a/templates/home.html b/templates/home.html index 24196dc..55c425f 100644 --- a/templates/home.html +++ b/templates/home.html @@ -177,6 +177,7 @@ data-attachment-url-template="/api/review-agent/conversations/__conversation_id__/attachments/" data-message-url-template="/api/review-agent/conversations/__conversation_id__/messages/" data-status-url-template="/api/review-agent/file-summary/__batch_id__/status/" + data-regulatory-status-url-template="/api/review-agent/regulatory-review/__batch_id__/status/" data-events-url-template="/api/review-agent/file-summary/__batch_id__/events/" >
@@ -221,10 +222,11 @@

工作流

+ {% if batch.risk_label %} +

{{ batch.risk_label }}

+ {% endif %} {% if batch.error_message %}

{{ batch.error_message }}

{% endif %}
    - {% for node in batch.node_runs.all %} + {% for node in batch.nodes %}
  1. {{ node.node_name }} @@ -250,11 +255,11 @@ {% empty %}
    暂无工作流
    {% endfor %} - {% if summary_batches %} + {% if workflow_cards %}
    - {% for batch in summary_batches %} + {% for batch in workflow_cards %}