Add customer service API deployment support
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
ExpandOutlined, HeartOutlined, PhoneOutlined, SearchOutlined, SendOutlined,
|
||||
ShareAltOutlined, ShrinkOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { plazaAmapConfig, plazaUserQuery } from "../../api";
|
||||
import { displayGraphName, getProjectContext, plazaAmapConfig, plazaUserQuery, travelAssistantQuery } from "../../api";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -69,6 +69,7 @@ type QueryResult = {
|
||||
};
|
||||
|
||||
type QueryPayload = {
|
||||
mode?: "spatial" | "travel_customer_service";
|
||||
question: string;
|
||||
graph_name: string;
|
||||
user_location: { lng: number; lat: number };
|
||||
@@ -76,6 +77,9 @@ type QueryPayload = {
|
||||
answer: string;
|
||||
results: QueryResult[];
|
||||
trace: Record<string, any>;
|
||||
plans?: Array<Record<string, any>>;
|
||||
follow_up_questions?: string[];
|
||||
risk_notes?: string[];
|
||||
};
|
||||
|
||||
const samples = [
|
||||
@@ -86,6 +90,14 @@ const samples = [
|
||||
"附近有没有公交地铁站?",
|
||||
];
|
||||
|
||||
const travelSamples = [
|
||||
"黄小西三日游多少钱?",
|
||||
"2天1晚想去黄果树,有哪些线路,顺便看附近酒店和可选车辆价格",
|
||||
"黄果树附近有哪些酒店餐饮,价格和余位能直接承诺吗?",
|
||||
"客户临时多加2人,原来5人用车还能坐吗?",
|
||||
"小七孔有哪些必付和可选费用,老人小孩有没有需要注意的?",
|
||||
];
|
||||
|
||||
const formatDistance = (m: number) => {
|
||||
if (m >= 1000) return `${(m / 1000).toFixed(1)}km`;
|
||||
return `${Math.round(m)}m`;
|
||||
@@ -121,6 +133,7 @@ export default function UserExperiencePanel() {
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const [photoIndex, setPhotoIndex] = useState(0);
|
||||
const [failedPhotos, setFailedPhotos] = useState<string[]>([]);
|
||||
const [projectContext, setProjectContextState] = useState(() => getProjectContext());
|
||||
|
||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||
const mapInstance = useRef<any>(null);
|
||||
@@ -129,6 +142,25 @@ export default function UserExperiencePanel() {
|
||||
|
||||
const results = payload?.results || [];
|
||||
const intent = payload?.intent;
|
||||
const graphKey = String(projectContext.graphName || "").toLowerCase();
|
||||
const isTravelCustomerService = graphKey.includes("baixinghui") || String(projectContext.graphName || "").includes("百姓惠");
|
||||
const isTravelPayload = payload?.mode === "travel_customer_service";
|
||||
|
||||
useEffect(() => {
|
||||
const updateContext = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
setProjectContextState(detail || getProjectContext());
|
||||
};
|
||||
window.addEventListener("znkg-project-context", updateContext);
|
||||
return () => window.removeEventListener("znkg-project-context", updateContext);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const nextSamples = isTravelCustomerService ? travelSamples : samples;
|
||||
setQuestion((current) => ([...samples, ...travelSamples].includes(current) ? nextSamples[0] : current));
|
||||
setPayload(null);
|
||||
setActiveId("");
|
||||
}, [isTravelCustomerService]);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
@@ -305,6 +337,40 @@ export default function UserExperiencePanel() {
|
||||
setQuestion(text);
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isTravelCustomerService) {
|
||||
const { data } = await travelAssistantQuery({
|
||||
question: text,
|
||||
graph_name: projectContext.graphName,
|
||||
use_llm: true,
|
||||
});
|
||||
const trace = data?.trace || {};
|
||||
const warnings = Array.isArray(trace.quality_checks)
|
||||
? trace.quality_checks
|
||||
.filter((item: any) => item?.status === "warn" || item?.status === "fail")
|
||||
.map((item: any) => item?.detail || item?.label)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
setPayload({
|
||||
mode: "travel_customer_service",
|
||||
question: text,
|
||||
graph_name: data?.graph_name || projectContext.graphName,
|
||||
user_location: { lng, lat },
|
||||
intent: {
|
||||
radius_m: radius,
|
||||
category: trace.response_mode_label || "百姓惠客服",
|
||||
keywords: data?.intent?.destinations || [],
|
||||
user_need: text,
|
||||
},
|
||||
answer: data?.copy_text || "当前知识图谱没有返回可直接回复的话术,请补充客户出行人数、天数、团期和必去景点后再试。",
|
||||
results: [],
|
||||
trace,
|
||||
plans: data?.plans || [],
|
||||
follow_up_questions: data?.follow_up_questions || [],
|
||||
risk_notes: warnings,
|
||||
});
|
||||
setActiveId("");
|
||||
return;
|
||||
}
|
||||
const { data } = await plazaUserQuery({
|
||||
question: text,
|
||||
lng,
|
||||
@@ -330,35 +396,50 @@ export default function UserExperiencePanel() {
|
||||
onChange={(e) => setQuestion(e.target.value)}
|
||||
onSearch={() => runQuery()}
|
||||
loading={loading}
|
||||
placeholder="搜索地点、公交、地铁、美食..."
|
||||
placeholder={isTravelCustomerService ? "输入客户原话,例如:黄小西三日游多少钱?" : "搜索地点、公交、地铁、美食..."}
|
||||
/>
|
||||
|
||||
<div className="kg-user-controls">
|
||||
<div className="kg-user-control-title">
|
||||
<Tag color="blue">游客坐标</Tag>
|
||||
<Button size="small" icon={<AimOutlined />} onClick={() => { setLng(106.702501); setLat(26.55806); }}>
|
||||
甲秀楼坐标
|
||||
</Button>
|
||||
</div>
|
||||
<div className="kg-user-control-grid">
|
||||
<label>
|
||||
<span>经度</span>
|
||||
<InputNumber value={lng} precision={6} onChange={(v) => setLng(Number(v || lng))} />
|
||||
</label>
|
||||
<label>
|
||||
<span>纬度</span>
|
||||
<InputNumber value={lat} precision={6} onChange={(v) => setLat(Number(v || lat))} />
|
||||
</label>
|
||||
<label>
|
||||
<span>默认半径</span>
|
||||
<InputNumber min={100} max={10000} step={100} value={radius} onChange={(v) => setRadius(Number(v || 1000))} />
|
||||
</label>
|
||||
</div>
|
||||
{isTravelCustomerService ? (
|
||||
<div className="kg-user-travel-mode">
|
||||
<Tag color="green">百姓惠客服问答</Tag>
|
||||
<Text type="secondary">当前图谱:{displayGraphName(projectContext.graphName)},输入客户自然语言后返回图谱召回话术、证据和追问。</Text>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="kg-user-control-title">
|
||||
<Tag color="blue">游客坐标</Tag>
|
||||
<Button size="small" icon={<AimOutlined />} onClick={() => { setLng(106.702501); setLat(26.55806); }}>
|
||||
甲秀楼坐标
|
||||
</Button>
|
||||
</div>
|
||||
<div className="kg-user-control-grid">
|
||||
<label>
|
||||
<span>经度</span>
|
||||
<InputNumber value={lng} precision={6} onChange={(v) => setLng(Number(v || lng))} />
|
||||
</label>
|
||||
<label>
|
||||
<span>纬度</span>
|
||||
<InputNumber value={lat} precision={6} onChange={(v) => setLat(Number(v || lat))} />
|
||||
</label>
|
||||
<label>
|
||||
<span>默认半径</span>
|
||||
<InputNumber min={100} max={10000} step={100} value={radius} onChange={(v) => setRadius(Number(v || 1000))} />
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="kg-user-result-head">
|
||||
<b><CompassOutlined /> 查询结果</b>
|
||||
{payload ? <span>{results.length} 个结果 · {intent?.category || "综合"} · {intent?.radius_m || radius}m</span> : null}
|
||||
{payload ? (
|
||||
<span>
|
||||
{isTravelPayload
|
||||
? `${displayGraphName(payload.graph_name)} · ${intent?.category || "客服问答"} · 方案 ${(payload.plans || []).length}`
|
||||
: `${results.length} 个结果 · ${intent?.category || "综合"} · ${intent?.radius_m || radius}m`}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="kg-user-results">
|
||||
@@ -370,7 +451,41 @@ export default function UserExperiencePanel() {
|
||||
<>
|
||||
<div className="kg-user-answer">{payload.answer}</div>
|
||||
{results.length === 0 ? (
|
||||
<Empty description="当前半径内没有匹配结果" style={{ marginTop: 70 }} />
|
||||
isTravelPayload ? (
|
||||
<div className="kg-user-travel-summary">
|
||||
{(payload.plans || []).slice(0, 4).map((plan, idx) => (
|
||||
<div className="kg-user-travel-plan" key={`${plan.plan_name || idx}`}>
|
||||
<Space size={6} wrap>
|
||||
<Tag color="blue">{idx + 1}</Tag>
|
||||
<b>{plan.plan_name || "未命名线路"}</b>
|
||||
{plan.duration_days && <Tag>{plan.duration_days}天</Tag>}
|
||||
{plan.fit_score && <Tag color="purple">匹配 {plan.fit_score}</Tag>}
|
||||
</Space>
|
||||
<p>{plan.route_summary || plan.quote_summary || plan.variant_summary || "图谱已命中该线路,更多细节请继续追问。"}</p>
|
||||
</div>
|
||||
))}
|
||||
{!!payload.follow_up_questions?.length && (
|
||||
<div className="kg-user-travel-followups">
|
||||
<Text strong>建议追问</Text>
|
||||
<Space wrap>
|
||||
{payload.follow_up_questions.slice(0, 4).map((item) => (
|
||||
<Tag color="gold" key={item} onClick={() => runQuery(item)}>{item}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
{!!payload.risk_notes?.length && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="客服承诺边界"
|
||||
description={payload.risk_notes.slice(0, 3).join(";")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Empty description="当前半径内没有匹配结果" style={{ marginTop: 70 }} />
|
||||
)
|
||||
) : results.map((r, idx) => {
|
||||
const photo = firstPhoto(r);
|
||||
return (
|
||||
@@ -587,6 +702,12 @@ export default function UserExperiencePanel() {
|
||||
.kg-user-control-grid .ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
.kg-user-travel-mode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.kg-user-result-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -614,6 +735,32 @@ export default function UserExperiencePanel() {
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.kg-user-travel-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
.kg-user-travel-plan {
|
||||
padding: 12px;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 8px;
|
||||
background: #fbfdff;
|
||||
}
|
||||
.kg-user-travel-plan p {
|
||||
margin: 8px 0 0;
|
||||
color: #4e5969;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.kg-user-travel-followups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.kg-user-travel-followups .ant-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
.kg-user-result-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 96px;
|
||||
|
||||
Reference in New Issue
Block a user