156 lines
8.9 KiB
HTML
156 lines
8.9 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>奶龙基金智能助手联调</title>
|
|
<style>
|
|
:root { --ink: #17212b; --muted: #607080; --line: #d9e1e7; --canvas: #f4f7f8; --surface: #ffffff; --brand: #008a7a; --brand-dark: #00695f; --accent: #e96f45; --visitor: #e8f6f1; --agent: #f2f5f7; --danger: #b43b2c; }
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; min-height: 100vh; background: var(--canvas); color: var(--ink); font-family: "Microsoft YaHei", "PingFang SC", sans-serif; }
|
|
main { width: min(100%, 980px); min-height: 100vh; margin: 0 auto; padding: 24px; display: grid; grid-template-rows: auto auto minmax(360px, 1fr) auto; gap: 14px; }
|
|
header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-bottom: 14px; border-bottom: 1px solid var(--line); }
|
|
.brand { display: flex; align-items: center; gap: 12px; }
|
|
.brand-mark { width: 38px; height: 38px; display: grid; place-items: center; background: var(--brand); color: #ffffff; border-radius: 8px; font-weight: 700; }
|
|
h1 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: 0; }
|
|
.status { margin: 2px 0 0; color: var(--muted); font-size: 13px; }
|
|
.role { color: var(--brand-dark); font-weight: 700; }
|
|
.samples { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
.sample { min-height: 34px; border: 1px solid var(--line); background: var(--surface); color: var(--ink); padding: 7px 10px; border-radius: 6px; cursor: pointer; font: inherit; font-size: 13px; }
|
|
.sample:hover, .sample:focus-visible { border-color: var(--brand); color: var(--brand-dark); outline: none; }
|
|
.chat { overflow-y: auto; display: flex; flex-direction: column; gap: 12px; padding: 18px; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
|
|
.message { display: flex; gap: 10px; max-width: min(82%, 620px); }
|
|
.message.user { align-self: flex-end; flex-direction: row-reverse; }
|
|
.avatar { flex: 0 0 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; background: var(--brand); color: #ffffff; font-size: 12px; font-weight: 700; }
|
|
.message.user .avatar { background: var(--accent); }
|
|
.bubble { margin: 0; padding: 10px 12px; background: var(--agent); border-radius: 6px; font-size: 14px; line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
.message.user .bubble { background: var(--visitor); }
|
|
.pending .bubble { color: var(--muted); }
|
|
.error .bubble { background: #fff0ed; color: var(--danger); }
|
|
form { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; }
|
|
input { min-width: 0; height: 44px; border: 1px solid var(--line); border-radius: 6px; padding: 0 12px; background: var(--surface); color: var(--ink); font: inherit; }
|
|
input:focus { border-color: var(--brand); outline: 2px solid #bde6df; }
|
|
button[type="submit"] { min-width: 76px; height: 44px; border: 0; border-radius: 6px; background: var(--brand); color: #ffffff; cursor: pointer; font: inherit; }
|
|
button[type="submit"]:disabled { cursor: wait; opacity: 0.6; }
|
|
@media (max-width: 620px) { main { padding: 14px; } header { align-items: flex-start; flex-direction: column; } .message { max-width: 92%; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<header>
|
|
<div class="brand">
|
|
<div class="brand-mark" aria-hidden="true">龙</div>
|
|
<div><h1>奶龙基金智能助手</h1><p class="status">公开知识与客服分流联调页</p></div>
|
|
</div>
|
|
<p id="connection-status" class="status"><span class="role">访客</span>:正在建立测试连接</p>
|
|
</header>
|
|
<section class="samples" aria-label="测试场景">
|
|
<button class="sample" type="button" data-message="你好">闲聊</button>
|
|
<button class="sample" type="button" data-message="基金赎回到账规则是什么">政策</button>
|
|
<button class="sample" type="button" data-message="我的持仓收益是多少">账户入口</button>
|
|
<button class="sample" type="button" data-message="验证码已经发给别人了">安全风险</button>
|
|
<button class="sample" type="button" data-message="帮我推荐一只收益最高的基金">合规拒答</button>
|
|
<button class="sample" type="button" data-message="我要转人工客服">人工服务</button>
|
|
</section>
|
|
<section id="chat" class="chat" aria-live="polite"></section>
|
|
<form id="composer">
|
|
<input id="message" type="text" maxlength="2000" autocomplete="off" placeholder="输入要测试的问题" aria-label="客服测试消息">
|
|
<button id="send" type="submit">发送</button>
|
|
</form>
|
|
</main>
|
|
<script>
|
|
let accessToken = "";
|
|
const sessionId = `visitor-${crypto.randomUUID()}`;
|
|
const chat = document.querySelector("#chat");
|
|
const input = document.querySelector("#message");
|
|
const composer = document.querySelector("#composer");
|
|
const sendButton = document.querySelector("#send");
|
|
const connectionStatus = document.querySelector("#connection-status");
|
|
|
|
function appendMessage(role, text, state = "") {
|
|
const wrapper = document.createElement("article");
|
|
wrapper.className = `message ${role} ${state}`;
|
|
const avatar = document.createElement("span");
|
|
avatar.className = "avatar";
|
|
avatar.textContent = role === "user" ? "我" : "龙";
|
|
const bubble = document.createElement("p");
|
|
bubble.className = "bubble";
|
|
bubble.textContent = text;
|
|
wrapper.append(avatar, bubble);
|
|
chat.append(wrapper);
|
|
chat.scrollTop = chat.scrollHeight;
|
|
return { wrapper, bubble };
|
|
}
|
|
|
|
async function ensureVisitorToken() {
|
|
if (accessToken) return;
|
|
const response = await fetch("/api/v1/visitor-tokens", { method: "POST" });
|
|
if (!response.ok) throw new Error("访客令牌获取失败");
|
|
accessToken = (await response.json()).access_token;
|
|
connectionStatus.innerHTML = '<span class="role">访客</span>:测试连接已建立';
|
|
}
|
|
|
|
function delay(milliseconds) {
|
|
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
|
|
}
|
|
|
|
async function pollRun(statusUrl, messageView) {
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
const response = await fetch(statusUrl, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
if (!response.ok) throw new Error("客服运行状态读取失败");
|
|
const run = (await response.json()).data;
|
|
if (run.status === "succeeded") {
|
|
messageView.bubble.textContent = run.result?.content || "客服未返回可显示内容。";
|
|
messageView.wrapper.classList.remove("pending");
|
|
chat.scrollTop = chat.scrollHeight;
|
|
return;
|
|
}
|
|
if (run.status === "failed" || run.status === "cancelled") {
|
|
messageView.bubble.textContent = `本次客服运行未完成:${run.error_code || run.status}`;
|
|
messageView.wrapper.className = "message error";
|
|
return;
|
|
}
|
|
await delay(500);
|
|
}
|
|
messageView.bubble.textContent = "客服请求仍在处理中,请确认 Worker 已启动后重试。";
|
|
messageView.wrapper.className = "message error";
|
|
}
|
|
|
|
async function sendMessage(message) {
|
|
const normalized = message.trim();
|
|
if (!normalized) return;
|
|
appendMessage("user", normalized);
|
|
input.value = "";
|
|
sendButton.disabled = true;
|
|
const pending = appendMessage("agent", "奶龙基金智能助手正在处理您的问题…", "pending");
|
|
try {
|
|
await ensureVisitorToken();
|
|
const response = await fetch("/api/v1/agent-runs", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
|
|
body: JSON.stringify({ agent_type: "customer_service", message: normalized, session_id: sessionId, idempotency_key: crypto.randomUUID().replaceAll("-", "") }),
|
|
});
|
|
if (!response.ok) throw new Error("客服请求创建失败");
|
|
await pollRun((await response.json()).data.status_url, pending);
|
|
} catch (error) {
|
|
pending.bubble.textContent = error instanceof Error ? error.message : "客服测试请求失败";
|
|
pending.wrapper.className = "message error";
|
|
} finally {
|
|
sendButton.disabled = false;
|
|
input.focus();
|
|
}
|
|
}
|
|
|
|
composer.addEventListener("submit", (event) => {
|
|
event.preventDefault();
|
|
void sendMessage(input.value);
|
|
});
|
|
document.querySelectorAll(".sample").forEach((button) => {
|
|
button.addEventListener("click", () => void sendMessage(button.dataset.message || ""));
|
|
});
|
|
appendMessage("agent", "您好呀,我是奶龙基金智能助手。这里可测试公开问题、账户入口、安全提示和人工服务路径。");
|
|
void ensureVisitorToken().catch(() => { connectionStatus.textContent = "访客:连接未建立,发送消息时将自动重试"; });
|
|
</script>
|
|
</body>
|
|
</html>
|