ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
+219
@@ -0,0 +1,219 @@
|
||||
"""实验 5-11:对话式界面定制系统 —— FastAPI 后端。
|
||||
|
||||
一个最小的 chatbot 后端:前端把用户消息 POST 到 /api/chat,后端返回回复。
|
||||
开发模式下用 `uvicorn main:app --reload` 启动,改动后端代码会自动 reload
|
||||
(对应书中所说的"FastAPI 的热加载")。
|
||||
|
||||
两种回复模式(默认保持"回声式"占位,聚焦 UI 定制这一主题):
|
||||
- **echo(默认)**:后端把用户消息原样回显,无需任何模型 Key,开箱即用;
|
||||
- **llm(可选)**:设置模型后走真实 LLM 对话,让运行起来的 chatbot 真会说话。
|
||||
通过命令行 `--model` 或环境变量 `CHAT_MODEL` 打开,复用与 agent.py 相同的
|
||||
OPENAI_API_KEY / OPENAI_BASE_URL 配置。
|
||||
|
||||
启动方式(二选一,行为一致):
|
||||
uvicorn main:app --reload --port 8000 # 书中示例:模块级 app + uvicorn 热加载
|
||||
python main.py --reload --port 8000 # 本文件自带的命令行入口(见 --help)
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
try: # dotenv 可选:让 --model 模式也能读到 .env 里的 OPENAI_API_KEY
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
app = FastAPI(title="Conversational UI Backend")
|
||||
|
||||
# 允许前端(Vite dev server, 5173)直接跨域访问,方便本地开发。
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
def _chat_model() -> str:
|
||||
"""当前回复模式:返回模型名则走真实 LLM,返回空串则为默认 echo 模式。
|
||||
|
||||
从环境变量 CHAT_MODEL 读取(而非模块级常量),这样即使 `--reload` 触发的
|
||||
子进程重新 import 本模块,也能透过环境变量拿到命令行传入的模型设置。
|
||||
"""
|
||||
return (os.getenv("CHAT_MODEL") or "").strip()
|
||||
|
||||
|
||||
def _llm_reply(message: str, model: str) -> str:
|
||||
"""走真实 LLM 生成回复,复用 agent.py 同款 OPENAI_* 配置。
|
||||
|
||||
任何异常都降级为清晰的提示(绝不编造回复),保证前端不至于白屏。
|
||||
"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except Exception:
|
||||
return "(未安装 openai 依赖,无法启用 LLM 模式;已回退占位回复)"
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
base_url = os.getenv("OPENAI_BASE_URL")
|
||||
orkey = os.getenv("OPENROUTER_API_KEY")
|
||||
# 通用 OpenRouter 兜底:无直连 key,或 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。
|
||||
prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
|
||||
if prefer_or or (not api_key and orkey):
|
||||
api_key, base_url = orkey, "https://openrouter.ai/api/v1"
|
||||
if "/" not in model:
|
||||
model = ("openai/" + model) if model.lower().startswith(("gpt-", "o1", "o3", "o4")) else "openai/gpt-5.6-luna"
|
||||
if not api_key:
|
||||
return "(未配置 OPENAI_API_KEY 或 OPENROUTER_API_KEY,无法启用 LLM 模式;已回退占位回复)"
|
||||
|
||||
client_kwargs = {"api_key": api_key, "timeout": 60.0, "max_retries": 2}
|
||||
if base_url:
|
||||
client_kwargs["base_url"] = base_url
|
||||
|
||||
try:
|
||||
client = OpenAI(**client_kwargs)
|
||||
resp = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": "你是一个乐于助人的中文智能助手,回答简洁友好。"},
|
||||
{"role": "user", "content": message},
|
||||
],
|
||||
temperature=(1 if any(k in (model or "").lower()
|
||||
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
|
||||
else 0.7),
|
||||
)
|
||||
return resp.choices[0].message.content or "(模型返回了空回复)"
|
||||
except Exception as e: # 网络/鉴权/模型名等问题都在此兜底
|
||||
return f"(调用 LLM 失败:{e};已回退占位回复)"
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
model = _chat_model()
|
||||
return {"status": "ok", "mode": "llm" if model else "echo", "model": model or None}
|
||||
|
||||
|
||||
@app.post("/api/chat")
|
||||
def chat(req: ChatRequest):
|
||||
"""默认回声式回复;设置 CHAT_MODEL 后走真实 LLM 对话。
|
||||
|
||||
本实验聚焦"对话式 UI 定制",后端逻辑刻意保持最小;
|
||||
如需真实客服体验,用 `python main.py --model <模型名>` 打开 LLM 模式即可。
|
||||
"""
|
||||
model = _chat_model()
|
||||
if model:
|
||||
return {"reply": _llm_reply(req.message, model)}
|
||||
return {"reply": f"我收到了你的消息:{req.message}"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 命令行入口:让后端既能 `uvicorn main:app --reload` 启动,也能 `python main.py`
|
||||
# 启动,并通过参数控制 host/port/热加载/回复模式/日志。
|
||||
# ---------------------------------------------------------------------------
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="main.py",
|
||||
description="实验 5-11:对话式界面定制系统 —— FastAPI 后端(最小 chatbot 服务)。"
|
||||
"为可对话定制的前端提供 /api/chat 载体,开发模式下配合 --reload 演示后端热加载。",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="127.0.0.1",
|
||||
help="监听地址;对外可用 0.0.0.0。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="监听端口(前端 vite.config.js 默认把 /api 代理到 8000)。",
|
||||
)
|
||||
reload_group = parser.add_mutually_exclusive_group()
|
||||
reload_group.add_argument(
|
||||
"--reload",
|
||||
dest="reload",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="开启热加载:改动后端 .py 自动重启(开发默认开启)。",
|
||||
)
|
||||
reload_group.add_argument(
|
||||
"--no-reload",
|
||||
dest="reload",
|
||||
action="store_false",
|
||||
help="关闭热加载(更接近生产运行)。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default=os.getenv("CHAT_MODEL") or None,
|
||||
metavar="NAME",
|
||||
help="打开真实 LLM 对话并指定模型名(如 gpt-5.6-luna);"
|
||||
"缺省则为默认的 echo 回声模式。也可用环境变量 CHAT_MODEL 设置。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
default="info",
|
||||
choices=["critical", "error", "warning", "info", "debug", "trace"],
|
||||
help="uvicorn 日志/输出级别。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--print-config",
|
||||
action="store_true",
|
||||
help="只打印生效配置(JSON)后退出,不真正监听端口(便于无网络/无端口环境下校验)。",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import json
|
||||
|
||||
args = parse_args(argv)
|
||||
|
||||
# 把 --model 写回环境变量:这样 --reload 派生的子进程重新 import 本模块时,
|
||||
# 也能通过 CHAT_MODEL 感知到 LLM 模式(子进程不共享本函数的局部状态)。
|
||||
if args.model:
|
||||
os.environ["CHAT_MODEL"] = args.model
|
||||
else:
|
||||
os.environ.pop("CHAT_MODEL", None)
|
||||
|
||||
config = {
|
||||
"host": args.host,
|
||||
"port": args.port,
|
||||
"reload": args.reload,
|
||||
"mode": "llm" if args.model else "echo",
|
||||
"model": args.model,
|
||||
"log_level": args.log_level,
|
||||
}
|
||||
|
||||
if args.print_config:
|
||||
print(json.dumps(config, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
import uvicorn
|
||||
|
||||
print(
|
||||
f"启动 FastAPI 后端:http://{args.host}:{args.port}"
|
||||
f" 模式={config['mode']}"
|
||||
f" 热加载={'开' if args.reload else '关'}"
|
||||
)
|
||||
# 用 import string 才能在 --reload 下工作;从 backend/ 目录运行 `python main.py`。
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=args.reload,
|
||||
log_level=args.log_level,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>对话式界面定制系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1797
File diff suppressed because it is too large
Load Diff
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "conversational-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "实验 5-11:对话式界面定制系统 —— React(Vite) 前端",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// ===========================================================================
|
||||
// 基础 chatbot 界面。
|
||||
// 这是一个"可被自然语言定制"的最小 React 应用:
|
||||
// - 标题文案、按钮文字等 UI 文本都写在这里(Agent 可按需求改文案);
|
||||
// - 颜色、字体、布局等样式集中在 theme.css(Agent 可按需求改样式)。
|
||||
// 用户在对话中说"把发送按钮改成蓝色 / 换成等宽字体 / 标题改成 XXX",
|
||||
// Agent 会定位并修改这些源码文件,Vite HMR 让改动即时生效。
|
||||
// ===========================================================================
|
||||
|
||||
// UI 文案(Agent 定制"文案"需求时修改这里)
|
||||
const HEADER_TITLE = "智能助手";
|
||||
const HEADER_SUBTITLE = "有什么可以帮你的吗?";
|
||||
const SEND_BUTTON_LABEL = "发送";
|
||||
|
||||
export default function App() {
|
||||
const [messages, setMessages] = useState([
|
||||
{ role: "assistant", text: "你好!我是你的智能助手,随时为你服务。" },
|
||||
]);
|
||||
const [input, setInput] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSend() {
|
||||
const text = input.trim();
|
||||
if (!text || loading) return;
|
||||
setMessages((m) => [...m, { role: "user", text }]);
|
||||
setInput("");
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: text }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
setMessages((m) => [...m, { role: "assistant", text: data.reply }]);
|
||||
} catch (e) {
|
||||
setMessages((m) => [
|
||||
...m,
|
||||
{ role: "assistant", text: "(后端未连接,这是本地占位回复)" },
|
||||
]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="header">
|
||||
<h1 className="header-title">{HEADER_TITLE}</h1>
|
||||
<p className="header-subtitle">{HEADER_SUBTITLE}</p>
|
||||
</header>
|
||||
|
||||
<main className="chat-window">
|
||||
{messages.map((m, i) => (
|
||||
<div key={i} className={`bubble bubble-${m.role}`}>
|
||||
{m.text}
|
||||
</div>
|
||||
))}
|
||||
{loading && <div className="bubble bubble-assistant">思考中…</div>}
|
||||
</main>
|
||||
|
||||
<footer className="composer">
|
||||
<input
|
||||
className="composer-input"
|
||||
value={input}
|
||||
placeholder="输入消息…"
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
||||
/>
|
||||
<button className="send-button" onClick={handleSend}>
|
||||
{SEND_BUTTON_LABEL}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.jsx";
|
||||
import "./theme.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/* ==========================================================================
|
||||
主题样式表(Agent 定制"颜色 / 字体 / 布局"需求时修改这里)
|
||||
为方便自然语言定制,关键视觉变量集中在 :root 里。
|
||||
========================================================================== */
|
||||
:root {
|
||||
--color-primary: #2563eb; /* 主色(发送按钮、用户气泡)——改为蓝色 */
|
||||
--color-primary-text: #ffffff; /* 主色上的文字颜色 */
|
||||
--color-bg: #f5f5f5; /* 页面背景 */
|
||||
--color-panel: #ffffff; /* 面板/卡片背景 */
|
||||
--color-text: #1f2937; /* 正文文字 */
|
||||
--font-family: system-ui, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-panel);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
flex: 1;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 75%;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.bubble-assistant {
|
||||
align-self: flex-start;
|
||||
background: #eef2f5;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.bubble-user {
|
||||
align-self: flex-end;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.composer-input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// Vite 开发服务器默认开启 HMR(热模块替换)。
|
||||
// 当 Agent 修改 src/ 下的源码时,浏览器无需整页刷新即可即时看到界面变化。
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
// 后端 FastAPI 跑在 8000,前端把 /api 请求代理过去,避免跨域。
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:55409",
|
||||
},
|
||||
},
|
||||
});
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
"""实验 5-11:对话式界面定制系统 —— FastAPI 后端。
|
||||
|
||||
一个最小的 chatbot 后端:前端把用户消息 POST 到 /api/chat,后端返回回复。
|
||||
开发模式下用 `uvicorn main:app --reload` 启动,改动后端代码会自动 reload
|
||||
(对应书中所说的"FastAPI 的热加载")。
|
||||
|
||||
两种回复模式(默认保持"回声式"占位,聚焦 UI 定制这一主题):
|
||||
- **echo(默认)**:后端把用户消息原样回显,无需任何模型 Key,开箱即用;
|
||||
- **llm(可选)**:设置模型后走真实 LLM 对话,让运行起来的 chatbot 真会说话。
|
||||
通过命令行 `--model` 或环境变量 `CHAT_MODEL` 打开,复用与 agent.py 相同的
|
||||
OPENAI_API_KEY / OPENAI_BASE_URL 配置。
|
||||
|
||||
启动方式(二选一,行为一致):
|
||||
uvicorn main:app --reload --port 8000 # 书中示例:模块级 app + uvicorn 热加载
|
||||
python main.py --reload --port 8000 # 本文件自带的命令行入口(见 --help)
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
try: # dotenv 可选:让 --model 模式也能读到 .env 里的 OPENAI_API_KEY
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
app = FastAPI(title="Conversational UI Backend")
|
||||
|
||||
# 允许前端(Vite dev server, 5173)直接跨域访问,方便本地开发。
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
def _chat_model() -> str:
|
||||
"""当前回复模式:返回模型名则走真实 LLM,返回空串则为默认 echo 模式。
|
||||
|
||||
从环境变量 CHAT_MODEL 读取(而非模块级常量),这样即使 `--reload` 触发的
|
||||
子进程重新 import 本模块,也能透过环境变量拿到命令行传入的模型设置。
|
||||
"""
|
||||
return (os.getenv("CHAT_MODEL") or "").strip()
|
||||
|
||||
|
||||
def _llm_reply(message: str, model: str) -> str:
|
||||
"""走真实 LLM 生成回复,复用 agent.py 同款 OPENAI_* 配置。
|
||||
|
||||
任何异常都降级为清晰的提示(绝不编造回复),保证前端不至于白屏。
|
||||
"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except Exception:
|
||||
return "(未安装 openai 依赖,无法启用 LLM 模式;已回退占位回复)"
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
base_url = os.getenv("OPENAI_BASE_URL")
|
||||
orkey = os.getenv("OPENROUTER_API_KEY")
|
||||
# 通用 OpenRouter 兜底:无直连 key,或 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。
|
||||
prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
|
||||
if prefer_or or (not api_key and orkey):
|
||||
api_key, base_url = orkey, "https://openrouter.ai/api/v1"
|
||||
if "/" not in model:
|
||||
model = ("openai/" + model) if model.lower().startswith(("gpt-", "o1", "o3", "o4")) else "openai/gpt-5.6-luna"
|
||||
if not api_key:
|
||||
return "(未配置 OPENAI_API_KEY 或 OPENROUTER_API_KEY,无法启用 LLM 模式;已回退占位回复)"
|
||||
|
||||
client_kwargs = {"api_key": api_key, "timeout": 60.0, "max_retries": 2}
|
||||
if base_url:
|
||||
client_kwargs["base_url"] = base_url
|
||||
|
||||
try:
|
||||
client = OpenAI(**client_kwargs)
|
||||
resp = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": "你是一个乐于助人的中文智能助手,回答简洁友好。"},
|
||||
{"role": "user", "content": message},
|
||||
],
|
||||
temperature=(1 if any(k in (model or "").lower()
|
||||
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
|
||||
else 0.7),
|
||||
)
|
||||
return resp.choices[0].message.content or "(模型返回了空回复)"
|
||||
except Exception as e: # 网络/鉴权/模型名等问题都在此兜底
|
||||
return f"(调用 LLM 失败:{e};已回退占位回复)"
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
model = _chat_model()
|
||||
return {"status": "ok", "mode": "llm" if model else "echo", "model": model or None}
|
||||
|
||||
|
||||
@app.post("/api/chat")
|
||||
def chat(req: ChatRequest):
|
||||
"""默认回声式回复;设置 CHAT_MODEL 后走真实 LLM 对话。
|
||||
|
||||
本实验聚焦"对话式 UI 定制",后端逻辑刻意保持最小;
|
||||
如需真实客服体验,用 `python main.py --model <模型名>` 打开 LLM 模式即可。
|
||||
"""
|
||||
model = _chat_model()
|
||||
if model:
|
||||
return {"reply": _llm_reply(req.message, model)}
|
||||
return {"reply": f"我收到了你的消息:{req.message}"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 命令行入口:让后端既能 `uvicorn main:app --reload` 启动,也能 `python main.py`
|
||||
# 启动,并通过参数控制 host/port/热加载/回复模式/日志。
|
||||
# ---------------------------------------------------------------------------
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="main.py",
|
||||
description="实验 5-11:对话式界面定制系统 —— FastAPI 后端(最小 chatbot 服务)。"
|
||||
"为可对话定制的前端提供 /api/chat 载体,开发模式下配合 --reload 演示后端热加载。",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="127.0.0.1",
|
||||
help="监听地址;对外可用 0.0.0.0。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="监听端口(前端 vite.config.js 默认把 /api 代理到 8000)。",
|
||||
)
|
||||
reload_group = parser.add_mutually_exclusive_group()
|
||||
reload_group.add_argument(
|
||||
"--reload",
|
||||
dest="reload",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="开启热加载:改动后端 .py 自动重启(开发默认开启)。",
|
||||
)
|
||||
reload_group.add_argument(
|
||||
"--no-reload",
|
||||
dest="reload",
|
||||
action="store_false",
|
||||
help="关闭热加载(更接近生产运行)。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default=os.getenv("CHAT_MODEL") or None,
|
||||
metavar="NAME",
|
||||
help="打开真实 LLM 对话并指定模型名(如 gpt-5.6-luna);"
|
||||
"缺省则为默认的 echo 回声模式。也可用环境变量 CHAT_MODEL 设置。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
default="info",
|
||||
choices=["critical", "error", "warning", "info", "debug", "trace"],
|
||||
help="uvicorn 日志/输出级别。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--print-config",
|
||||
action="store_true",
|
||||
help="只打印生效配置(JSON)后退出,不真正监听端口(便于无网络/无端口环境下校验)。",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import json
|
||||
|
||||
args = parse_args(argv)
|
||||
|
||||
# 把 --model 写回环境变量:这样 --reload 派生的子进程重新 import 本模块时,
|
||||
# 也能通过 CHAT_MODEL 感知到 LLM 模式(子进程不共享本函数的局部状态)。
|
||||
if args.model:
|
||||
os.environ["CHAT_MODEL"] = args.model
|
||||
else:
|
||||
os.environ.pop("CHAT_MODEL", None)
|
||||
|
||||
config = {
|
||||
"host": args.host,
|
||||
"port": args.port,
|
||||
"reload": args.reload,
|
||||
"mode": "llm" if args.model else "echo",
|
||||
"model": args.model,
|
||||
"log_level": args.log_level,
|
||||
}
|
||||
|
||||
if args.print_config:
|
||||
print(json.dumps(config, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
import uvicorn
|
||||
|
||||
print(
|
||||
f"启动 FastAPI 后端:http://{args.host}:{args.port}"
|
||||
f" 模式={config['mode']}"
|
||||
f" 热加载={'开' if args.reload else '关'}"
|
||||
)
|
||||
# 用 import string 才能在 --reload 下工作;从 backend/ 目录运行 `python main.py`。
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=args.reload,
|
||||
log_level=args.log_level,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>对话式界面定制系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1797
File diff suppressed because it is too large
Load Diff
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "conversational-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "实验 5-11:对话式界面定制系统 —— React(Vite) 前端",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// ===========================================================================
|
||||
// 基础 chatbot 界面。
|
||||
// 这是一个"可被自然语言定制"的最小 React 应用:
|
||||
// - 标题文案、按钮文字等 UI 文本都写在这里(Agent 可按需求改文案);
|
||||
// - 颜色、字体、布局等样式集中在 theme.css(Agent 可按需求改样式)。
|
||||
// 用户在对话中说"把发送按钮改成蓝色 / 换成等宽字体 / 标题改成 XXX",
|
||||
// Agent 会定位并修改这些源码文件,Vite HMR 让改动即时生效。
|
||||
// ===========================================================================
|
||||
|
||||
// UI 文案(Agent 定制"文案"需求时修改这里)
|
||||
const HEADER_TITLE = "智能助手";
|
||||
const HEADER_SUBTITLE = "有什么可以帮你的吗?";
|
||||
const SEND_BUTTON_LABEL = "发送";
|
||||
|
||||
export default function App() {
|
||||
const [messages, setMessages] = useState([
|
||||
{ role: "assistant", text: "你好!我是你的智能助手,随时为你服务。" },
|
||||
]);
|
||||
const [input, setInput] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSend() {
|
||||
const text = input.trim();
|
||||
if (!text || loading) return;
|
||||
setMessages((m) => [...m, { role: "user", text }]);
|
||||
setInput("");
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: text }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
setMessages((m) => [...m, { role: "assistant", text: data.reply }]);
|
||||
} catch (e) {
|
||||
setMessages((m) => [
|
||||
...m,
|
||||
{ role: "assistant", text: "(后端未连接,这是本地占位回复)" },
|
||||
]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="header">
|
||||
<h1 className="header-title">{HEADER_TITLE}</h1>
|
||||
<p className="header-subtitle">{HEADER_SUBTITLE}</p>
|
||||
</header>
|
||||
|
||||
<main className="chat-window">
|
||||
{messages.map((m, i) => (
|
||||
<div key={i} className={`bubble bubble-${m.role}`}>
|
||||
{m.text}
|
||||
</div>
|
||||
))}
|
||||
{loading && <div className="bubble bubble-assistant">思考中…</div>}
|
||||
</main>
|
||||
|
||||
<footer className="composer">
|
||||
<input
|
||||
className="composer-input"
|
||||
value={input}
|
||||
placeholder="输入消息…"
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
||||
/>
|
||||
<button className="send-button" onClick={handleSend}>
|
||||
{SEND_BUTTON_LABEL}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.jsx";
|
||||
import "./theme.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/* ==========================================================================
|
||||
主题样式表(Agent 定制"颜色 / 字体 / 布局"需求时修改这里)
|
||||
为方便自然语言定制,关键视觉变量集中在 :root 里。
|
||||
========================================================================== */
|
||||
:root {
|
||||
--color-primary: #2563eb; /* 主色(发送按钮、用户气泡)——初始为绿色 */
|
||||
--color-primary-text: #ffffff; /* 主色上的文字颜色 */
|
||||
--color-bg: #f5f5f5; /* 页面背景 */
|
||||
--color-panel: #ffffff; /* 面板/卡片背景 */
|
||||
--color-text: #1f2937; /* 正文文字 */
|
||||
--font-family: monospace; /* 将字体改为等宽字体 */
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-panel);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
flex: 1;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 75%;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.bubble-assistant {
|
||||
align-self: flex-start;
|
||||
background: #eef2f5;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.bubble-user {
|
||||
align-self: flex-end;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.composer-input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// Vite 开发服务器默认开启 HMR(热模块替换)。
|
||||
// 当 Agent 修改 src/ 下的源码时,浏览器无需整页刷新即可即时看到界面变化。
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
// 后端 FastAPI 跑在 8000,前端把 /api 请求代理过去,避免跨域。
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:55670",
|
||||
},
|
||||
},
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
+219
@@ -0,0 +1,219 @@
|
||||
"""实验 5-11:对话式界面定制系统 —— FastAPI 后端。
|
||||
|
||||
一个最小的 chatbot 后端:前端把用户消息 POST 到 /api/chat,后端返回回复。
|
||||
开发模式下用 `uvicorn main:app --reload` 启动,改动后端代码会自动 reload
|
||||
(对应书中所说的"FastAPI 的热加载")。
|
||||
|
||||
两种回复模式(默认保持"回声式"占位,聚焦 UI 定制这一主题):
|
||||
- **echo(默认)**:后端把用户消息原样回显,无需任何模型 Key,开箱即用;
|
||||
- **llm(可选)**:设置模型后走真实 LLM 对话,让运行起来的 chatbot 真会说话。
|
||||
通过命令行 `--model` 或环境变量 `CHAT_MODEL` 打开,复用与 agent.py 相同的
|
||||
OPENAI_API_KEY / OPENAI_BASE_URL 配置。
|
||||
|
||||
启动方式(二选一,行为一致):
|
||||
uvicorn main:app --reload --port 8000 # 书中示例:模块级 app + uvicorn 热加载
|
||||
python main.py --reload --port 8000 # 本文件自带的命令行入口(见 --help)
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
try: # dotenv 可选:让 --model 模式也能读到 .env 里的 OPENAI_API_KEY
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
app = FastAPI(title="Conversational UI Backend")
|
||||
|
||||
# 允许前端(Vite dev server, 5173)直接跨域访问,方便本地开发。
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
def _chat_model() -> str:
|
||||
"""当前回复模式:返回模型名则走真实 LLM,返回空串则为默认 echo 模式。
|
||||
|
||||
从环境变量 CHAT_MODEL 读取(而非模块级常量),这样即使 `--reload` 触发的
|
||||
子进程重新 import 本模块,也能透过环境变量拿到命令行传入的模型设置。
|
||||
"""
|
||||
return (os.getenv("CHAT_MODEL") or "").strip()
|
||||
|
||||
|
||||
def _llm_reply(message: str, model: str) -> str:
|
||||
"""走真实 LLM 生成回复,复用 agent.py 同款 OPENAI_* 配置。
|
||||
|
||||
任何异常都降级为清晰的提示(绝不编造回复),保证前端不至于白屏。
|
||||
"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except Exception:
|
||||
return "(未安装 openai 依赖,无法启用 LLM 模式;已回退占位回复)"
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
base_url = os.getenv("OPENAI_BASE_URL")
|
||||
orkey = os.getenv("OPENROUTER_API_KEY")
|
||||
# 通用 OpenRouter 兜底:无直连 key,或 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。
|
||||
prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
|
||||
if prefer_or or (not api_key and orkey):
|
||||
api_key, base_url = orkey, "https://openrouter.ai/api/v1"
|
||||
if "/" not in model:
|
||||
model = ("openai/" + model) if model.lower().startswith(("gpt-", "o1", "o3", "o4")) else "openai/gpt-5.6-luna"
|
||||
if not api_key:
|
||||
return "(未配置 OPENAI_API_KEY 或 OPENROUTER_API_KEY,无法启用 LLM 模式;已回退占位回复)"
|
||||
|
||||
client_kwargs = {"api_key": api_key, "timeout": 60.0, "max_retries": 2}
|
||||
if base_url:
|
||||
client_kwargs["base_url"] = base_url
|
||||
|
||||
try:
|
||||
client = OpenAI(**client_kwargs)
|
||||
resp = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": "你是一个乐于助人的中文智能助手,回答简洁友好。"},
|
||||
{"role": "user", "content": message},
|
||||
],
|
||||
temperature=(1 if any(k in (model or "").lower()
|
||||
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
|
||||
else 0.7),
|
||||
)
|
||||
return resp.choices[0].message.content or "(模型返回了空回复)"
|
||||
except Exception as e: # 网络/鉴权/模型名等问题都在此兜底
|
||||
return f"(调用 LLM 失败:{e};已回退占位回复)"
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
model = _chat_model()
|
||||
return {"status": "ok", "mode": "llm" if model else "echo", "model": model or None}
|
||||
|
||||
|
||||
@app.post("/api/chat")
|
||||
def chat(req: ChatRequest):
|
||||
"""默认回声式回复;设置 CHAT_MODEL 后走真实 LLM 对话。
|
||||
|
||||
本实验聚焦"对话式 UI 定制",后端逻辑刻意保持最小;
|
||||
如需真实客服体验,用 `python main.py --model <模型名>` 打开 LLM 模式即可。
|
||||
"""
|
||||
model = _chat_model()
|
||||
if model:
|
||||
return {"reply": _llm_reply(req.message, model)}
|
||||
return {"reply": f"我收到了你的消息:{req.message}"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 命令行入口:让后端既能 `uvicorn main:app --reload` 启动,也能 `python main.py`
|
||||
# 启动,并通过参数控制 host/port/热加载/回复模式/日志。
|
||||
# ---------------------------------------------------------------------------
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="main.py",
|
||||
description="实验 5-11:对话式界面定制系统 —— FastAPI 后端(最小 chatbot 服务)。"
|
||||
"为可对话定制的前端提供 /api/chat 载体,开发模式下配合 --reload 演示后端热加载。",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="127.0.0.1",
|
||||
help="监听地址;对外可用 0.0.0.0。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="监听端口(前端 vite.config.js 默认把 /api 代理到 8000)。",
|
||||
)
|
||||
reload_group = parser.add_mutually_exclusive_group()
|
||||
reload_group.add_argument(
|
||||
"--reload",
|
||||
dest="reload",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="开启热加载:改动后端 .py 自动重启(开发默认开启)。",
|
||||
)
|
||||
reload_group.add_argument(
|
||||
"--no-reload",
|
||||
dest="reload",
|
||||
action="store_false",
|
||||
help="关闭热加载(更接近生产运行)。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default=os.getenv("CHAT_MODEL") or None,
|
||||
metavar="NAME",
|
||||
help="打开真实 LLM 对话并指定模型名(如 gpt-5.6-luna);"
|
||||
"缺省则为默认的 echo 回声模式。也可用环境变量 CHAT_MODEL 设置。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
default="info",
|
||||
choices=["critical", "error", "warning", "info", "debug", "trace"],
|
||||
help="uvicorn 日志/输出级别。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--print-config",
|
||||
action="store_true",
|
||||
help="只打印生效配置(JSON)后退出,不真正监听端口(便于无网络/无端口环境下校验)。",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import json
|
||||
|
||||
args = parse_args(argv)
|
||||
|
||||
# 把 --model 写回环境变量:这样 --reload 派生的子进程重新 import 本模块时,
|
||||
# 也能通过 CHAT_MODEL 感知到 LLM 模式(子进程不共享本函数的局部状态)。
|
||||
if args.model:
|
||||
os.environ["CHAT_MODEL"] = args.model
|
||||
else:
|
||||
os.environ.pop("CHAT_MODEL", None)
|
||||
|
||||
config = {
|
||||
"host": args.host,
|
||||
"port": args.port,
|
||||
"reload": args.reload,
|
||||
"mode": "llm" if args.model else "echo",
|
||||
"model": args.model,
|
||||
"log_level": args.log_level,
|
||||
}
|
||||
|
||||
if args.print_config:
|
||||
print(json.dumps(config, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
import uvicorn
|
||||
|
||||
print(
|
||||
f"启动 FastAPI 后端:http://{args.host}:{args.port}"
|
||||
f" 模式={config['mode']}"
|
||||
f" 热加载={'开' if args.reload else '关'}"
|
||||
)
|
||||
# 用 import string 才能在 --reload 下工作;从 backend/ 目录运行 `python main.py`。
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=args.reload,
|
||||
log_level=args.log_level,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+1
@@ -0,0 +1 @@
|
||||
:root{--color-primary: #2563eb;--color-primary-text: #ffffff;--color-bg: #f5f5f5;--color-panel: #ffffff;--color-text: #1f2937;--font-family: monospace;--radius: 12px}*{box-sizing:border-box}body{margin:0;background:var(--color-bg);color:var(--color-text);font-family:var(--font-family)}.app{max-width:640px;margin:0 auto;min-height:100vh;display:flex;flex-direction:column;background:var(--color-panel)}.header{padding:20px 24px;border-bottom:1px solid #e5e7eb}.header-title{margin:0;font-size:20px}.header-subtitle{margin:4px 0 0;font-size:13px;color:#6b7280}.chat-window{flex:1;padding:20px 24px;display:flex;flex-direction:column;gap:12px;overflow-y:auto}.bubble{max-width:75%;padding:10px 14px;border-radius:var(--radius);line-height:1.5;white-space:pre-wrap}.bubble-assistant{align-self:flex-start;background:#eef2f5;color:var(--color-text)}.bubble-user{align-self:flex-end;background:var(--color-primary);color:var(--color-primary-text)}.composer{display:flex;gap:8px;padding:16px 24px;border-top:1px solid #e5e7eb}.composer-input{flex:1;padding:10px 12px;border:1px solid #d1d5db;border-radius:var(--radius);font-size:14px;font-family:inherit}.send-button{padding:10px 20px;border:none;border-radius:var(--radius);background:var(--color-primary);color:var(--color-primary-text);font-size:14px;cursor:pointer}.send-button:hover{opacity:.9}
|
||||
+40
File diff suppressed because one or more lines are too long
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>对话式界面定制系统</title>
|
||||
<script type="module" crossorigin src="/assets/index-CvgOOKX3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CCn-3TaK.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>对话式界面定制系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1797
File diff suppressed because it is too large
Load Diff
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "conversational-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "实验 5-11:对话式界面定制系统 —— React(Vite) 前端",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// ===========================================================================
|
||||
// 基础 chatbot 界面。
|
||||
// 这是一个"可被自然语言定制"的最小 React 应用:
|
||||
// - 标题文案、按钮文字等 UI 文本都写在这里(Agent 可按需求改文案);
|
||||
// - 颜色、字体、布局等样式集中在 theme.css(Agent 可按需求改样式)。
|
||||
// 用户在对话中说"把发送按钮改成蓝色 / 换成等宽字体 / 标题改成 XXX",
|
||||
// Agent 会定位并修改这些源码文件,Vite HMR 让改动即时生效。
|
||||
// ===========================================================================
|
||||
|
||||
// UI 文案(Agent 定制"文案"需求时修改这里)
|
||||
const HEADER_TITLE = "我的专属客服";
|
||||
const HEADER_SUBTITLE = "有什么可以帮你的吗?";
|
||||
const SEND_BUTTON_LABEL = "发送";
|
||||
|
||||
export default function App() {
|
||||
const [messages, setMessages] = useState([
|
||||
{ role: "assistant", text: "你好!我是你的智能助手,随时为你服务。" },
|
||||
]);
|
||||
const [input, setInput] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSend() {
|
||||
const text = input.trim();
|
||||
if (!text || loading) return;
|
||||
setMessages((m) => [...m, { role: "user", text }]);
|
||||
setInput("");
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: text }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
setMessages((m) => [...m, { role: "assistant", text: data.reply }]);
|
||||
} catch (e) {
|
||||
setMessages((m) => [
|
||||
...m,
|
||||
{ role: "assistant", text: "(后端未连接,这是本地占位回复)" },
|
||||
]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="header">
|
||||
<h1 className="header-title">{HEADER_TITLE}</h1>
|
||||
<p className="header-subtitle">{HEADER_SUBTITLE}</p>
|
||||
</header>
|
||||
|
||||
<main className="chat-window">
|
||||
{messages.map((m, i) => (
|
||||
<div key={i} className={`bubble bubble-${m.role}`}>
|
||||
{m.text}
|
||||
</div>
|
||||
))}
|
||||
{loading && <div className="bubble bubble-assistant">思考中…</div>}
|
||||
</main>
|
||||
|
||||
<footer className="composer">
|
||||
<input
|
||||
className="composer-input"
|
||||
value={input}
|
||||
placeholder="输入消息…"
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
||||
/>
|
||||
<button className="send-button" onClick={handleSend}>
|
||||
{SEND_BUTTON_LABEL}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.jsx";
|
||||
import "./theme.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/* ==========================================================================
|
||||
主题样式表(Agent 定制"颜色 / 字体 / 布局"需求时修改这里)
|
||||
为方便自然语言定制,关键视觉变量集中在 :root 里。
|
||||
========================================================================== */
|
||||
:root {
|
||||
--color-primary: #2563eb; /* 主色(发送按钮、用户气泡)——改为蓝色 */
|
||||
--color-primary-text: #ffffff; /* 主色上的文字颜色 */
|
||||
--color-bg: #f5f5f5; /* 页面背景 */
|
||||
--color-panel: #ffffff; /* 面板/卡片背景 */
|
||||
--color-text: #1f2937; /* 正文文字 */
|
||||
--font-family: monospace; /* 将字体改为等宽字体 */
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-panel);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
flex: 1;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 75%;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.bubble-assistant {
|
||||
align-self: flex-start;
|
||||
background: #eef2f5;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.bubble-user {
|
||||
align-self: flex-end;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.composer-input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// Vite 开发服务器默认开启 HMR(热模块替换)。
|
||||
// 当 Agent 修改 src/ 下的源码时,浏览器无需整页刷新即可即时看到界面变化。
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
// 后端 FastAPI 跑在 8000,前端把 /api 请求代理过去,避免跨域。
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:56044",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"returncode": 0,
|
||||
"latency_s": 0.819,
|
||||
"stdout": "\n> conversational-ui-frontend@0.1.0 build\n> vite build\n\nvite v6.4.3 building for production...\ntransforming...\n✓ 27 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.42 kB │ gzip: 0.31 kB\ndist/assets/index-CCn-3TaK.css 1.35 kB │ gzip: 0.56 kB\ndist/assets/index-CvgOOKX3.js 145.07 kB │ gzip: 46.92 kB\n✓ built in 405ms\n",
|
||||
"stderr": ""
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,211 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"experiment": "5-11",
|
||||
"run_id": "20260729T212933Z-5_11-hmr",
|
||||
"started_at_utc": "2026-07-29T21:29:33.901387+00:00",
|
||||
"completed_at_utc": "2026-07-29T21:30:12.405005+00:00",
|
||||
"provider": "ark",
|
||||
"endpoint": "https://ark.cn-beijing.volces.com/api/v3",
|
||||
"model": "doubao-seed-1-6-flash-250615",
|
||||
"source": {
|
||||
"manuscript": "book/chapter5.md#实验-5-11",
|
||||
"campaign_sha256": "11972f611b1ce9cbeff086955c29e562b0e516b8b468a55d3c072ddafcf1d3dc"
|
||||
},
|
||||
"servers": {
|
||||
"frontend": {
|
||||
"command": [
|
||||
"npm",
|
||||
"run",
|
||||
"dev",
|
||||
"--",
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
"56043",
|
||||
"--strictPort"
|
||||
]
|
||||
},
|
||||
"backend": {
|
||||
"command": [
|
||||
"python",
|
||||
"main.py",
|
||||
"--reload",
|
||||
"--port",
|
||||
"56044"
|
||||
],
|
||||
"health": {
|
||||
"status": "ok",
|
||||
"mode": "echo",
|
||||
"model": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"rounds": [
|
||||
{
|
||||
"round": 1,
|
||||
"requirement": "把发送按钮和用户消息气泡的主题色从绿色改成蓝色,必须使用 #2563eb。",
|
||||
"kind": "color",
|
||||
"expected": "rgb(37, 99, 235)",
|
||||
"observed": "rgb(37, 99, 235)",
|
||||
"changed_files": [
|
||||
{
|
||||
"path": "src/App.jsx",
|
||||
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
|
||||
"after_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545"
|
||||
},
|
||||
{
|
||||
"path": "src/theme.css",
|
||||
"before_sha256": "499a1d01a5dfe83bb8496349a51a9a9abd8c49df94afcb7d9615920c5653ff16",
|
||||
"after_sha256": "c494cac4e0c0eb23b5b7f91925cb0997ab7eceae5e3d517736b8a71b61338a3b"
|
||||
}
|
||||
],
|
||||
"chat_state_retained": true,
|
||||
"screenshot": "round-1.png"
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"requirement": "把整个界面的字体换成等宽字体(monospace),保留上一轮蓝色主题。",
|
||||
"kind": "font",
|
||||
"expected": "monospace",
|
||||
"observed": "monospace",
|
||||
"changed_files": [
|
||||
{
|
||||
"path": "src/App.jsx",
|
||||
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
|
||||
"after_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545"
|
||||
},
|
||||
{
|
||||
"path": "src/theme.css",
|
||||
"before_sha256": "c494cac4e0c0eb23b5b7f91925cb0997ab7eceae5e3d517736b8a71b61338a3b",
|
||||
"after_sha256": "ab503f9f2821a97af18c0386893bf93cba4d9da92debad5cd740e37614a1eb68"
|
||||
}
|
||||
],
|
||||
"chat_state_retained": true,
|
||||
"screenshot": "round-2.png"
|
||||
},
|
||||
{
|
||||
"round": 3,
|
||||
"requirement": "把顶部标题改成“我的专属客服”,保留前两轮的蓝色和等宽字体。",
|
||||
"kind": "title",
|
||||
"expected": "我的专属客服",
|
||||
"observed": "我的专属客服",
|
||||
"changed_files": [
|
||||
{
|
||||
"path": "src/App.jsx",
|
||||
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
|
||||
"after_sha256": "76af82f286e3a6affb85a7c2835e78d5c97708aa5eb9088ae5693256aac76e11"
|
||||
},
|
||||
{
|
||||
"path": "src/theme.css",
|
||||
"before_sha256": "ab503f9f2821a97af18c0386893bf93cba4d9da92debad5cd740e37614a1eb68",
|
||||
"after_sha256": "4b217ab0f2c33dda062217f7090b1c0da9d3cc4192add8ca8acb72a70b649e3b"
|
||||
}
|
||||
],
|
||||
"chat_state_retained": true,
|
||||
"screenshot": "round-3.png"
|
||||
}
|
||||
],
|
||||
"browser": {
|
||||
"browser": "Chromium",
|
||||
"version": "139.0.7258.5",
|
||||
"vite_hmr_websockets": [
|
||||
{
|
||||
"event": "opened",
|
||||
"url": "ws://127.0.0.1:56043/?token=ylSMD3fv1F2P"
|
||||
}
|
||||
],
|
||||
"navigation_count_after_initial_load": 0,
|
||||
"sentinel_chat_state_retained": true,
|
||||
"final_title": "我的专属客服",
|
||||
"final_color": "rgb(37, 99, 235)",
|
||||
"final_font": "monospace"
|
||||
},
|
||||
"build": {
|
||||
"returncode": 0,
|
||||
"latency_s": 0.819,
|
||||
"stdout": "\n> conversational-ui-frontend@0.1.0 build\n> vite build\n\nvite v6.4.3 building for production...\ntransforming...\n✓ 27 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.42 kB │ gzip: 0.31 kB\ndist/assets/index-CCn-3TaK.css 1.35 kB │ gzip: 0.56 kB\ndist/assets/index-CvgOOKX3.js 145.07 kB │ gzip: 46.92 kB\n✓ built in 405ms\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"usage": {
|
||||
"calls": 4,
|
||||
"prompt_tokens": 9157,
|
||||
"completion_tokens": 6656,
|
||||
"total_tokens": 15813,
|
||||
"latency_s": 34.724
|
||||
},
|
||||
"artifacts": {
|
||||
"backend.log": {
|
||||
"path": "backend.log",
|
||||
"sha256": "5f932b9b3bb7dfe8069dfb88d5bec4cdeacc454fbdd7472dd066999089b3c090",
|
||||
"bytes": 822
|
||||
},
|
||||
"build.json": {
|
||||
"path": "build.json",
|
||||
"sha256": "bc9bd81991925315ad5a9045ec8db3d6fa025f6cf7de810770f27e3804b857a7",
|
||||
"bytes": 472
|
||||
},
|
||||
"final.png": {
|
||||
"path": "final.png",
|
||||
"sha256": "06d905eaf43a7db3730ae2aebcb65976a880e1761bedf9854ceb3c6bf85ce352",
|
||||
"bytes": 30534
|
||||
},
|
||||
"frontend.log": {
|
||||
"path": "frontend.log",
|
||||
"sha256": "c38d7e06f6656d8a14ab216329383fedd4feb924eee00dfbd6ac371669cdc7fb",
|
||||
"bytes": 598
|
||||
},
|
||||
"receipts.checkpoint.json": {
|
||||
"path": "receipts.checkpoint.json",
|
||||
"sha256": "88664e677ef9a1665dd4b64d94163fe9d75188e2a34eedd833802f66882756c2",
|
||||
"bytes": 58118
|
||||
},
|
||||
"receipts.json": {
|
||||
"path": "receipts.json",
|
||||
"sha256": "88664e677ef9a1665dd4b64d94163fe9d75188e2a34eedd833802f66882756c2",
|
||||
"bytes": 58118
|
||||
},
|
||||
"round-1.png": {
|
||||
"path": "round-1.png",
|
||||
"sha256": "76c8b80499d55d5a2247ceed6064fcea3470fe516b4efa17a37fb3b3b0539c2c",
|
||||
"bytes": 31747
|
||||
},
|
||||
"round-2.png": {
|
||||
"path": "round-2.png",
|
||||
"sha256": "cf092ba62997a36b21e129dc252b1dd18c37c7a8b572c4aec98d0f8a6c3d9395",
|
||||
"bytes": 29210
|
||||
},
|
||||
"round-3.png": {
|
||||
"path": "round-3.png",
|
||||
"sha256": "06d905eaf43a7db3730ae2aebcb65976a880e1761bedf9854ceb3c6bf85ce352",
|
||||
"bytes": 30534
|
||||
},
|
||||
"rounds.json": {
|
||||
"path": "rounds.json",
|
||||
"sha256": "253ff0223525af311c3ed8eb279d37f24cdf570b3b27e83c1fae039362335d91",
|
||||
"bytes": 2369
|
||||
},
|
||||
"final-source/src/App.jsx": {
|
||||
"path": "app/frontend/src/App.jsx",
|
||||
"sha256": "76af82f286e3a6affb85a7c2835e78d5c97708aa5eb9088ae5693256aac76e11",
|
||||
"bytes": 2750
|
||||
},
|
||||
"final-source/src/theme.css": {
|
||||
"path": "app/frontend/src/theme.css",
|
||||
"sha256": "4b217ab0f2c33dda062217f7090b1c0da9d3cc4192add8ca8acb72a70b649e3b",
|
||||
"bytes": 2209
|
||||
}
|
||||
},
|
||||
"acceptance_gates": {
|
||||
"real_react_vite_dev_server": true,
|
||||
"real_fastapi_backend_reload_mode": true,
|
||||
"real_model_generated_three_sequential_edits": true,
|
||||
"vite_hmr_websocket_observed": true,
|
||||
"no_full_page_navigation_during_three_edits": true,
|
||||
"react_chat_state_preserved_across_hmr": true,
|
||||
"all_color_font_title_requests_visible": true,
|
||||
"final_vite_build_passed": true,
|
||||
"raw_provider_receipts_complete": true,
|
||||
"rendered_browser_images_retained": true
|
||||
},
|
||||
"official_complete": true
|
||||
}
|
||||
+383
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,65 @@
|
||||
[
|
||||
{
|
||||
"round": 1,
|
||||
"requirement": "把发送按钮和用户消息气泡的主题色从绿色改成蓝色,必须使用 #2563eb。",
|
||||
"kind": "color",
|
||||
"expected": "rgb(37, 99, 235)",
|
||||
"observed": "rgb(37, 99, 235)",
|
||||
"changed_files": [
|
||||
{
|
||||
"path": "src/App.jsx",
|
||||
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
|
||||
"after_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545"
|
||||
},
|
||||
{
|
||||
"path": "src/theme.css",
|
||||
"before_sha256": "499a1d01a5dfe83bb8496349a51a9a9abd8c49df94afcb7d9615920c5653ff16",
|
||||
"after_sha256": "c494cac4e0c0eb23b5b7f91925cb0997ab7eceae5e3d517736b8a71b61338a3b"
|
||||
}
|
||||
],
|
||||
"chat_state_retained": true,
|
||||
"screenshot": "round-1.png"
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"requirement": "把整个界面的字体换成等宽字体(monospace),保留上一轮蓝色主题。",
|
||||
"kind": "font",
|
||||
"expected": "monospace",
|
||||
"observed": "monospace",
|
||||
"changed_files": [
|
||||
{
|
||||
"path": "src/App.jsx",
|
||||
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
|
||||
"after_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545"
|
||||
},
|
||||
{
|
||||
"path": "src/theme.css",
|
||||
"before_sha256": "c494cac4e0c0eb23b5b7f91925cb0997ab7eceae5e3d517736b8a71b61338a3b",
|
||||
"after_sha256": "ab503f9f2821a97af18c0386893bf93cba4d9da92debad5cd740e37614a1eb68"
|
||||
}
|
||||
],
|
||||
"chat_state_retained": true,
|
||||
"screenshot": "round-2.png"
|
||||
},
|
||||
{
|
||||
"round": 3,
|
||||
"requirement": "把顶部标题改成“我的专属客服”,保留前两轮的蓝色和等宽字体。",
|
||||
"kind": "title",
|
||||
"expected": "我的专属客服",
|
||||
"observed": "我的专属客服",
|
||||
"changed_files": [
|
||||
{
|
||||
"path": "src/App.jsx",
|
||||
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
|
||||
"after_sha256": "76af82f286e3a6affb85a7c2835e78d5c97708aa5eb9088ae5693256aac76e11"
|
||||
},
|
||||
{
|
||||
"path": "src/theme.css",
|
||||
"before_sha256": "ab503f9f2821a97af18c0386893bf93cba4d9da92debad5cd740e37614a1eb68",
|
||||
"after_sha256": "4b217ab0f2c33dda062217f7090b1c0da9d3cc4192add8ca8acb72a70b649e3b"
|
||||
}
|
||||
],
|
||||
"chat_state_retained": true,
|
||||
"screenshot": "round-3.png"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user