Files
group_fqcd_jr/app/static/index.html
T

156 lines
8.9 KiB
HTML
Raw Normal View History

2026-09-10 19:51:08 +08:00
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>南方财富智能助手联调</title>
2026-09-10 19:51:08 +08:00
<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>
2026-09-10 19:51:08 +08:00
</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");
2026-09-10 19:51:08 +08:00
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", "您好呀,我是南方财富智能助手。这里可测试公开问题、账户入口、安全提示和人工服务路径。");
2026-09-10 19:51:08 +08:00
void ensureVisitorToken().catch(() => { connectionStatus.textContent = "访客:连接未建立,发送消息时将自动重试"; });
</script>
</body>
</html>