fix
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user