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="
card
", + ) + + 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"], "
card
") + 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"])