online-QA / app.py
Neil67's picture
Make service menu interactive
33be3b6 verified
Raw
History Blame Contribute Delete
14.2 kB
import gradio as gr
# 範例客服知識庫:部署到 Hugging Face Spaces 時不需要額外資料庫或 API 金鑰。
# 若未來要串接真實模型、RAG 或 Supabase,可替換 search_answer() 的查詢邏輯。
FAQ_DATA = [
{
"keywords": ["營業", "時間", "幾點", "開門"],
"answer": "我們的線上客服系統全天候開放。若是人工客服,服務時間為週一至週五 09:00-18:00。",
},
{
"keywords": ["退貨", "退款", "取消", "換貨"],
"answer": "若需要退貨或退款,請提供訂單編號與問題描述,客服人員會協助確認後續流程。",
},
{
"keywords": ["訂單", "查詢", "物流", "配送", "出貨"],
"answer": "您可以提供訂單編號,我們會協助查詢目前的出貨與配送狀態。",
},
{
"keywords": ["付款", "信用卡", "發票", "刷卡"],
"answer": "目前支援信用卡、轉帳與行動支付。發票資訊可於訂單成立後在會員中心查詢。",
},
{
"keywords": ["帳號", "登入", "密碼", "註冊"],
"answer": "若無法登入,請先使用忘記密碼功能重設密碼;若仍有問題,請提供註冊信箱讓客服協助。",
},
]
INITIAL_CHAT_HISTORY = [
(None, "您好,歡迎使用 AI 線上客服系統。請輸入您想查詢的問題,我會協助您快速找到合適的回覆。")
]
SERVICE_PROMPTS = {
"預約與營業時間": {
"message": "您選擇了「預約與營業時間」。可以輸入想預約的日期、時段或想確認的營業時間。",
"placeholder": "例如:我想預約週五下午,請問有空檔嗎?",
},
"訂單與服務進度": {
"message": "您選擇了「訂單與服務進度」。請提供訂單編號、服務項目或想查詢的進度內容。",
"placeholder": "例如:我的訂單編號是 A12345,想查詢目前進度",
},
"付款與發票資訊": {
"message": "您選擇了「付款與發票資訊」。可以詢問付款方式、刷卡、轉帳或發票相關問題。",
"placeholder": "例如:請問可以刷卡嗎?發票要在哪裡查詢?",
},
"會員帳號協助": {
"message": "您選擇了「會員帳號協助」。可以描述登入、註冊、密碼或會員資料相關問題。",
"placeholder": "例如:我忘記密碼,無法登入會員中心",
},
"退貨與退款流程": {
"message": "您選擇了「退貨與退款流程」。請提供訂單編號與需要協助的退款或退貨狀況。",
"placeholder": "例如:我想申請退款,訂單編號是 A12345",
},
}
def search_answer(question: str) -> str:
"""依照使用者輸入的關鍵字回傳客服示範答案。"""
clean_question = question.strip()
if not clean_question:
return "請先輸入想查詢的問題,我會協助您找出可能的答案。"
# 使用簡單關鍵字比對做教學示範,避免 Space 部署時需要下載大型模型。
for item in FAQ_DATA:
if any(keyword in clean_question for keyword in item["keywords"]):
return item["answer"]
return (
"目前沒有找到完全符合的答案。您可以換個方式描述問題,"
"例如提供訂單編號、付款方式或遇到的錯誤訊息。"
)
def submit_message(
message: str,
history: list[tuple[str | None, str]],
service_topic: str,
) -> tuple[list[tuple[str | None, str]], str]:
"""將使用者訊息、選單情境與系統回覆加入聊天紀錄,並清空輸入框。"""
history = list(history or INITIAL_CHAT_HISTORY)
display_message = message.strip() or f"我想詢問:{service_topic}"
query = f"{service_topic} {display_message}"
answer = search_answer(query)
history.append((display_message, answer))
return history, ""
def select_service_topic(
service_topic: str,
history: list[tuple[str | None, str]],
) -> tuple[list[tuple[str | None, str]], str]:
"""選擇左側服務項目時,立即在聊天框顯示對應提示並更新輸入提示文字。"""
history = list(history or INITIAL_CHAT_HISTORY)
guide = SERVICE_PROMPTS.get(service_topic, SERVICE_PROMPTS["預約與營業時間"])
assistant_message = guide["message"]
if not history or history[-1][1] != assistant_message:
history.append((None, assistant_message))
return history, guide["placeholder"]
# 自訂 CSS 用於建立高端醫美接待台風格的客服介面。
CUSTOM_CSS = """
:root {
--ink: #17212b;
--muted: #687583;
--line: #e3e8ec;
--panel: #ffffff;
--soft: #f7faf9;
--sage: #3f766c;
--sage-dark: #244f49;
--rose: #c99a8a;
--rose-soft: #f6e9e4;
}
.gradio-container {
max-width: 1180px !important;
margin: 0 auto !important;
background:
radial-gradient(circle at top left, rgba(201, 154, 138, 0.18), transparent 34%),
linear-gradient(180deg, #fbfcfb 0%, #f3f7f6 100%) !important;
color: var(--ink);
}
body {
background: #f3f7f6;
}
.main-shell {
padding: 26px 18px 20px;
}
#hero {
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(247, 250, 249, 0.92)),
linear-gradient(135deg, rgba(63, 118, 108, 0.14), rgba(201, 154, 138, 0.18));
border: 1px solid rgba(63, 118, 108, 0.16);
border-radius: 18px;
padding: 30px 34px;
color: var(--ink);
box-shadow: 0 22px 60px rgba(36, 79, 73, 0.12);
position: relative;
overflow: hidden;
}
#hero::after {
content: "";
position: absolute;
width: 210px;
height: 210px;
right: -76px;
top: -92px;
border-radius: 999px;
border: 34px solid rgba(201, 154, 138, 0.16);
}
.eyebrow {
color: var(--sage);
font-size: 13px;
font-weight: 700;
letter-spacing: 1.8px;
text-transform: uppercase;
margin-bottom: 10px;
}
#hero h1 {
font-size: 38px;
line-height: 1.2;
margin: 0 0 10px;
letter-spacing: 0;
color: var(--ink);
}
#hero p {
color: #4f5f68;
font-size: 16px;
line-height: 1.7;
margin: 0;
max-width: 720px;
}
.status-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 14px;
margin: 16px 0;
}
.status-item {
background: rgba(255, 255, 255, 0.86);
border: 1px solid var(--line);
border-radius: 14px;
padding: 16px 18px;
box-shadow: 0 12px 30px rgba(23, 33, 43, 0.04);
}
.status-label {
color: var(--muted);
font-size: 13px;
margin-bottom: 4px;
}
.status-value {
color: var(--sage-dark);
font-size: 16px;
font-weight: 700;
}
.clinic-layout {
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
gap: 18px;
align-items: stretch;
}
.concierge-card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 18px;
padding: 22px;
box-shadow: 0 18px 42px rgba(23, 33, 43, 0.08);
}
.concierge-card h2 {
font-size: 19px;
margin: 0 0 8px;
color: var(--ink);
}
.concierge-card p {
color: var(--muted);
line-height: 1.7;
margin: 0 0 16px;
font-size: 14px;
}
.menu-panel .wrap {
gap: 10px;
}
.menu-panel label {
border: 1px solid #e5ddd9 !important;
background: #fffaf8 !important;
border-radius: 12px !important;
padding: 12px 13px !important;
margin-bottom: 8px !important;
color: #614d47 !important;
font-weight: 650 !important;
}
.menu-panel label:hover {
border-color: var(--rose) !important;
background: #fff5f1 !important;
}
.mini-note {
border-top: 1px solid var(--line);
padding-top: 15px;
color: var(--muted);
font-size: 13px;
line-height: 1.65;
}
#workspace {
background: rgba(255, 255, 255, 0.94);
border: 1px solid var(--line);
border-radius: 18px;
padding: 18px;
box-shadow: 0 18px 42px rgba(23, 33, 43, 0.08);
}
#chatbot {
border: 1px solid #dbe4e2;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(180deg, #ffffff 0%, #f8fbfa 100%);
min-height: 430px;
}
#input-row {
align-items: stretch;
margin-top: 16px;
}
#send-button {
min-height: 72px;
border-radius: 14px !important;
background: linear-gradient(135deg, var(--sage-dark), var(--sage)) !important;
border: 1px solid rgba(36, 79, 73, 0.25) !important;
box-shadow: 0 14px 26px rgba(63, 118, 108, 0.22);
font-weight: 700 !important;
}
#send-button:hover {
filter: brightness(1.04);
}
textarea, input {
border-radius: 14px !important;
}
#notice {
background: rgba(255, 255, 255, 0.78);
border: 1px solid var(--line);
border-left: 4px solid var(--rose);
border-radius: 16px;
color: var(--muted);
padding: 14px 18px;
margin-top: 16px;
}
#notice h3 {
color: var(--ink);
margin-top: 0;
margin-bottom: 8px;
font-size: 16px;
}
@media (max-width: 760px) {
.main-shell {
padding: 14px 8px;
}
#hero {
padding: 22px 18px;
}
#hero h1 {
font-size: 28px;
}
.status-grid {
grid-template-columns: 1fr;
}
.clinic-layout {
grid-template-columns: 1fr;
}
}
"""
with gr.Blocks(title="AI 線上客服系統", css=CUSTOM_CSS, theme=gr.themes.Soft()) as demo:
with gr.Column(elem_classes=["main-shell"]):
gr.HTML(
"""
<section id="hero">
<div class="eyebrow">Concierge Support Desk</div>
<h1>AI 線上客服系統</h1>
<p>以高端接待流程為設計核心,提供清楚、穩定且有質感的線上客服體驗。輸入問題後,系統會依照示範客服知識庫回傳可能的答案。</p>
</section>
<section class="status-grid">
<div class="status-item">
<div class="status-label">服務狀態</div>
<div class="status-value">線上服務中</div>
</div>
</section>
"""
)
with gr.Row(elem_classes=["clinic-layout"]):
with gr.Column(elem_classes=["concierge-card", "menu-panel"]):
gr.HTML(
"""
<div class="eyebrow">Service Menu</div>
<h2>專屬客服接待</h2>
<p>請先選擇想諮詢的服務類別,再輸入問題。系統會依照選單情境協助回覆。</p>
"""
)
service_topic = gr.Radio(
choices=[
"預約與營業時間",
"訂單與服務進度",
"付款與發票資訊",
"會員帳號協助",
"退貨與退款流程",
],
value="預約與營業時間",
label="服務選單",
elem_classes=["menu-radio"],
)
gr.HTML(
"""
<div class="mini-note">建議輸入具體資訊,例如訂單編號、付款方式、遇到的狀況或想確認的服務項目。</div>
"""
)
with gr.Column(elem_id="workspace"):
# Chatbot 使用 bubble layout,呈現仿聊天對話氣泡效果。
chatbot = gr.Chatbot(
value=INITIAL_CHAT_HISTORY,
label="AI 客服對話",
elem_id="chatbot",
height=500,
layout="bubble",
)
# State 儲存目前聊天紀錄,讓每次送出後能延續上下文畫面。
chat_state = gr.State(INITIAL_CHAT_HISTORY)
with gr.Row(elem_id="input-row"):
question_input = gr.Textbox(
label="請輸入問題",
placeholder=SERVICE_PROMPTS["預約與營業時間"]["placeholder"],
lines=2,
scale=5,
)
submit_btn = gr.Button(
"送出查詢",
variant="primary",
scale=1,
elem_id="send-button",
)
service_topic.change(
fn=select_service_topic,
inputs=[service_topic, chat_state],
outputs=[chatbot, question_input],
api_name=False,
).then(
fn=lambda history: history,
inputs=chatbot,
outputs=chat_state,
api_name=False,
)
submit_btn.click(
fn=submit_message,
inputs=[question_input, chat_state, service_topic],
outputs=[chatbot, question_input],
api_name=False,
).then(
fn=lambda history: history,
inputs=chatbot,
outputs=chat_state,
api_name=False,
)
# 讓使用者按 Enter 也能送出,符合聊天介面的常見操作。
question_input.submit(
fn=submit_message,
inputs=[question_input, chat_state, service_topic],
outputs=[chatbot, question_input],
api_name=False,
).then(
fn=lambda history: history,
inputs=chatbot,
outputs=chat_state,
api_name=False,
)
gr.Markdown(
"### 使用提醒\n此模型僅供教學展示,回覆內容不代表正式客服承諾;實際服務請以官方公告與人工客服回覆為準。",
elem_id="notice",
)
if __name__ == "__main__":
# Hugging Face Spaces 會自動執行 app.py;本機測試時可用 python app.py 啟動。
demo.launch()