fix: paginate chat detail messages
This commit is contained in:
@@ -52,6 +52,8 @@ const retrievalDetailOpen = ref(false);
|
|||||||
const previewKnowledgeId = ref<number | null>(null);
|
const previewKnowledgeId = ref<number | null>(null);
|
||||||
const chatDetail = ref<ChatDetail | null>(null);
|
const chatDetail = ref<ChatDetail | null>(null);
|
||||||
const chatDetailOpen = ref(false);
|
const chatDetailOpen = ref(false);
|
||||||
|
const chatDetailLoading = ref(false);
|
||||||
|
const chatDetailSessionId = ref<number | null>(null);
|
||||||
const selectedAiLog = ref<AiLogRecord | null>(null);
|
const selectedAiLog = ref<AiLogRecord | null>(null);
|
||||||
const aiLogDetailOpen = ref(false);
|
const aiLogDetailOpen = ref(false);
|
||||||
const recordTab = ref("chats");
|
const recordTab = ref("chats");
|
||||||
@@ -63,6 +65,7 @@ const pagers = reactive({
|
|||||||
retrievals: { page: 1, pageSize: 20, total: 0 },
|
retrievals: { page: 1, pageSize: 20, total: 0 },
|
||||||
attention: { page: 1, pageSize: 20, total: 0 },
|
attention: { page: 1, pageSize: 20, total: 0 },
|
||||||
});
|
});
|
||||||
|
const chatDetailMessagePager = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||||
const editingModelId = ref<number | null>(null);
|
const editingModelId = ref<number | null>(null);
|
||||||
const editingKnowledgeId = ref<number | null>(null);
|
const editingKnowledgeId = ref<number | null>(null);
|
||||||
const batchNodeToken = ref("");
|
const batchNodeToken = ref("");
|
||||||
@@ -739,13 +742,38 @@ async function resetChatFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openChatDetail(row: ChatRecord) {
|
async function openChatDetail(row: ChatRecord) {
|
||||||
chatDetail.value = await api.chatDetail(row.id);
|
await openChatSession(row.id);
|
||||||
chatDetailOpen.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openChatSession(sessionId: number) {
|
async function openChatSession(sessionId: number) {
|
||||||
chatDetail.value = await api.chatDetail(sessionId);
|
|
||||||
chatDetailOpen.value = true;
|
chatDetailOpen.value = true;
|
||||||
|
chatDetail.value = null;
|
||||||
|
chatDetailSessionId.value = sessionId;
|
||||||
|
Object.assign(chatDetailMessagePager, { page: 1, pageSize: chatDetailMessagePager.pageSize, total: 0 });
|
||||||
|
await loadChatDetail(sessionId, 1, chatDetailMessagePager.pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChatDetail(sessionId = chatDetailSessionId.value, page = chatDetailMessagePager.page, pageSize = chatDetailMessagePager.pageSize) {
|
||||||
|
if (!sessionId) return;
|
||||||
|
chatDetailLoading.value = true;
|
||||||
|
try {
|
||||||
|
const detail = await api.chatDetail(sessionId, { messagePage: page, messagePageSize: pageSize });
|
||||||
|
chatDetail.value = detail;
|
||||||
|
const messagePage = detail.messagesPage;
|
||||||
|
Object.assign(chatDetailMessagePager, {
|
||||||
|
page: messagePage?.page ?? page,
|
||||||
|
pageSize: messagePage?.pageSize ?? pageSize,
|
||||||
|
total: messagePage?.total ?? detail.messages.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : "聊天详情加载失败");
|
||||||
|
} finally {
|
||||||
|
chatDetailLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeChatDetailMessagePage(page: number, pageSize: number) {
|
||||||
|
await loadChatDetail(chatDetailSessionId.value, page, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openAiLogDetail(row: AiLogRecord) {
|
async function openAiLogDetail(row: AiLogRecord) {
|
||||||
@@ -1206,6 +1234,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
<el-drawer v-model="chatDetailOpen" size="760px" title="聊天详情">
|
<el-drawer v-model="chatDetailOpen" size="760px" title="聊天详情">
|
||||||
|
<div v-loading="chatDetailLoading" class="chat-detail-body">
|
||||||
<template v-if="chatDetail">
|
<template v-if="chatDetail">
|
||||||
<section class="chat-summary">
|
<section class="chat-summary">
|
||||||
<div><span>会话ID</span><strong>{{ chatDetail.session.id }}</strong></div>
|
<div><span>会话ID</span><strong>{{ chatDetail.session.id }}</strong></div>
|
||||||
@@ -1235,6 +1264,12 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
<AdminPagination
|
||||||
|
:page="chatDetailMessagePager.page"
|
||||||
|
:page-size="chatDetailMessagePager.pageSize"
|
||||||
|
:total="chatDetailMessagePager.total"
|
||||||
|
@change="changeChatDetailMessagePage"
|
||||||
|
/>
|
||||||
|
|
||||||
<h3 class="detail-title">关联 AI 请求</h3>
|
<h3 class="detail-title">关联 AI 请求</h3>
|
||||||
<el-collapse>
|
<el-collapse>
|
||||||
@@ -1263,6 +1298,8 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
</template>
|
</template>
|
||||||
|
<el-empty v-else-if="!chatDetailLoading" description="聊天详情加载失败或暂无数据" :image-size="80" />
|
||||||
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
<el-drawer v-model="aiLogDetailOpen" size="820px" title="AI 请求详情">
|
<el-drawer v-model="aiLogDetailOpen" size="820px" title="AI 请求详情">
|
||||||
|
|||||||
@@ -201,7 +201,8 @@ export const api = {
|
|||||||
saveConfig: (payload: Record<string, unknown>) =>
|
saveConfig: (payload: Record<string, unknown>) =>
|
||||||
request<SystemConfigItem>("/admin/config", { method: "PUT", body: JSON.stringify(payload) }),
|
request<SystemConfigItem>("/admin/config", { method: "PUT", body: JSON.stringify(payload) }),
|
||||||
chats: (query: ChatRecordQuery = {}) => request<PageResult<ChatRecord>>(`/admin/chat/list${queryString(query)}`),
|
chats: (query: ChatRecordQuery = {}) => request<PageResult<ChatRecord>>(`/admin/chat/list${queryString(query)}`),
|
||||||
chatDetail: (sessionId: number) => request<ChatDetail>(`/admin/chat/${sessionId}`),
|
chatDetail: (sessionId: number, query: { messagePage?: number; messagePageSize?: number } = {}) =>
|
||||||
|
request<ChatDetail>(`/admin/chat/${sessionId}${queryString(query)}`),
|
||||||
exportChats: (query: ChatRecordQuery = {}) => download(`/admin/chat/export${queryString(query)}`, "chat_records.csv"),
|
exportChats: (query: ChatRecordQuery = {}) => download(`/admin/chat/export${queryString(query)}`, "chat_records.csv"),
|
||||||
aiLogs: (query: { sessionId?: number; userId?: number; status?: string; page?: number; pageSize?: number } = {}) =>
|
aiLogs: (query: { sessionId?: number; userId?: number; status?: string; page?: number; pageSize?: number } = {}) =>
|
||||||
request<PageResult<AiLogRecord>>(`/admin/ai-log/list${queryString(query)}`),
|
request<PageResult<AiLogRecord>>(`/admin/ai-log/list${queryString(query)}`),
|
||||||
|
|||||||
@@ -1738,6 +1738,10 @@ textarea {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-detail-body {
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-summary {
|
.chat-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ export interface RetrievedKnowledgeChunk {
|
|||||||
export interface ChatDetail {
|
export interface ChatDetail {
|
||||||
session: ChatRecord;
|
session: ChatRecord;
|
||||||
messages: ChatMessageRecord[];
|
messages: ChatMessageRecord[];
|
||||||
|
messagesPage?: PageResult<ChatMessageRecord>;
|
||||||
aiLogs: AiLogRecord[];
|
aiLogs: AiLogRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ def export_chats(
|
|||||||
@router.get("/chat/{session_id}")
|
@router.get("/chat/{session_id}")
|
||||||
def chat_detail(
|
def chat_detail(
|
||||||
session_id: int,
|
session_id: int,
|
||||||
|
messagePage: int = Query(default=1, ge=1),
|
||||||
|
messagePageSize: int = Query(default=20, ge=10, le=100),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: Admin = Depends(get_current_admin),
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -97,10 +99,13 @@ def chat_detail(
|
|||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="会话不存在")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="会话不存在")
|
||||||
|
|
||||||
session, user = session_row
|
session, user = session_row
|
||||||
|
message_query = select(ChatMessage).where(ChatMessage.session_id == session_id)
|
||||||
|
message_total = db.scalar(select(func.count()).select_from(message_query.subquery())) or 0
|
||||||
messages = db.scalars(
|
messages = db.scalars(
|
||||||
select(ChatMessage)
|
message_query
|
||||||
.where(ChatMessage.session_id == session_id)
|
|
||||||
.order_by(ChatMessage.created_at.asc(), ChatMessage.id.asc())
|
.order_by(ChatMessage.created_at.asc(), ChatMessage.id.asc())
|
||||||
|
.offset((messagePage - 1) * messagePageSize)
|
||||||
|
.limit(messagePageSize)
|
||||||
).all()
|
).all()
|
||||||
ai_logs = db.scalars(
|
ai_logs = db.scalars(
|
||||||
select(AiRequestLog)
|
select(AiRequestLog)
|
||||||
@@ -111,6 +116,12 @@ def chat_detail(
|
|||||||
{
|
{
|
||||||
"session": _chat_row_dict(session, user),
|
"session": _chat_row_dict(session, user),
|
||||||
"messages": [_message_dict(item) for item in messages],
|
"messages": [_message_dict(item) for item in messages],
|
||||||
|
"messagesPage": page_result(
|
||||||
|
[_message_dict(item) for item in messages],
|
||||||
|
total=message_total,
|
||||||
|
page=messagePage,
|
||||||
|
page_size=messagePageSize,
|
||||||
|
),
|
||||||
"aiLogs": [_ai_log_dict(item, include_prompt=True) for item in ai_logs],
|
"aiLogs": [_ai_log_dict(item, include_prompt=True) for item in ai_logs],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ from sqlalchemy.orm import Session
|
|||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
from app.api.admin_agent_records import attention_list, retrieval_logs
|
from app.api.admin_agent_records import attention_list, retrieval_logs
|
||||||
from app.api.admin_records import ai_logs
|
from app.api.admin_records import ai_logs, chat_detail
|
||||||
from app.api.admin_users import list_users
|
from app.api.admin_users import list_users
|
||||||
from app.models import Base
|
from app.models import Base
|
||||||
|
from app.models.chat import ChatMessage, ChatSession
|
||||||
from app.models.knowledge import HumanAttentionRecord, KnowledgeRetrievalLog
|
from app.models.knowledge import HumanAttentionRecord, KnowledgeRetrievalLog
|
||||||
from app.models.logs import AiRequestLog
|
from app.models.logs import AiRequestLog
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -43,6 +44,27 @@ def test_ai_log_page_does_not_load_large_detail_fields():
|
|||||||
assert all(item["retrievedChunks"] == [] for item in response["data"]["items"])
|
assert all(item["retrievedChunks"] == [] for item in response["data"]["items"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_detail_messages_are_paginated():
|
||||||
|
with _database() as db:
|
||||||
|
user = User(id=1, phone="13800000000", name="学员", daily_chat_limit=10)
|
||||||
|
session = ChatSession(id=1, user_id=1, title="长会话", message_count=25)
|
||||||
|
db.add_all([user, session])
|
||||||
|
db.add_all([
|
||||||
|
ChatMessage(id=index + 1, session_id=1, user_id=1, role="user", content=f"消息{index + 1}")
|
||||||
|
for index in range(25)
|
||||||
|
])
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
response = chat_detail(1, messagePage=2, messagePageSize=10, db=db, current_admin=object())
|
||||||
|
|
||||||
|
data = response["data"]
|
||||||
|
assert data["session"]["messageCount"] == 25
|
||||||
|
assert data["messagesPage"]["total"] == 25
|
||||||
|
assert data["messagesPage"]["page"] == 2
|
||||||
|
assert len(data["messages"]) == 10
|
||||||
|
assert data["messages"][0]["content"] == "消息11"
|
||||||
|
|
||||||
|
|
||||||
def test_retrieval_and_attention_lists_are_paginated():
|
def test_retrieval_and_attention_lists_are_paginated():
|
||||||
with _database() as db:
|
with _database() as db:
|
||||||
for index in range(21):
|
for index in range(21):
|
||||||
|
|||||||
Reference in New Issue
Block a user