| import streamlit as st |
| from google import genai |
| from google.genai import types |
| import requests |
| import os |
| import time |
|
|
| |
| st.set_page_config(page_title="CIE CIE TAIPEI 線上客服", page_icon="🍸", layout="wide") |
|
|
| st.markdown( |
| """ |
| <style> |
| /* 1. 隱藏側邊欄捲軸 */ |
| [data-testid="stSidebar"] section::-webkit-scrollbar { display: none; } |
| [data-testid="stSidebar"] section { -ms-overflow-style: none; scrollbar-width: none; } |
| |
| /* 🚀 2. 全域字體縮小,適應手機螢幕 */ |
| html, body, [class*="st-"] { |
| font-size: 14px !important; |
| } |
| |
| /* 🚀 3. 調整範例按鈕樣式:變矮、字體變小、支援多行自動折行 */ |
| .stButton button { |
| width: 100%; |
| padding: 6px 10px; /* 縮小上下內距 */ |
| border-radius: 12px; |
| border: 1px solid #D4AF37; |
| color: #D4AF37; |
| background-color: transparent; |
| font-size: 13px !important; /* 讓按鈕字體再小一點 */ |
| line-height: 1.3; |
| min-height: auto; |
| white-space: normal; /* 允許長句子自動折行,避免撐爆寬度 */ |
| height: auto; |
| } |
| |
| /* 4. 極度壓縮主畫面空白 */ |
| .block-container { |
| padding-top: 1.2rem !important; |
| padding-bottom: 5rem !important; |
| padding-left: 0.8rem !important; |
| padding-right: 0.8rem !important; |
| } |
| |
| /* 5. 隱藏 Header 背景與右側選單,但保留左側展開按鈕 */ |
| header[data-testid="stHeader"] { |
| background-color: rgba(0,0,0,0) !important; |
| } |
| header[data-testid="stHeader"] #MainMenu { |
| visibility: hidden; |
| } |
| div[data-testid="collapsedControl"] { |
| visibility: visible !important; |
| background-color: #1A1A1A !important; |
| color: #D4AF37 !important; |
| border-radius: 0 5px 5px 0; |
| top: 5px; |
| } |
| </style> |
| """, |
| unsafe_allow_html=True |
| ) |
|
|
| |
| api_key = os.environ.get("GEMINI_API_KEY") |
| if not api_key: |
| st.error("請確認已經在 Space 的 Settings 設定了 GEMINI_API_KEY") |
| st.stop() |
|
|
| |
| client = genai.Client(api_key=api_key) |
|
|
| |
| |
| MD_BASE_URL = "https://raw.githubusercontent.com/ciecietaipei/ciecietaipei.github.io/main/ai-knowledge/" |
| |
| LOGO_URL = "https://raw.githubusercontent.com/ciecietaipei/ciecietaipei.github.io/main/assets/ciecie_logo_circle.png" |
| HOME_URL = "https://ciecietaipei.github.io" |
|
|
| KNOWLEDGE_MAP = { |
| "🍸 原創調酒 (Cocktails)": { |
| "raw_url": f"{MD_BASE_URL}cocktails.md", |
| "page_url": f"{HOME_URL}/cocktails.html", |
| "repo_url": "https://github.com/ciecietaipei/ciecietaipei.github.io/blob/main/ai-knowledge/cocktails.md" |
| }, |
| "🍷 經典酒水 (Drinks)": { |
| "raw_url": f"{MD_BASE_URL}drinks.md", |
| "page_url": f"{HOME_URL}/drinks.html", |
| "repo_url": "https://github.com/ciecietaipei/ciecietaipei.github.io/blob/main/ai-knowledge/drinks.md" |
| }, |
| "🍽️ 精緻餐點 (Foods)": { |
| "raw_url": f"{MD_BASE_URL}foods.md", |
| "page_url": f"{HOME_URL}/foods.html", |
| "repo_url": "https://github.com/ciecietaipei/ciecietaipei.github.io/blob/main/ai-knowledge/foods.md" |
| }, |
| "🛋️ 空間環境 (Environment)": { |
| "raw_url": f"{MD_BASE_URL}environment.md", |
| "page_url": f"{HOME_URL}/environment.html", |
| "repo_url": "https://github.com/ciecietaipei/ciecietaipei.github.io/blob/main/ai-knowledge/environment.md" |
| } |
| } |
|
|
| |
| def fetch_all_knowledge(): |
| combined_knowledge = "" |
| with st.spinner("正在同步 CIE CIE 最新菜單與資訊..."): |
| for category, info in KNOWLEDGE_MAP.items(): |
| try: |
| response = requests.get(info["raw_url"]) |
| response.raise_for_status() |
| combined_knowledge += f"\n\n## 【領域:{category}】\n" |
| combined_knowledge += response.text |
| except Exception as e: |
| st.warning(f"無法同步 {category} 的資料:請確認 GitHub 路徑是否正確。錯誤訊息:{e}") |
| return combined_knowledge |
|
|
| |
| if "knowledge" not in st.session_state: |
| st.session_state.knowledge = fetch_all_knowledge() |
|
|
| if "messages" not in st.session_state: |
| st.session_state.messages = [] |
|
|
| if "example_prompt" not in st.session_state: |
| st.session_state.example_prompt = None |
|
|
| |
| with st.sidebar: |
| |
| st.markdown(f'<a href="{HOME_URL}" target="_blank"><img src="{LOGO_URL}" width="100%" style="border-radius:50%; margin-bottom:20px;"></a>', unsafe_allow_html=True) |
| st.title("⚙️ 知識庫狀態") |
| for category, info in KNOWLEDGE_MAP.items(): |
| with st.expander(category): |
| st.markdown(f"🔗 [瀏覽網頁]({info['page_url']})") |
| st.markdown(f"📂 [GitHub 原始碼]({info['repo_url']})") |
| st.markdown("---") |
| if st.button("🔄 手動更新菜單資訊"): |
| st.session_state.knowledge = fetch_all_knowledge() |
| st.success("資料已重新抓取!") |
|
|
| |
| st.title("🍸 CIECIE 微醺客服") |
| st.caption("關於酒單、餐點、環境或訂位資訊與規範,歡迎直接詢問!台北市大安區信義路四段390號;02-2709-3446") |
|
|
| |
| example_cols = st.columns(6) |
| examples = [ |
| "🍸 有推薦的『茶香』或『花果系』原創調酒嗎?", |
| "🍜 深夜想吃點熱的主食,聽說有厲害的餐點?", |
| "🛋️ 幫朋友慶生,請問二樓的沙發區環境如何?", |
| "🪵 想獨飲,推薦一杯酒感重、帶木質調的大人味特調?", |
| "🥃 請問你們有供應單一麥芽威士忌的單杯純飲嗎?", |
| "🍤 有什麼推薦適合四個人的下酒菜分享?" |
| ] |
|
|
| for col, ex in zip(example_cols, examples): |
| if col.button(ex): |
| st.session_state.example_prompt = ex |
|
|
| |
| def get_gemini_response(user_input): |
| system_instruction = f""" |
| 你現在是位於台北市大安區信義安和的微醺酒吧『CIE CIE TAIPEI』的專業線上客服小助手。 |
| 你的說話風格熱情、有質感且帶有一點輕鬆微醺的氛圍。 |
| |
| 以下是從官方網站最新同步的資訊(包含原創調酒、經典酒水、精緻餐點、環境與規範): |
| --- |
| {st.session_state.knowledge} |
| --- |
| |
| 【核心任務】: |
| 請嚴格基於上述提供的資訊來回答客人的問題。 |
| 如果客人問了菜單上沒有的品項,或超出了上述資訊範圍,請禮貌地告知:「目前小助手手邊沒有這項資訊,建議您直接私訊我們的 IG 或是現場詢問調酒師喔!」 |
| |
| 【嚴格輸出限制】: |
| 1. 在回覆的最一開始,請務必先明確標示你是參考了文本中的哪個分類來作答(請擷取對應的【領域:XXX】標題)。例如:「🍸 **為您翻閱:【領域:🍸 原創調酒 (Cocktails)】**」,換行後再開始熱情地回答。 |
| 2. 絕對禁止輸出任何你的內部思考過程、計畫草稿、英文標籤、或是角色設定分析。 |
| 3. 所有的輸出內容必須 100% 都是直接面對客人溝通的繁體中文對話。 |
| """ |
| |
| |
| max_retries = 2 |
| for attempt in range(max_retries): |
| try: |
| |
| history_format = [] |
| for msg in st.session_state.messages: |
| role = "user" if msg["role"] == "user" else "model" |
| history_format.append( |
| types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])]) |
| ) |
|
|
| |
| chat = client.chats.create( |
| model="gemma-4-31b-it", |
| config=types.GenerateContentConfig( |
| system_instruction=system_instruction, |
| temperature=0.4, |
| ), |
| history=history_format |
| ) |
| |
| response = chat.send_message(user_input) |
| return response.text |
| |
| except Exception as e: |
| error_msg = str(e) |
| |
| |
| if "500" in error_msg and attempt < (max_retries - 1): |
| time.sleep(5) |
| continue |
| |
| |
| if "429" in error_msg or "quota" in error_msg.lower(): |
| return "⚠️ **系統提示:**\n\n目前吧台前詢問的客人較多,小助手稍微忙不過來了!請您先喝杯水,稍等幾分鐘後再試喔!" |
| elif "500" in error_msg: |
| return "🔄 **系統提示:**\n\n小助手剛剛去倒了杯酒(連線短暫不穩定),請您再點擊發送一次剛剛的問題喔!" |
| else: |
| return f"❌ **發生預期外錯誤**\n\n訊息:{error_msg}" |
|
|
| |
| prompt = st.chat_input("想喝點什麼?或是想了解什麼資訊呢?") |
|
|
| if st.session_state.example_prompt: |
| prompt = st.session_state.example_prompt |
| st.session_state.example_prompt = None |
|
|
| if prompt: |
| st.session_state.messages.append({"role": "user", "content": prompt}) |
| |
| for message in st.session_state.messages: |
| with st.chat_message(message["role"]): |
| st.markdown(message["content"]) |
|
|
| with st.chat_message("assistant"): |
| response_text = get_gemini_response(prompt) |
| st.markdown(response_text) |
| |
| st.session_state.messages.append({"role": "assistant", "content": response_text}) |
| st.rerun() |
| else: |
| for message in st.session_state.messages: |
| with st.chat_message(message["role"]): |
| st.markdown(message["content"]) |