Add webhook to profile

This commit is contained in:
Xin Wang
2026-08-07 11:21:09 +08:00
parent 7575afaeee
commit bdbda8047d

View File

@@ -1,10 +1,253 @@
import { PlaceholderPage } from "./PlaceholderPage"; "use client";
import {
Braces,
CheckCircle2,
Eye,
EyeOff,
Globe2,
Loader2,
LockKeyhole,
Send,
Webhook,
} from "lucide-react";
import { useState } from "react";
import { SectionCard } from "@/components/editor/section-card";
import { ListPageLayout } from "@/components/layout/list-page-layout";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
type TestStatus = "idle" | "loading" | "success" | "error";
function mockWebhookPayload() {
return {
id: "evt_mock_001",
event: "call.analysis.completed",
timestamp: new Date().toISOString(),
data: {
conversationId: "conv_mock_001",
assistantId: "asst_mock_001",
analysis: {
customer_intent: "high",
need_follow_up: true,
},
},
};
}
function isHttpsUrl(value: string): boolean {
try {
return new URL(value).protocol === "https:";
} catch {
return false;
}
}
export function ProfilePage() { export function ProfilePage() {
const [enabled, setEnabled] = useState(true);
const [url, setUrl] = useState("");
const [secret, setSecret] = useState("");
const [secretVisible, setSecretVisible] = useState(false);
const [testStatus, setTestStatus] = useState<TestStatus>("idle");
const [testMessage, setTestMessage] = useState("");
const [payload, setPayload] = useState<Record<string, unknown> | null>(null);
async function sendTestEvent() {
if (!isHttpsUrl(url.trim())) {
setTestStatus("error");
setTestMessage("请填写完整的 HTTPS Webhook URL。测试不会发起网络请求。");
setPayload(null);
return;
}
setTestStatus("loading");
setTestMessage("");
setPayload(null);
await new Promise((resolve) => window.setTimeout(resolve, 700));
setPayload(mockWebhookPayload());
setTestStatus("success");
setTestMessage("已在浏览器中生成测试事件,未向目标地址发送请求。");
}
return ( return (
<PlaceholderPage <ListPageLayout
title="个人中心" title="个人中心"
description="管理账户信息、团队成员、密钥与偏好设置。" description="管理站点级配置。Webhook 将统一接收所有助手的通话分析完成事件。"
/> className="max-w-[1120px]"
>
<div className="grid gap-4 lg:grid-cols-[280px_minmax(0,1fr)]">
<aside className="space-y-4">
<div className="relative overflow-hidden rounded-2xl border border-hairline bg-card p-5 shadow-sm">
<div className="pointer-events-none absolute -right-12 -top-16 h-36 w-36 rounded-full bg-gradient-sky/25 blur-3xl" />
<div className="relative">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-surface-strong text-foreground">
<Globe2 size={18} />
</div>
<div className="mt-4 caption-label text-muted-soft"></div>
<div className="mt-1 font-display text-2xl text-foreground">
</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
assistantId
conversationId
</p>
</div>
</div>
<div className="rounded-2xl border border-hairline bg-canvas-soft p-4">
<div className="flex items-start gap-3">
<LockKeyhole
size={16}
className="mt-0.5 shrink-0 text-muted-foreground"
/>
<div>
<div className="text-sm font-medium text-foreground">
</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
</p>
</div>
</div>
</div>
</aside>
<SectionCard
icon={<Webhook size={15} />}
title="通话分析 Webhook"
description="站点级事件出口;后端接入后将在分析完成时执行 HTTP POST"
className="self-start"
>
<div className="flex flex-col gap-3 rounded-xl border border-hairline bg-canvas-soft px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium text-foreground">
</span>
<Badge
variant="secondary"
className="bg-surface-strong text-muted-foreground"
>
</Badge>
</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
call.analysis.completed
</p>
</div>
<Switch
checked={enabled}
onCheckedChange={setEnabled}
aria-label="启用分析完成 Webhook"
/>
</div>
<div className="grid gap-3 pt-1">
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
Webhook URL
</span>
<Input
value={url}
onChange={(event) => {
setUrl(event.target.value);
setTestStatus("idle");
setTestMessage("");
setPayload(null);
}}
disabled={!enabled}
placeholder="https://example.com/webhooks/call-analysis"
className="border-hairline-strong bg-background"
/>
<p className="mt-1.5 text-xs leading-5 text-muted-foreground">
HTTPS ID
</p>
</label>
<label className="block">
<span className="mb-1.5 block text-sm font-medium text-foreground">
</span>
<div className="relative">
<Input
type={secretVisible ? "text" : "password"}
value={secret}
onChange={(event) => setSecret(event.target.value)}
disabled={!enabled}
placeholder="用于生成 HMAC-SHA256 签名"
className="border-hairline-strong bg-background pr-10"
/>
<button
type="button"
disabled={!enabled}
onClick={() => setSecretVisible((visible) => !visible)}
className="absolute right-2 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
aria-label={secretVisible ? "隐藏签名密钥" : "显示签名密钥"}
>
{secretVisible ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
</div>
<p className="mt-1.5 text-xs leading-5 text-muted-foreground">
使 timestamp + raw body payload
</p>
</label>
</div>
<div className="flex flex-wrap items-center gap-3 border-t border-hairline pt-4">
<Button
type="button"
variant="outline"
className="gap-2 border-hairline-strong"
disabled={!enabled || testStatus === "loading"}
onClick={() => void sendTestEvent()}
>
{testStatus === "loading" ? (
<Loader2 size={15} className="animate-spin" />
) : (
<Send size={15} />
)}
</Button>
<Button type="button" disabled>
</Button>
<span className="text-xs text-muted-soft"> API</span>
</div>
{testMessage && (
<div
role="status"
className={`flex items-start gap-2.5 rounded-xl border px-3.5 py-3 text-xs leading-5 ${
testStatus === "error"
? "border-destructive/30 bg-destructive/5 text-destructive"
: "border-hairline bg-canvas-soft text-muted-foreground"
}`}
>
{testStatus === "success" && (
<CheckCircle2 size={15} className="mt-0.5 shrink-0 text-success" />
)}
<span>{testMessage}</span>
</div>
)}
{payload && (
<div className="overflow-hidden rounded-xl border border-hairline bg-background">
<div className="flex items-center gap-2 border-b border-hairline px-3.5 py-2.5">
<Braces size={14} className="text-muted-foreground" />
<span className="text-xs font-medium text-foreground">
Mock payload
</span>
</div>
<pre className="max-h-72 overflow-auto p-3.5 font-mono text-[11px] leading-5 text-muted-foreground">
{JSON.stringify(payload, null, 2)}
</pre>
</div>
)}
</SectionCard>
</div>
</ListPageLayout>
); );
} }