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( """
Concierge Support Desk

AI 線上客服系統

以高端接待流程為設計核心,提供清楚、穩定且有質感的線上客服體驗。輸入問題後,系統會依照示範客服知識庫回傳可能的答案。

服務狀態
線上服務中
""" ) with gr.Row(elem_classes=["clinic-layout"]): with gr.Column(elem_classes=["concierge-card", "menu-panel"]): gr.HTML( """
Service Menu

專屬客服接待

請先選擇想諮詢的服務類別,再輸入問題。系統會依照選單情境協助回覆。

""" ) service_topic = gr.Radio( choices=[ "預約與營業時間", "訂單與服務進度", "付款與發票資訊", "會員帳號協助", "退貨與退款流程", ], value="預約與營業時間", label="服務選單", elem_classes=["menu-radio"], ) gr.HTML( """
建議輸入具體資訊,例如訂單編號、付款方式、遇到的狀況或想確認的服務項目。
""" ) 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()