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(
"""
以高端接待流程為設計核心,提供清楚、穩定且有質感的線上客服體驗。輸入問題後,系統會依照示範客服知識庫回傳可能的答案。AI 線上客服系統
請先選擇想諮詢的服務類別,再輸入問題。系統會依照選單情境協助回覆。
""" ) service_topic = gr.Radio( choices=[ "預約與營業時間", "訂單與服務進度", "付款與發票資訊", "會員帳號協助", "退貨與退款流程", ], value="預約與營業時間", label="服務選單", elem_classes=["menu-radio"], ) gr.HTML( """