This commit is contained in:
2026-05-09 01:30:39 -06:00
parent 48e6ddeeac
commit 4660b51de3
4 changed files with 47 additions and 63 deletions

View File

@@ -6,7 +6,7 @@ from ninja.errors import HttpError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Count from django.db.models import Count, Prefetch
from .models import Conversation, Message from .models import Conversation, Message
from .schemas import ConversationOut, MessageOut, PromptHistoryItemOut from .schemas import ConversationOut, MessageOut, PromptHistoryItemOut
from account.models import RoleChoices from account.models import RoleChoices
@@ -66,11 +66,13 @@ def list_prompt_history(request, task_id: int):
conversations = Conversation.objects.filter( conversations = Conversation.objects.filter(
user=request.user, user=request.user,
task_id=task_id, task_id=task_id,
).prefetch_related("messages") ).prefetch_related(
Prefetch("messages", queryset=Message.objects.order_by("created", "id"))
)
items = [] items = []
for conv in conversations: for conv in conversations:
messages = list(conv.messages.all().order_by("created", "id")) messages = list(conv.messages.all())
for idx, user_msg in enumerate(messages): for idx, user_msg in enumerate(messages):
if user_msg.role != "user": if user_msg.role != "user":
continue continue

View File

@@ -3,32 +3,32 @@ from django.conf import settings
from openai import AsyncOpenAI from openai import AsyncOpenAI
SYSTEM_PROMPT = """你是一个网页生成助手。根据用户的需求描述,生成 HTML、CSS 和 JavaScript 代码。 SYSTEM_PROMPT = """你是一个网页生成助手。根据用户的需求描述,生成网页代码。
规则: 规则:
1. 始终使用三个独立的代码块返回代码,分别用 ```html、```css、```js 标记 1. 使用一个 ```html 代码块返回所有代码
2. HTML 代码只需要 body 内的内容,不需要完整的 HTML 文档结构 2. HTML 代码只需要 body 内的内容,不需要完整的 HTML 文档结构
3. CSS 和 JS 可以为空,但仍然需要返回空的代码块 3. CSS 样式写在 <style> 标签内JavaScript 写在 <script> 标签内,都放在代码块
4. 用中文回复,先简要说明你做了什么,然后给出代码 4. 用中文回复,先简要说明你做了什么,然后给出代码
5. 在已有代码基础上修改时,返回完整的修改后代码,不要只返回片段 5. 在已有代码基础上修改时,返回完整的修改后代码,不要只返回片段
6. 由于任何外部链接都被屏蔽,使用纯 HTML、CSS 和 JS 实现功能,不要依赖外部库 6. 由于任何外部链接都被屏蔽,使用纯 HTML、CSS 和 JS 实现功能,不要依赖外部库
输出格式示例(必须严格遵守,个代码块缺一不可 输出格式示例(必须严格遵守,只用一个代码块):
好的,我为你创建了一个点击按钮变色的示例。 好的,我为你创建了一个点击按钮变色的示例。
```html ```html
<button id="btn">点击我</button> <style>
```
```css
button { padding: 8px 16px; } button { padding: 8px 16px; }
``` </style>
```js <button id="btn">点击我</button>
<script>
document.getElementById('btn').onclick = function() { document.getElementById('btn').onclick = function() {
this.style.background = 'red'; this.style.background = 'red';
}; };
</script>
```""" ```"""
GUIDANCE_SYSTEM_PROMPT = """你是一个提示词写作教练,帮助学生写出清晰、具体的网页需求描述。 GUIDANCE_SYSTEM_PROMPT = """你是一个提示词写作教练,帮助学生写出清晰、具体的网页需求描述。
@@ -61,13 +61,6 @@ NON_THINKING_EXTRA_BODY = {"thinking": {"type": "disabled"}}
ARK_MODELS = {"doubao-seed-2-0-lite-260215"} ARK_MODELS = {"doubao-seed-2-0-lite-260215"}
def build_messages(history: list[dict]) -> list[dict]:
"""Build the message list for the LLM API call."""
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.extend(history)
return messages
def _get_client(model: str) -> tuple[AsyncOpenAI, str]: def _get_client(model: str) -> tuple[AsyncOpenAI, str]:
"""Return (client, model_id) for the given model name.""" """Return (client, model_id) for the given model name."""
requested_model = model or DEFAULT_MODEL requested_model = model or DEFAULT_MODEL
@@ -114,19 +107,12 @@ def _chat_completion_kwargs(
return kwargs return kwargs
async def stream_chat(history: list[dict], model: str = ""): async def _stream_completion(messages: list[dict], model: str = ""):
"""Stream chat completion from the LLM. Yields content chunks."""
messages = build_messages(history)
client, resolved_model = _get_client(model) client, resolved_model = _get_client(model)
requested_model = model or DEFAULT_MODEL requested_model = model or DEFAULT_MODEL
async with client as c: async with client as c:
stream = await c.chat.completions.create( stream = await c.chat.completions.create(
**_chat_completion_kwargs( **_chat_completion_kwargs(requested_model, resolved_model, messages, stream=True),
requested_model,
resolved_model,
messages,
stream=True,
),
) )
async for chunk in stream: async for chunk in stream:
delta = chunk.choices[0].delta delta = chunk.choices[0].delta
@@ -134,8 +120,15 @@ async def stream_chat(history: list[dict], model: str = ""):
yield delta.content yield delta.content
async def stream_chat(history: list[dict], model: str = ""):
"""Stream chat completion from the LLM. Yields content chunks."""
messages = [{"role": "system", "content": SYSTEM_PROMPT}, *history]
async for chunk in _stream_completion(messages, model):
yield chunk
def extract_code(text: str) -> dict: def extract_code(text: str) -> dict:
"""Extract HTML, CSS, JS code blocks from AI response text.""" """Extract code from AI response. Supports single HTML block (new) or separate html/css/js blocks (legacy)."""
result = {"html": None, "css": None, "js": None} result = {"html": None, "css": None, "js": None}
pattern = r"```(html|css|js|javascript|typescript|ts|jsx|tsx)\s*\n(.*?)```" pattern = r"```(html|css|js|javascript|typescript|ts|jsx|tsx)\s*\n(.*?)```"
matches = re.findall(pattern, text, re.DOTALL | re.IGNORECASE) matches = re.findall(pattern, text, re.DOTALL | re.IGNORECASE)
@@ -146,17 +139,21 @@ def extract_code(text: str) -> dict:
if lang in result and result[lang] is None: if lang in result and result[lang] is None:
result[lang] = code.strip() result[lang] = code.strip()
# Fallback: extract <style> and <script> from HTML block # Single HTML block: extract <style>/<script> contents and strip them from html
if result["html"]: if result["html"] and result["css"] is None and result["js"] is None:
if result["css"] is None: html = result["html"]
style_match = re.search(r"<style[^>]*>(.*?)</style>", result["html"], re.DOTALL | re.IGNORECASE)
style_match = re.search(r"<style[^>]*>(.*?)</style>", html, re.DOTALL | re.IGNORECASE)
if style_match: if style_match:
result["css"] = style_match.group(1).strip() result["css"] = style_match.group(1).strip()
html = re.sub(r"<style[^>]*>.*?</style>", "", html, flags=re.DOTALL | re.IGNORECASE)
if result["js"] is None: script_match = re.search(r"<script[^>]*>(.*?)</script>", html, re.DOTALL | re.IGNORECASE)
script_match = re.search(r"<script[^>]*>(.*?)</script>", result["html"], re.DOTALL | re.IGNORECASE)
if script_match: if script_match:
result["js"] = script_match.group(1).strip() result["js"] = script_match.group(1).strip()
html = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE)
result["html"] = html.strip()
return result return result
@@ -169,20 +166,6 @@ def parse_guidance_response(full_response: str) -> tuple[str, bool]:
async def stream_guidance(history: list[dict]): async def stream_guidance(history: list[dict]):
"""Stream guidance coaching response. Yields content chunks.""" """Stream guidance coaching response. Yields content chunks."""
messages = [{"role": "system", "content": GUIDANCE_SYSTEM_PROMPT}] messages = [{"role": "system", "content": GUIDANCE_SYSTEM_PROMPT}, *history]
messages.extend(history) async for chunk in _stream_completion(messages):
client, model = _get_client("") yield chunk
requested_model = DEFAULT_MODEL
async with client as c:
stream = await c.chat.completions.create(
**_chat_completion_kwargs(
requested_model,
model,
messages,
stream=True,
),
)
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
yield delta.content

View File

@@ -47,6 +47,6 @@ class ConversationOut(Schema):
"task_id": conv.task_id, "task_id": conv.task_id,
"task_title": conv.task.title, "task_title": conv.task.title,
"is_active": conv.is_active, "is_active": conv.is_active,
"message_count": conv.messages.count(), "message_count": conv.msg_count if hasattr(conv, "msg_count") else conv.messages.count(),
"created": conv.created.isoformat(), "created": conv.created.isoformat(),
} }

View File

@@ -22,6 +22,7 @@ from django.db.models import (
) )
from account.decorators import admin_required from account.decorators import admin_required
from prompt.models import Conversation, Message from prompt.models import Conversation, Message
from .classifier import classify_conversation_messages
from .schemas import ( from .schemas import (
@@ -150,8 +151,6 @@ def create_submission(request, payload: SubmissionIn):
code_js=payload.js, code_js=payload.js,
source="manual", source="manual",
) )
from .classifier import classify_conversation_messages
threading.Thread(target=classify_conversation_messages, args=(conversation.id,), daemon=True).start()
else: else:
conversation = ( conversation = (
Conversation.objects.filter(user=request.user, task=task) Conversation.objects.filter(user=request.user, task=task)
@@ -159,8 +158,8 @@ def create_submission(request, payload: SubmissionIn):
.order_by("-msg_count", "-created") .order_by("-msg_count", "-created")
.first() .first()
) )
if conversation: if conversation:
from .classifier import classify_conversation_messages
threading.Thread(target=classify_conversation_messages, args=(conversation.id,), daemon=True).start() threading.Thread(target=classify_conversation_messages, args=(conversation.id,), daemon=True).start()
submission = Submission.objects.create( submission = Submission.objects.create(