feat(dashboard): 增加首页工作台并调整聊天入口
This commit is contained in:
@@ -2,10 +2,11 @@ from django.contrib import admin
|
||||
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView
|
||||
from django.urls import include, path
|
||||
|
||||
from review_agent.views import attachment_manager, knowledge_base_manager, stream_chat, workspace
|
||||
from review_agent.views import attachment_manager, home_dashboard, knowledge_base_manager, stream_chat, workspace
|
||||
|
||||
urlpatterns = [
|
||||
path("", workspace, name="home"),
|
||||
path("", home_dashboard, name="home"),
|
||||
path("chat/", workspace, name="chat"),
|
||||
path("knowledge-base/", knowledge_base_manager, name="knowledge_base_manager"),
|
||||
path("attachments/", attachment_manager, name="attachment_manager"),
|
||||
path("", include("review_agent.urls")),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Count, Q
|
||||
from django.db.models import Count, Q, Sum
|
||||
import json
|
||||
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse, StreamingHttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.http import urlencode
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from .services import (
|
||||
@@ -28,6 +29,29 @@ from .models import KnowledgeBaseDocument
|
||||
from .regulatory_review.services.info_extract import ensure_regulatory_condition_candidates
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def home_dashboard(request: HttpRequest) -> HttpResponse:
|
||||
"""Renders the data-first home dashboard for the current user."""
|
||||
|
||||
if request.GET.get("conversation"):
|
||||
query = {"conversation": request.GET["conversation"]}
|
||||
search = (request.GET.get("q") or "").strip()
|
||||
if search:
|
||||
query["q"] = search
|
||||
return redirect(f"/chat/?{urlencode(query)}")
|
||||
|
||||
context = build_home_dashboard_context(request.user)
|
||||
return render(
|
||||
request,
|
||||
"workbench.html",
|
||||
{
|
||||
"page_title": "首页",
|
||||
"dashboard": context,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def workspace(request: HttpRequest) -> HttpResponse:
|
||||
@@ -39,7 +63,7 @@ def workspace(request: HttpRequest) -> HttpResponse:
|
||||
|
||||
if action == "new_conversation":
|
||||
conversation = create_conversation(request.user)
|
||||
return redirect(f"/?conversation={conversation.pk}")
|
||||
return redirect(f"/chat/?conversation={conversation.pk}")
|
||||
|
||||
if action == "send_message":
|
||||
content = (request.POST.get("prompt") or "").strip()
|
||||
@@ -47,7 +71,7 @@ def workspace(request: HttpRequest) -> HttpResponse:
|
||||
conversation = create_conversation(request.user)
|
||||
if content:
|
||||
send_message(conversation, content)
|
||||
return redirect(f"/?conversation={conversation.pk}")
|
||||
return redirect(f"/chat/?conversation={conversation.pk}")
|
||||
|
||||
search = (request.GET.get("q") or "").strip()
|
||||
conversations = list_conversations(request.user, search)
|
||||
@@ -325,3 +349,139 @@ def _format_form_fill_label(batch: ApplicationFormFillBatch) -> str:
|
||||
if batch.risk_notes:
|
||||
parts.append(f"提示 {len(batch.risk_notes)}")
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
def build_home_dashboard_context(user) -> dict[str, object]:
|
||||
conversations = Conversation.objects.filter(user=user)
|
||||
active_attachments = FileAttachment.objects.filter(user=user).exclude(
|
||||
upload_status=FileAttachment.UploadStatus.DELETED
|
||||
)
|
||||
active_knowledge_documents = KnowledgeBaseDocument.objects.filter(user=user).exclude(
|
||||
status=KnowledgeBaseDocument.Status.DELETED
|
||||
)
|
||||
knowledge_context = build_knowledge_base_context_for_user(user)
|
||||
builtin_source_count = int(knowledge_context.get("source_count") or 0)
|
||||
collection_chunk_count = int((knowledge_context.get("collection") or {}).get("count") or 0)
|
||||
managed_document_count = active_knowledge_documents.count()
|
||||
file_batches = FileSummaryBatch.objects.filter(user=user).select_related("conversation")
|
||||
regulatory_batches = RegulatoryReviewBatch.objects.filter(user=user).select_related("conversation")
|
||||
form_fill_batches = ApplicationFormFillBatch.objects.filter(user=user, is_deleted=False).select_related("conversation")
|
||||
|
||||
batch_status_counts = _build_batch_status_counts(file_batches, regulatory_batches, form_fill_batches)
|
||||
total_batches = file_batches.count() + regulatory_batches.count() + form_fill_batches.count()
|
||||
successful_batches = batch_status_counts["success"]
|
||||
handled_batches = successful_batches + batch_status_counts["failed"]
|
||||
recent_records = _build_recent_dashboard_records(
|
||||
conversations.order_by("-updated_at", "-id")[:8],
|
||||
file_batches.order_by("-created_at", "-id")[:8],
|
||||
regulatory_batches.order_by("-created_at", "-id")[:8],
|
||||
form_fill_batches.order_by("-created_at", "-id")[:8],
|
||||
)
|
||||
|
||||
return {
|
||||
"metrics": {
|
||||
"conversation_count": conversations.count(),
|
||||
"recent_conversation_count": conversations.filter(messages__isnull=False).distinct().count(),
|
||||
"attachment_count": active_attachments.count(),
|
||||
"active_attachment_count": active_attachments.filter(is_active=True).count(),
|
||||
"knowledge_document_count": managed_document_count + builtin_source_count,
|
||||
"running_batch_count": batch_status_counts["running"],
|
||||
"handled_batch_count": handled_batches,
|
||||
"success_batch_count": successful_batches,
|
||||
"waiting_batch_count": batch_status_counts["waiting"],
|
||||
"failed_batch_count": batch_status_counts["failed"],
|
||||
"total_batch_count": total_batches,
|
||||
},
|
||||
"knowledge": {
|
||||
"document_count": managed_document_count,
|
||||
"builtin_source_count": builtin_source_count,
|
||||
"total_material_count": managed_document_count + builtin_source_count,
|
||||
"active_document_count": active_knowledge_documents.filter(is_active=True).count(),
|
||||
"indexed_document_count": active_knowledge_documents.filter(indexed_chunk_count__gt=0).count(),
|
||||
"managed_chunk_count": active_knowledge_documents.aggregate(total=Sum("indexed_chunk_count"))["total"] or 0,
|
||||
"chunk_count": collection_chunk_count,
|
||||
},
|
||||
"attachments": {
|
||||
"attachment_count": active_attachments.count(),
|
||||
"active_attachment_count": active_attachments.filter(is_active=True).count(),
|
||||
"recent_attachment_count": active_attachments.order_by("-created_at", "-id")[:5].count(),
|
||||
"conversation_count": active_attachments.values("conversation_id").distinct().count(),
|
||||
},
|
||||
"workflow": {
|
||||
"file_summary_count": file_batches.count(),
|
||||
"regulatory_review_count": regulatory_batches.count(),
|
||||
"application_form_fill_count": form_fill_batches.count(),
|
||||
**batch_status_counts,
|
||||
},
|
||||
"recent_records": recent_records,
|
||||
}
|
||||
|
||||
|
||||
def _build_batch_status_counts(file_batches, regulatory_batches, form_fill_batches) -> dict[str, int]:
|
||||
running_statuses = {
|
||||
FileSummaryBatch.Status.PENDING,
|
||||
FileSummaryBatch.Status.RUNNING,
|
||||
ApplicationFormFillBatch.Status.PENDING,
|
||||
ApplicationFormFillBatch.Status.RUNNING,
|
||||
RegulatoryReviewBatch.Status.PENDING,
|
||||
RegulatoryReviewBatch.Status.RUNNING,
|
||||
}
|
||||
waiting_statuses = {
|
||||
ApplicationFormFillBatch.Status.WAITING_USER,
|
||||
RegulatoryReviewBatch.Status.WAITING_USER,
|
||||
}
|
||||
success_statuses = {
|
||||
FileSummaryBatch.Status.SUCCESS,
|
||||
RegulatoryReviewBatch.Status.SUCCESS,
|
||||
ApplicationFormFillBatch.Status.SUCCESS,
|
||||
ApplicationFormFillBatch.Status.PARTIAL_SUCCESS,
|
||||
}
|
||||
failed_statuses = {
|
||||
FileSummaryBatch.Status.FAILED,
|
||||
RegulatoryReviewBatch.Status.FAILED,
|
||||
ApplicationFormFillBatch.Status.FAILED,
|
||||
}
|
||||
statuses = [
|
||||
*file_batches.values_list("status", flat=True),
|
||||
*regulatory_batches.values_list("status", flat=True),
|
||||
*form_fill_batches.values_list("status", flat=True),
|
||||
]
|
||||
return {
|
||||
"running": sum(1 for status in statuses if status in running_statuses),
|
||||
"waiting": sum(1 for status in statuses if status in waiting_statuses),
|
||||
"success": sum(1 for status in statuses if status in success_statuses),
|
||||
"failed": sum(1 for status in statuses if status in failed_statuses),
|
||||
}
|
||||
|
||||
|
||||
def _build_recent_dashboard_records(conversations, file_batches, regulatory_batches, form_fill_batches) -> list[dict[str, object]]:
|
||||
records = []
|
||||
for conversation in conversations:
|
||||
records.append(
|
||||
{
|
||||
"type": "对话",
|
||||
"title": conversation.title or "新对话",
|
||||
"status": "已更新",
|
||||
"updated_at": conversation.updated_at,
|
||||
"url": f"/chat/?conversation={conversation.pk}",
|
||||
}
|
||||
)
|
||||
for batch in file_batches:
|
||||
records.append(_batch_record(batch, "文件汇总"))
|
||||
for batch in regulatory_batches:
|
||||
status = batch.status
|
||||
risk_label = _format_risk_label(batch.risk_summary or {})
|
||||
records.append(_batch_record(batch, "法规核查", status_label=risk_label or status))
|
||||
for batch in form_fill_batches:
|
||||
records.append(_batch_record(batch, "申报填表"))
|
||||
return sorted(records, key=lambda item: item["updated_at"], reverse=True)[:8]
|
||||
|
||||
|
||||
def _batch_record(batch, record_type: str, status_label: str | None = None) -> dict[str, object]:
|
||||
return {
|
||||
"type": record_type,
|
||||
"title": batch.batch_no,
|
||||
"status": status_label or batch.status,
|
||||
"updated_at": batch.created_at,
|
||||
"url": f"/chat/?conversation={batch.conversation_id}",
|
||||
}
|
||||
|
||||
@@ -1478,6 +1478,116 @@ input:focus {
|
||||
background: #eaf2ff;
|
||||
}
|
||||
|
||||
.dashboard-page {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 12px;
|
||||
height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
padding: 16px 24px 20px;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.dashboard-hero,
|
||||
.metric-grid,
|
||||
.dashboard-split,
|
||||
.dashboard-panel {
|
||||
width: min(1440px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dashboard-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard-hero h1 {
|
||||
margin: 2px 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.dashboard-hero p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dashboard-primary-action {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-height: 104px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.metric-card span,
|
||||
.metric-card em {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-card strong {
|
||||
color: var(--text);
|
||||
font-size: 30px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dashboard-split {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dashboard-stat-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-stat-list div {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.dashboard-stat-list dt {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dashboard-stat-list dd {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.recent-activity-table td {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.table-empty,
|
||||
.attachment-manager-empty {
|
||||
color: var(--muted);
|
||||
@@ -2293,6 +2403,23 @@ input:focus {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dashboard-page {
|
||||
height: auto;
|
||||
min-height: calc(100vh - 60px);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.dashboard-hero {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metric-grid,
|
||||
.dashboard-split,
|
||||
.dashboard-stat-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-caret {
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="tabbar" role="tablist" aria-label="页面切换">
|
||||
<a class="tab" href="/" role="tab" aria-selected="false">首页</a>
|
||||
<a class="tab" href="/" role="tab" aria-selected="false">审核智能体</a>
|
||||
<a class="tab" href="{% url 'home' %}" role="tab" aria-selected="false">首页</a>
|
||||
<a class="tab" href="{% url 'chat' %}" role="tab" aria-selected="false">审核智能体</a>
|
||||
<a class="tab" href="{% url 'knowledge_base_manager' %}" role="tab" aria-selected="false">知识库管理</a>
|
||||
<a class="tab active" href="{% url 'attachment_manager' %}" role="tab" aria-selected="true">附件管理</a>
|
||||
</div>
|
||||
@@ -52,7 +52,7 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if selected_conversation %}
|
||||
<a class="return-chat-link" href="{% url 'home' %}?conversation={{ selected_conversation.pk }}">返回对话</a>
|
||||
<a class="return-chat-link" href="{% url 'chat' %}?conversation={{ selected_conversation.pk }}">返回对话</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="tabbar" role="tablist" aria-label="页面切换">
|
||||
<a class="tab" href="/" role="tab" aria-selected="false">首页</a>
|
||||
<a class="tab active" href="/" role="tab" aria-selected="true">审核智能体</a>
|
||||
<a class="tab" href="{% url 'home' %}" role="tab" aria-selected="false">首页</a>
|
||||
<a class="tab active" href="{% url 'chat' %}" role="tab" aria-selected="true">审核智能体</a>
|
||||
<a class="tab" href="{% url 'knowledge_base_manager' %}" role="tab" aria-selected="false">知识库管理</a>
|
||||
<a class="tab" href="{% url 'attachment_manager' %}" role="tab" aria-selected="false">附件管理</a>
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@
|
||||
>
|
||||
<a
|
||||
class="history-link"
|
||||
href="/?conversation={{ conversation.pk }}{% if search_query %}&q={{ search_query|urlencode }}{% endif %}"
|
||||
href="{% url 'chat' %}?conversation={{ conversation.pk }}{% if search_query %}&q={{ search_query|urlencode }}{% endif %}"
|
||||
>
|
||||
<span class="history-title">{{ conversation.title|default:"新对话" }}</span>
|
||||
<span class="history-meta">{{ conversation.updated_at|date:"m月d日 H:i" }}</span>
|
||||
@@ -202,7 +202,7 @@
|
||||
</div>
|
||||
|
||||
<div class="composer-wrap">
|
||||
<form class="composer" action="/" method="post" id="chatComposer">
|
||||
<form class="composer" action="{% url 'chat' %}" method="post" id="chatComposer">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="send_message">
|
||||
<input type="hidden" name="conversation_id" id="conversationIdInput" value="{% if current_conversation %}{{ current_conversation.pk }}{% endif %}">
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="tabbar" role="tablist" aria-label="页面切换">
|
||||
<a class="tab" href="/" role="tab" aria-selected="false">首页</a>
|
||||
<a class="tab" href="/" role="tab" aria-selected="false">审核智能体</a>
|
||||
<a class="tab" href="{% url 'home' %}" role="tab" aria-selected="false">首页</a>
|
||||
<a class="tab" href="{% url 'chat' %}" role="tab" aria-selected="false">审核智能体</a>
|
||||
<a class="tab active" href="{% url 'knowledge_base_manager' %}" role="tab" aria-selected="true">知识库管理</a>
|
||||
<a class="tab" href="{% url 'attachment_manager' %}" role="tab" aria-selected="false">附件管理</a>
|
||||
</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
<div class="knowledge-hero-actions">
|
||||
<span class="knowledge-status status-{{ knowledge_base.status.code }}">{{ knowledge_base.status.label }}</span>
|
||||
<a class="return-chat-link" href="{% url 'home' %}">返回对话</a>
|
||||
<a class="return-chat-link" href="{% url 'chat' %}">返回对话</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
173
templates/workbench.html
Normal file
173
templates/workbench.html
Normal file
@@ -0,0 +1,173 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}首页 - DEMO-AGENT V2{% endblock %}
|
||||
{% block body_class %}app-body{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="app-shell">
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="tabbar" role="tablist" aria-label="页面切换">
|
||||
<a class="tab active" href="{% url 'home' %}" role="tab" aria-selected="true">首页</a>
|
||||
<a class="tab" href="{% url 'chat' %}" role="tab" aria-selected="false">审核智能体</a>
|
||||
<a class="tab" href="{% url 'knowledge_base_manager' %}" role="tab" aria-selected="false">知识库管理</a>
|
||||
<a class="tab" href="{% url 'attachment_manager' %}" role="tab" aria-selected="false">附件管理</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topbar-right">
|
||||
<div class="user-menu">
|
||||
<button class="user-menu-trigger" type="button">
|
||||
<span class="avatar large">{{ request.user.username|slice:":1"|upper }}</span>
|
||||
<div class="user-copy">
|
||||
<strong>{{ request.user.username }}</strong>
|
||||
<span>当前登录用户</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="dashboard-page">
|
||||
<header class="dashboard-hero attachment-manager-toolbar">
|
||||
<div>
|
||||
<p class="eyebrow">首页</p>
|
||||
<h1>注册资料审核工作台</h1>
|
||||
<p>当前账号资料、知识库、附件与审核处理数据总览。</p>
|
||||
</div>
|
||||
<a class="return-chat-link dashboard-primary-action" href="{% url 'chat' %}">进入审核智能体</a>
|
||||
</header>
|
||||
|
||||
<section class="metric-grid" aria-label="首页关键指标">
|
||||
<article class="metric-card">
|
||||
<span>对话总数</span>
|
||||
<strong>{{ dashboard.metrics.conversation_count }}</strong>
|
||||
<em>已处理 {{ dashboard.metrics.recent_conversation_count }}</em>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>附件总数</span>
|
||||
<strong>{{ dashboard.metrics.attachment_count }}</strong>
|
||||
<em>启用 {{ dashboard.metrics.active_attachment_count }}</em>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>知识库材料</span>
|
||||
<strong>{{ dashboard.metrics.knowledge_document_count }}</strong>
|
||||
<em>管理 {{ dashboard.knowledge.document_count }} · 内置 {{ dashboard.knowledge.builtin_source_count }}</em>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>执行中批次</span>
|
||||
<strong>{{ dashboard.metrics.running_batch_count }}</strong>
|
||||
<em>总批次 {{ dashboard.metrics.total_batch_count }}</em>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>已处理批次</span>
|
||||
<strong>{{ dashboard.metrics.handled_batch_count }}</strong>
|
||||
<em>成功 {{ dashboard.metrics.success_batch_count }}</em>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>等待确认</span>
|
||||
<strong>{{ dashboard.metrics.waiting_batch_count }}</strong>
|
||||
<em>需人工处理</em>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>失败批次</span>
|
||||
<strong>{{ dashboard.metrics.failed_batch_count }}</strong>
|
||||
<em>需排查</em>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>申报填表</span>
|
||||
<strong>{{ dashboard.workflow.application_form_fill_count }}</strong>
|
||||
<em>自动填表批次</em>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="dashboard-split">
|
||||
<section class="attachment-manager-panel dashboard-panel">
|
||||
<div class="summary-subheading">
|
||||
<h3>知识库概览</h3>
|
||||
</div>
|
||||
<dl class="dashboard-stat-list">
|
||||
<div>
|
||||
<dt>管理文档</dt>
|
||||
<dd>{{ dashboard.knowledge.document_count }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>内置材料</dt>
|
||||
<dd>{{ dashboard.knowledge.builtin_source_count }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>已索引</dt>
|
||||
<dd>{{ dashboard.knowledge.indexed_document_count }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>向量片段</dt>
|
||||
<dd>{{ dashboard.knowledge.chunk_count }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="attachment-manager-panel dashboard-panel">
|
||||
<div class="summary-subheading">
|
||||
<h3>附件与文档概览</h3>
|
||||
</div>
|
||||
<dl class="dashboard-stat-list">
|
||||
<div>
|
||||
<dt>附件总数</dt>
|
||||
<dd>{{ dashboard.attachments.attachment_count }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>启用附件</dt>
|
||||
<dd>{{ dashboard.attachments.active_attachment_count }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>最近上传</dt>
|
||||
<dd>{{ dashboard.attachments.recent_attachment_count }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>关联对话</dt>
|
||||
<dd>{{ dashboard.attachments.conversation_count }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="attachment-manager-panel dashboard-panel">
|
||||
<div class="summary-subheading">
|
||||
<h3>最近处理记录</h3>
|
||||
<span>最近 8 条</span>
|
||||
</div>
|
||||
<div class="attachment-table-wrap">
|
||||
<table class="attachment-table recent-activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>名称或批次号</th>
|
||||
<th>状态</th>
|
||||
<th>更新时间</th>
|
||||
<th>入口</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for record in dashboard.recent_records %}
|
||||
<tr>
|
||||
<td>{{ record.type }}</td>
|
||||
<td class="attachment-name">{{ record.title }}</td>
|
||||
<td>{{ record.status }}</td>
|
||||
<td>{{ record.updated_at|date:"Y-m-d H:i" }}</td>
|
||||
<td class="attachment-actions">
|
||||
<a href="{{ record.url }}">查看</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="table-empty">暂无处理记录</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -31,7 +31,7 @@ def test_workspace_renders_application_form_fill_workflow_card(client, django_us
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
assert "AFF-CARD" in content
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_workspace_renders_summary_panel(client, django_user_model):
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
@@ -37,7 +37,7 @@ def test_workspace_links_to_attachment_manager(client, django_user_model):
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
@@ -85,7 +85,7 @@ def test_attachment_manager_selects_conversation_and_lists_attachments(client, d
|
||||
assert "编辑" in content
|
||||
assert "删除" in content
|
||||
assert "attachment-manager-split" in content
|
||||
assert reverse("home") + f"?conversation={conversation.pk}" in content
|
||||
assert reverse("chat") + f"?conversation={conversation.pk}" in content
|
||||
|
||||
|
||||
def test_attachment_manager_uses_compact_admin_layout(client, django_user_model):
|
||||
@@ -142,7 +142,7 @@ def test_workspace_renders_workflow_history_as_batch_carousel(client, django_use
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
@@ -265,7 +265,7 @@ def test_workspace_tool_buttons_fill_default_prompts(client, django_user_model):
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
script = open("static/js/app.js", encoding="utf-8").read()
|
||||
|
||||
146
tests/test_home_dashboard.py
Normal file
146
tests/test_home_dashboard.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from review_agent.models import (
|
||||
ApplicationFormFillBatch,
|
||||
Conversation,
|
||||
FileAttachment,
|
||||
FileSummaryBatch,
|
||||
KnowledgeBaseDocument,
|
||||
RegulatoryReviewBatch,
|
||||
)
|
||||
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_home_dashboard_renders_current_user_metrics(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
other = django_user_model.objects.create_user(username="other", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="注册资料会话")
|
||||
other_conversation = Conversation.objects.create(user=other, title="其他用户会话")
|
||||
FileAttachment.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
original_name="active.docx",
|
||||
storage_path="x/active.docx",
|
||||
file_size=128,
|
||||
is_active=True,
|
||||
)
|
||||
FileAttachment.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
original_name="deleted.docx",
|
||||
storage_path="x/deleted.docx",
|
||||
file_size=128,
|
||||
is_active=False,
|
||||
upload_status=FileAttachment.UploadStatus.DELETED,
|
||||
)
|
||||
FileAttachment.objects.create(
|
||||
conversation=other_conversation,
|
||||
user=other,
|
||||
original_name="other.docx",
|
||||
storage_path="x/other.docx",
|
||||
file_size=128,
|
||||
)
|
||||
KnowledgeBaseDocument.objects.create(
|
||||
user=user,
|
||||
display_name="法规资料",
|
||||
original_name="rule.md",
|
||||
storage_path="kb/rule.md",
|
||||
file_size=64,
|
||||
is_active=True,
|
||||
indexed_chunk_count=3,
|
||||
)
|
||||
KnowledgeBaseDocument.objects.create(
|
||||
user=user,
|
||||
display_name="删除资料",
|
||||
original_name="deleted.md",
|
||||
storage_path="kb/deleted.md",
|
||||
file_size=64,
|
||||
status=KnowledgeBaseDocument.Status.DELETED,
|
||||
is_active=False,
|
||||
indexed_chunk_count=5,
|
||||
)
|
||||
KnowledgeBaseDocument.objects.create(
|
||||
user=other,
|
||||
display_name="其他资料",
|
||||
original_name="other.md",
|
||||
storage_path="kb/other.md",
|
||||
file_size=64,
|
||||
indexed_chunk_count=9,
|
||||
)
|
||||
summary = FileSummaryBatch.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
batch_no="FS-RUN",
|
||||
status=FileSummaryBatch.Status.RUNNING,
|
||||
)
|
||||
RegulatoryReviewBatch.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
source_summary_batch=summary,
|
||||
batch_no="RR-WAIT",
|
||||
status=RegulatoryReviewBatch.Status.WAITING_USER,
|
||||
risk_summary={"high": 2},
|
||||
)
|
||||
ApplicationFormFillBatch.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
source_summary_batch=summary,
|
||||
batch_no="AFF-OK",
|
||||
status=ApplicationFormFillBatch.Status.SUCCESS,
|
||||
)
|
||||
FileSummaryBatch.objects.create(
|
||||
conversation=other_conversation,
|
||||
user=other,
|
||||
batch_no="FS-OTHER",
|
||||
status=FileSummaryBatch.Status.FAILED,
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(reverse("home"))
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
assert "注册资料审核工作台" in content
|
||||
assert "当前账号资料、知识库、附件与审核处理数据总览" in content
|
||||
assert "工作流流程" not in content
|
||||
assert "对话总数" in content
|
||||
assert "附件总数" in content
|
||||
assert "知识库材料" in content
|
||||
assert "内置材料" in content
|
||||
assert f"管理 {1} · 内置" in content
|
||||
assert "向量片段" in content
|
||||
assert "FS-RUN" in content
|
||||
assert "RR-WAIT" in content
|
||||
assert "AFF-OK" in content
|
||||
assert "FS-OTHER" not in content
|
||||
assert "其他用户会话" not in content
|
||||
assert f'href="{reverse("chat")}?conversation={conversation.pk}"' in content
|
||||
|
||||
|
||||
def test_chat_route_renders_review_agent_workspace(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="审核会话")
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
assert "审核智能体" in content
|
||||
assert 'id="summaryPanel"' in content
|
||||
assert f'action="{reverse("chat")}"' in content
|
||||
assert f'href="{reverse("chat")}?conversation={conversation.pk}"' in content
|
||||
|
||||
|
||||
def test_legacy_home_conversation_redirects_to_chat(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="旧入口会话")
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response["Location"] == f"{reverse('chat')}?conversation={conversation.pk}"
|
||||
@@ -44,7 +44,7 @@ def test_workspace_renders_regulatory_workflow_card(client, django_user_model):
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
assert "RR-CARD" in content
|
||||
@@ -97,7 +97,7 @@ def test_workspace_renders_condition_confirmation_form(client, django_user_model
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
assert "适用条件确认" in content
|
||||
@@ -152,7 +152,7 @@ def test_workspace_refreshes_incomplete_condition_confirmation_candidates(client
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
assert "体外诊断试剂" in content
|
||||
@@ -193,7 +193,7 @@ def test_workspace_renders_rectification_actions_and_summaries(client, tmp_path,
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
response = client.get(f"{reverse('chat')}?conversation={conversation.pk}")
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
assert "data-rectification-action=\"full-review\"" in content
|
||||
|
||||
Reference in New Issue
Block a user