From 8845845bf0145f2bf202a8dfa3d6cf5b00635599 Mon Sep 17 00:00:00 2001
From: yuetsh <517252939@qq.com>
Date: Wed, 6 May 2026 07:13:06 -0600
Subject: [PATCH] add message history
---
prompt/api.py | 54 ++++++++++++++++++++++++++-
prompt/schemas.py | 13 +++++++
prompt/tests.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 159 insertions(+), 1 deletion(-)
diff --git a/prompt/api.py b/prompt/api.py
index 628a9d8..9866dd1 100644
--- a/prompt/api.py
+++ b/prompt/api.py
@@ -8,7 +8,7 @@ from django.contrib.auth.decorators import login_required
from django.db.models import Count
from .models import Conversation, Message
-from .schemas import ConversationOut, MessageOut
+from .schemas import ConversationOut, MessageOut, PromptHistoryItemOut
from account.models import RoleChoices
router = Router()
@@ -55,6 +55,58 @@ def list_messages(request, conversation_id: UUID):
]
+@router.get("/history/{task_id}", response=List[PromptHistoryItemOut])
+@login_required
+def list_prompt_history(request, task_id: int):
+ """
+ 获取当前用户在某任务下的历史对话轮次。
+
+ 只返回用户提示词和后一条 assistant 消息中的页面代码,用于前端渲染缩略图。
+ """
+ conversations = Conversation.objects.filter(
+ user=request.user,
+ task_id=task_id,
+ ).prefetch_related("messages")
+
+ items = []
+ for conv in conversations:
+ messages = list(conv.messages.all().order_by("created", "id"))
+ for idx, user_msg in enumerate(messages):
+ if user_msg.role != "user":
+ continue
+
+ assistant_msg = None
+ for reply in messages[idx + 1:]:
+ if reply.role == "user":
+ break
+ if reply.role == "assistant":
+ assistant_msg = reply
+ break
+
+ if not assistant_msg:
+ continue
+
+ items.append(
+ (
+ user_msg.created,
+ {
+ "user_message_id": user_msg.id,
+ "assistant_message_id": assistant_msg.id,
+ "submission_id": assistant_msg.submission_id,
+ "source": user_msg.source,
+ "prompt": user_msg.content,
+ "prompt_level": user_msg.prompt_level,
+ "code_html": assistant_msg.code_html,
+ "code_css": assistant_msg.code_css,
+ "code_js": assistant_msg.code_js,
+ "created": user_msg.created.isoformat(),
+ },
+ )
+ )
+
+ return [item for _, item in sorted(items, key=lambda row: row[0], reverse=True)]
+
+
@router.post("/conversations/{conversation_id}/classify")
@login_required
def classify_conversation(request, conversation_id: UUID, force: bool = False):
diff --git a/prompt/schemas.py b/prompt/schemas.py
index 3b7559f..9cc9c5f 100644
--- a/prompt/schemas.py
+++ b/prompt/schemas.py
@@ -15,6 +15,19 @@ class MessageOut(Schema):
created: str
+class PromptHistoryItemOut(Schema):
+ user_message_id: int
+ assistant_message_id: int
+ submission_id: Optional[UUID] = None
+ source: str
+ prompt: str
+ prompt_level: Optional[int] = None
+ code_html: Optional[str] = None
+ code_css: Optional[str] = None
+ code_js: Optional[str] = None
+ created: str
+
+
class ConversationOut(Schema):
id: UUID
user_id: int
diff --git a/prompt/tests.py b/prompt/tests.py
index 448f902..62a7385 100644
--- a/prompt/tests.py
+++ b/prompt/tests.py
@@ -175,3 +175,96 @@ class DeleteMessagePairTest(TestCase):
self.client.force_login(other)
resp = self.client.delete(f"/api/prompt/messages/{self.asst_msg.id}/pair")
self.assertEqual(resp.status_code, 403)
+
+
+class PromptHistoryTest(TestCase):
+ def setUp(self):
+ self.user = _make_user("history-user")
+ self.other = _make_user("history-other")
+ self.task = _make_task()
+ self.other_task = Task.objects.create(
+ title="Other Task", task_type="challenge", display=2, content=""
+ )
+
+ def _pair(
+ self,
+ user,
+ task,
+ prompt,
+ source="conversation",
+ html="page",
+ css="main { color: red; }",
+ js="",
+ ):
+ conv = Conversation.objects.create(user=user, task=task)
+ user_msg = Message.objects.create(
+ conversation=conv,
+ role="user",
+ source=source,
+ content=prompt,
+ )
+ asst_msg = Message.objects.create(
+ conversation=conv,
+ role="assistant",
+ source=source,
+ content="" if source == "manual" else "answer",
+ code_html=html,
+ code_css=css,
+ code_js=js,
+ )
+ return user_msg, asst_msg
+
+ def test_history_returns_ai_and_manual_prompt_rounds_with_page_code(self):
+ ai_user, ai_asst = self._pair(self.user, self.task, "做一个登录页")
+ manual_user, manual_asst = self._pair(
+ self.user,
+ self.task,
+ "我让外部 AI 做一个卡片",
+ source="manual",
+ html="",
+ )
+
+ self.client.force_login(self.user)
+ resp = self.client.get(f"/api/prompt/history/{self.task.id}")
+
+ self.assertEqual(resp.status_code, 200)
+ data = resp.json()
+ ids = {item["user_message_id"] for item in data}
+ self.assertEqual(ids, {ai_user.id, manual_user.id})
+ by_source = {item["source"]: item for item in data}
+ self.assertEqual(by_source["conversation"]["assistant_message_id"], ai_asst.id)
+ self.assertEqual(by_source["conversation"]["prompt"], "做一个登录页")
+ self.assertEqual(by_source["manual"]["assistant_message_id"], manual_asst.id)
+ self.assertEqual(by_source["manual"]["code_html"], "")
+ self.assertNotIn("content", by_source["conversation"])
+
+ def test_history_is_scoped_to_current_user_and_task(self):
+ own_user, _ = self._pair(self.user, self.task, "自己的提示词")
+ self._pair(self.other, self.task, "别人的提示词")
+ self._pair(self.user, self.other_task, "其他任务提示词")
+
+ self.client.force_login(self.user)
+ resp = self.client.get(f"/api/prompt/history/{self.task.id}")
+
+ self.assertEqual(resp.status_code, 200)
+ data = resp.json()
+ self.assertEqual(len(data), 1)
+ self.assertEqual(data[0]["user_message_id"], own_user.id)
+ self.assertEqual(data[0]["prompt"], "自己的提示词")
+
+ def test_history_keeps_rounds_without_page_code(self):
+ conv = Conversation.objects.create(user=self.user, task=self.task)
+ user_msg = Message.objects.create(conversation=conv, role="user", content="只聊天")
+ asst_msg = Message.objects.create(conversation=conv, role="assistant", content="没有代码")
+
+ self.client.force_login(self.user)
+ resp = self.client.get(f"/api/prompt/history/{self.task.id}")
+
+ self.assertEqual(resp.status_code, 200)
+ data = resp.json()
+ self.assertEqual(len(data), 1)
+ self.assertEqual(data[0]["user_message_id"], user_msg.id)
+ self.assertEqual(data[0]["assistant_message_id"], asst_msg.id)
+ self.assertIsNone(data[0]["code_html"])
+ self.assertIsNone(data[0]["code_css"])
+ self.assertIsNone(data[0]["code_js"])