根據香港法律,不得在業務過程中,向未成年人售賣或供應令人醺醉的酒類。
Under the law of Hong Kong, intoxicating liquor must not be sold or supplied to a minor in the course of business.

Domaine Georges Lignier & Fils Bonnes-Mares Grand Cru 1999 (Allen Meadows:91)

FRR1679

Notify me when this product is available:

Domaine Georges Lignier & Fils 紮根於 Morey-Saint-Denis 村莊,是該區的精品酒莊,擁有逾百年歷史。自 2003 年起,酒莊逐步交由創辦人 Georges Lignier 的外甥孫 Benoit Stehly 掌舵。

酒莊風格以「傳統與優雅」著稱,保留較高比例的原串發酵 (Whole-Bunch Fermentation),追求通透感與陳年潛力。

現時酒莊擁有 16 公頃的葡萄園,涵蓋 17 個產區,其中包括 5 個極具代表性的特級園(Grand Cru)。

這款來自特級園 Bonnes-Mares,葡萄藤平均樹齡超過 50 年。由於酒莊在 Bonnes-Mares 僅有約 0.33 公頃年產量平均只有1,000-1,500支,極為稀少。1999 年是布爾岡公認的好年份,結構與陳年潛力兼備。

產區保護條例:
布爾岡 Cote de Nuits, AOC Bonnes-Mares Grand Cru

產地:
法國

種類:
紅葡萄酒

葡萄品種:
Pinot Noir

酒精度:
13.5%

容量:
750毫升

釀造:

60%-80% 原串發酵,發酵前低溫浸泡,使用天然酵母。在小橡木桶中熟成 18 個月 (30%-50% 新桶)

品酒筆記:
(Allen Meadows' rating: 91 points)
鮮明的木質香料氣息,伴隨著紅、黑漿果香。風味強勁而結構紮實,擁有極佳的集中度與出色的複雜度。其基本特徵非常接近 Clos de la Roche(酒莊另一款特級園),但集中度更高,餘韻也更悠長。如果能在酒窖中陳年 10 年以上,這將是一款極其出色的佳釀。


Domaine Georges Lignier & Filsis an artisanal estate with over a century of history in the heart of Morey-Saint-Denis. Since 2003, the domaine has been gradually handed over to Benoît Stehly, the great-nephew of founder Georges Lignier.

The estate’s style is renowned for “tradition and elegance,”, characterized by a relatively high proportion of whole-bunch fermentation to pursue exceptional transparency and long-term aging potential.

At present, the domaine owns 16 hectares across 17 appellations, including five prestigious Grands Crus. This exceptional wine is crafted from the Grand Cru Bonnes-Mares, where the vines average over 50 years of age. As the estate holds only about 0.33 hectares in Bonnes-Mares, is extremely limited—averaging only 1,000 to 1,500 bottles.The 1999 vintage is recognized as a benchmark year in Burgundy, offering both powerful structure and superb longevity.

Appellation:
AOC Bonnes-Mares Grand Cru, Cote de Nuits, Burgundy

Country of Origin:
France

Type:
Red/Rouge (Still)

Grape Varieties:
Pinot Noir

Alcohol Content:
13.5%

Volume:
750mL

Vinification:

60–80% whole bunch fermentation, preceded by cold maceration, using native yeasts. Matured for 18 months in traditional French oak barrels, with 30–50% new oak.

Tasting note:
(Allen Meadows' rating: 91 points)
Noticeable wood spice and an intense mix of both red and black fruit aromas leads to powerful, solidly structured flavors that possess very good intensity and excellent complexity. This is very Clos de la Roche in basic character though it has better concentration and a longer finish. This should be a very fine effort if it's given 10+ years in the cellar.



Next Previous

(function () { var CHAT_IDS = ["sens-ai-root", "sens-ai-panel", "sens-ai-launcher", "sens-ai-toast"]; function dedupeChatWidgets() { CHAT_IDS.forEach(function (id) { var nodes = document.querySelectorAll("#" + id); for (var i = nodes.length - 1; i > 0; i--) { nodes[i].remove(); } }); } function mountChatToBody() { CHAT_IDS.forEach(function (id) { var el = document.getElementById(id); if (el && el.parentNode !== document.body) { document.body.appendChild(el); } }); } dedupeChatWidgets(); if (window.__SENS_AI_BOOTED__) { return; } mountChatToBody(); var rootMount = document.getElementById("sens-ai-root"); var CONFIG = { apiUrl: "/apps/sens-ai/chat", shopUrl: (rootMount && rootMount.getAttribute("data-shop-url")) || window.location.origin }; var STORAGE_LAYOUT = "sens_ai_panel_layout"; var STORAGE_MIN = "sens_ai_panel_minimized"; var STORAGE_SESSION = "sens_ai_chat_session"; var STORAGE_NAV_OPEN = "sens_ai_open_after_nav"; var DEFAULT_W = 420; var DEFAULT_H = 600; var MIN_W = 320; var MIN_H = 360; var MINIMIZED_H = 52; var MARGIN = 16; var MOBILE = 768; var MOBILE_DOCK_H_RATIO = 0.82; var panel = document.getElementById("sens-ai-panel"); var launcher = document.getElementById("sens-ai-launcher"); var dragHandle = document.getElementById("sens-ai-drag-handle"); var newChatBtn = document.getElementById("sens-ai-new-chat"); var maximizeBtn = document.getElementById("sens-ai-maximize"); var minimizeBtn = document.getElementById("sens-ai-minimize"); var messagesEl = document.getElementById("sens-ai-messages"); var formEl = document.getElementById("sens-ai-form"); var inputEl = document.getElementById("sens-ai-input"); var sendEl = document.getElementById("sens-ai-send"); var toastEl = document.getElementById("sens-ai-toast"); if (!panel || !launcher) return; window.__SENS_AI_BOOTED__ = true; var layout = null; var expandedLayout = null; var preMaximizeLayout = null; var isMinimized = false; var isMaximized = false; var isDragging = false; var isResizing = false; var dragState = null; var resizeState = null; var headerClickStart = null; var HEADER_CLICK_THRESHOLD = 6; var history = []; var busy = false; var WELCOME_MESSAGE = "Welcome — I'm your SENS cellar advisor. Tell me what you're eating, celebrating, or craving, and I'll suggest bottles from our shop."; var BUTLER_AVATAR_LABEL = "Your Wine Butler"; var BUTLER_AVATAR_SIZE = 40; var BUTLER_AVATAR_SVG = '"; function getButlerAvatarUrl() { if (!rootMount) return ""; return String(rootMount.getAttribute("data-butler-avatar-url") || "").trim(); } function getButlerAdvisorName() { if (!rootMount) return "Mirai みらい"; var name = String(rootMount.getAttribute("data-butler-advisor-name") || "Mirai みらい").trim(); return name || "Mirai みらい"; } function applyButlerAvatarContent(avatar) { var url = getButlerAvatarUrl(); avatar.classList.toggle("sens-ai-msg__avatar--image", !!url); avatar.textContent = ""; if (url) { var img = document.createElement("img"); img.src = url; img.alt = ""; img.width = BUTLER_AVATAR_SIZE; img.height = BUTLER_AVATAR_SIZE; img.loading = "lazy"; img.decoding = "async"; avatar.appendChild(img); return; } avatar.innerHTML = BUTLER_AVATAR_SVG; } function createButlerSender() { var advisorName = getButlerAdvisorName(); var sender = document.createElement("div"); sender.className = "sens-ai-msg__sender"; var avatar = document.createElement("div"); avatar.className = "sens-ai-msg__avatar"; avatar.setAttribute("role", "img"); avatar.setAttribute("aria-label", advisorName + ", " + BUTLER_AVATAR_LABEL); avatar.setAttribute("title", advisorName); applyButlerAvatarContent(avatar); var name = document.createElement("p"); name.className = "sens-ai-msg__sender-name"; name.textContent = advisorName; sender.appendChild(avatar); sender.appendChild(name); return sender; } function ensureBotMessageAvatars() { messagesEl.querySelectorAll(".sens-ai-msg--bot").forEach(function (msg) { var sender = msg.querySelector(":scope > .sens-ai-msg__sender"); if (sender) { var avatar = sender.querySelector(".sens-ai-msg__avatar"); if (avatar) applyButlerAvatarContent(avatar); var nameEl = sender.querySelector(".sens-ai-msg__sender-name"); if (nameEl) nameEl.textContent = getButlerAdvisorName(); return; } var bareAvatar = msg.querySelector(":scope > .sens-ai-msg__avatar"); if (bareAvatar) { bareAvatar.replaceWith(createButlerSender()); return; } msg.insertBefore(createButlerSender(), msg.firstChild); }); } function isMobile() { return window.innerWidth <= MOBILE; } function mobileDockLayout() { var w = window.innerWidth; var ratio = isMaximized ? 1 : MOBILE_DOCK_H_RATIO; var h = Math.round(Math.min(window.innerHeight * ratio, window.innerHeight - (isMaximized ? 0 : 48))); return { x: 0, y: Math.max(0, window.innerHeight - h), width: w, height: h }; } function maximizeLayout() { if (isMobile()) { return { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight }; } return { x: MARGIN, y: MARGIN, width: window.innerWidth - MARGIN * 2, height: window.innerHeight - MARGIN * 2 }; } function setButtonTip(btn, tip) { if (!btn) return; btn.setAttribute("data-tip", tip); btn.title = tip; btn.setAttribute("aria-label", tip); } function syncMaximizeButton() { if (!maximizeBtn) return; if (isMaximized) { maximizeBtn.textContent = "⤢"; setButtonTip(maximizeBtn, "Restore size · 還原大小"); } else { maximizeBtn.textContent = "⛶"; setButtonTip(maximizeBtn, "Maximize · 放大"); } } function setMaximized(max) { if (isMinimized || isMobile() && isMinimized) return; if (max === isMaximized) return; isMaximized = max; panel.classList.toggle("is-maximized", max); syncMaximizeButton(); if (max) { preMaximizeLayout = layout; var next = maximizeLayout(); layout = next; panel.style.left = next.x + "px"; panel.style.top = next.y + "px"; panel.style.width = next.width + "px"; panel.style.height = next.height + "px"; expandedLayout = next; return; } if (preMaximizeLayout) { applyLayout(preMaximizeLayout); expandedLayout = layout; preMaximizeLayout = null; } else if (!isMobile()) { applyLayout(expandedLayout || defaultLayout()); } else { applyLayout(mobileDockLayout()); } } function defaultLayout() { if (isMobile()) { return mobileDockLayout(); } var w = DEFAULT_W; var h = DEFAULT_H; return { x: Math.max(MARGIN, window.innerWidth - w - MARGIN), y: Math.max(MARGIN, window.innerHeight - h - MARGIN), width: w, height: h }; } function syncMobileMode() { panel.classList.toggle("sens-ai-panel--mobile-dock", isMobile()); } function clampLayout(next, opts) { opts = opts || {}; if (isMaximized && !isMinimized) { return next; } if (isMobile() && !isMinimized && !opts.allowFreePosition) { return mobileDockLayout(); } var minW = opts.minWidth || MIN_W; var minH = opts.minHeight || MIN_H; var maxW = window.innerWidth - MARGIN * 2; var maxH = window.innerHeight - MARGIN * 2; var width = Math.max(minW, Math.min(next.width, maxW)); var height = Math.max(minH, Math.min(next.height, maxH)); var x = Math.max(MARGIN, Math.min(next.x, window.innerWidth - width - MARGIN)); var y = Math.max(MARGIN, Math.min(next.y, window.innerHeight - height - MARGIN)); return { x: x, y: y, width: width, height: height }; } function loadMinimizedPreference() { try { if (sessionStorage.getItem(STORAGE_NAV_OPEN) === "1") { sessionStorage.removeItem(STORAGE_NAV_OPEN); return false; } var raw = localStorage.getItem(STORAGE_MIN); if (raw === "0") return false; if (raw === "1") return true; } catch (e) {} return true; } function saveChatSession() { try { sessionStorage.setItem( STORAGE_SESSION, JSON.stringify({ html: messagesEl.innerHTML, history: history }) ); } catch (e) {} } function restoreChatSession() { try { var raw = sessionStorage.getItem(STORAGE_SESSION); if (!raw) return false; var data = JSON.parse(raw); if (data && data.html) { messagesEl.innerHTML = data.html; ensureBotMessageAvatars(); } if (data && Array.isArray(data.history)) { history = data.history; } return !!(data && data.html); } catch (e) {} return false; } function clearChatSession() { try { sessionStorage.removeItem(STORAGE_SESSION); } catch (e) {} } function persistOpenForNavigation() { try { localStorage.setItem(STORAGE_MIN, "0"); sessionStorage.setItem(STORAGE_NAV_OPEN, "1"); } catch (e) {} saveChatSession(); } function isWineProductLink(link) { if (!link || !link.href) return false; if (link.getAttribute("href") === "#") return false; return ( link.classList.contains("sens-ai-btn--link") || (link.closest(".sens-ai-wine-title") && link.closest("#sens-ai-panel")) ); } function handleWineProductNavigation(event) { var link = event.target.closest("#sens-ai-panel a.sens-ai-btn--link, #sens-ai-panel .sens-ai-wine-title a"); if (!link || !isWineProductLink(link)) return; persistOpenForNavigation(); } function loadLayout() { if (isMobile()) { return mobileDockLayout(); } try { var raw = localStorage.getItem(STORAGE_LAYOUT); if (raw) return clampLayout(JSON.parse(raw), { allowFreePosition: true }); } catch (e) {} return defaultLayout(); } function saveLayout(next) { try { localStorage.setItem(STORAGE_LAYOUT, JSON.stringify(next)); } catch (e) {} } function applyLayout(next) { layout = clampLayout(next, isMinimized ? { minWidth: 180, minHeight: MINIMIZED_H, allowFreePosition: true } : {}); panel.style.left = layout.x + "px"; panel.style.top = layout.y + "px"; panel.style.width = layout.width + "px"; panel.style.height = layout.height + "px"; if (!isMobile() && !isMaximized && !isMinimized) saveLayout(layout); } function setMinimized(min) { if (min && isMaximized) setMaximized(false); isMinimized = min; panel.classList.remove("is-minimized"); panel.setAttribute("aria-hidden", min ? "true" : "false"); launcher.hidden = !min; try { localStorage.setItem(STORAGE_MIN, min ? "1" : "0"); } catch (e) {} if (min) { if (layout) expandedLayout = layout; panel.classList.remove("is-ready"); } else { panel.classList.add("is-ready"); applyLayout(isMobile() ? mobileDockLayout() : (expandedLayout || defaultLayout())); } } function openPanel() { setMinimized(false); inputEl.focus(); } function initPanelPosition() { layout = loadLayout(); expandedLayout = layout; applyLayout(layout); restoreChatSession(); ensureBotMessageAvatars(); setMinimized(loadMinimizedPreference()); scrollMessages(); } function onPointerMove(clientX, clientY) { if (dragState) { if (isMobile()) { endPointer(); return; } applyLayout({ x: dragState.originX + (clientX - dragState.startX), y: dragState.originY + (clientY - dragState.startY), width: layout.width, height: layout.height }); if (!isMinimized) expandedLayout = layout; } if (resizeState && !isMinimized && !isMobile()) { var dx = clientX - resizeState.startX; var dy = clientY - resizeState.startY; var x = resizeState.originX; var y = resizeState.originY; var w = resizeState.originW; var h = resizeState.originH; var handle = resizeState.handle; if (handle.indexOf("e") >= 0) w = resizeState.originW + dx; if (handle.indexOf("w") >= 0) { w = resizeState.originW - dx; x = resizeState.originX + dx; } if (handle.indexOf("s") >= 0) h = resizeState.originH + dy; if (handle.indexOf("n") >= 0) { h = resizeState.originH - dy; y = resizeState.originY + dy; } var next = clampLayout({ x: x, y: y, width: w, height: h }); if (handle.indexOf("w") >= 0) next.x = resizeState.originX + resizeState.originW - next.width; if (handle.indexOf("n") >= 0) next.y = resizeState.originY + resizeState.originH - next.height; applyLayout(next); expandedLayout = layout; } } function endPointer(e) { if (dragState && headerClickStart && !isMinimized) { var clientX = headerClickStart.x; var clientY = headerClickStart.y; if (e) { if (typeof e.clientX === "number") { clientX = e.clientX; clientY = e.clientY; } else if (e.changedTouches && e.changedTouches[0]) { clientX = e.changedTouches[0].clientX; clientY = e.changedTouches[0].clientY; } } var dx = clientX - headerClickStart.x; var dy = clientY - headerClickStart.y; if (dx * dx + dy * dy <= HEADER_CLICK_THRESHOLD * HEADER_CLICK_THRESHOLD) { setMinimized(true); } } headerClickStart = null; if (dragState || resizeState) { dragState = null; resizeState = null; isDragging = false; isResizing = false; panel.classList.remove("is-dragging", "is-resizing"); document.body.style.userSelect = ""; } } function beginDrag(clientX, clientY) { if (isMobile()) return; if (isMaximized) setMaximized(false); isDragging = true; panel.classList.add("is-dragging"); dragState = { startX: clientX, startY: clientY, originX: layout.x, originY: layout.y }; document.body.style.userSelect = "none"; } function beginResize(handle, clientX, clientY) { if (isMinimized) return; if (isMaximized) setMaximized(false); isResizing = true; panel.classList.add("is-resizing"); resizeState = { handle: handle, startX: clientX, startY: clientY, originX: layout.x, originY: layout.y, originW: layout.width, originH: layout.height }; document.body.style.userSelect = "none"; } dragHandle.addEventListener("mousedown", function (e) { if (e.button !== 0 || isMinimized) return; if (e.target.closest(".sens-ai-panel__icon-btn")) return; headerClickStart = { x: e.clientX, y: e.clientY }; e.preventDefault(); beginDrag(e.clientX, e.clientY); }); dragHandle.addEventListener("touchstart", function (e) { if (isMinimized) return; if (e.target.closest(".sens-ai-panel__icon-btn")) return; var t = e.touches[0]; if (!t) return; headerClickStart = { x: t.clientX, y: t.clientY }; beginDrag(t.clientX, t.clientY); }, { passive: true }); panel.querySelectorAll("[data-resize]").forEach(function (el) { function start(e, clientX, clientY) { e.preventDefault(); e.stopPropagation(); beginResize(el.getAttribute("data-resize"), clientX, clientY); } el.addEventListener("mousedown", function (e) { if (e.button !== 0) return; start(e, e.clientX, e.clientY); }); el.addEventListener("touchstart", function (e) { var t = e.touches[0]; if (!t) return; start(e, t.clientX, t.clientY); }, { passive: false }); }); window.addEventListener("mousemove", function (e) { onPointerMove(e.clientX, e.clientY); }); window.addEventListener("mouseup", endPointer); window.addEventListener("touchmove", function (e) { var t = e.touches[0]; if (t && (dragState || resizeState)) onPointerMove(t.clientX, t.clientY); }, { passive: true }); window.addEventListener("touchend", endPointer); window.addEventListener("resize", function () { syncMobileMode(); if (isMaximized && !isMinimized) { var next = maximizeLayout(); layout = next; panel.style.left = next.x + "px"; panel.style.top = next.y + "px"; panel.style.width = next.width + "px"; panel.style.height = next.height + "px"; expandedLayout = next; return; } if (isMobile() && !isMinimized) { applyLayout(mobileDockLayout()); expandedLayout = layout; } else if (layout && !isMinimized) { applyLayout(layout); } }); minimizeBtn.addEventListener("click", function (e) { e.stopPropagation(); setMinimized(true); }); if (maximizeBtn) { maximizeBtn.addEventListener("click", function (e) { e.stopPropagation(); if (isMinimized) return; setMaximized(!isMaximized); }); } if (newChatBtn) { newChatBtn.addEventListener("click", function (e) { e.stopPropagation(); startNewChat(); }); } launcher.addEventListener("click", openPanel); function resetInputHeight() { inputEl.style.height = "24px"; } function growInputHeight() { inputEl.style.height = "24px"; inputEl.style.height = Math.min(inputEl.scrollHeight, 180) + "px"; } function escapeHtml(text) { return String(text) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function formatReply(text) { var safe = escapeHtml(text || ""); safe = safe.replace(/\*\*(.+?)\*\*/g, "$1"); return safe.replace(/\n/g, "
"); } var LOADING_MESSAGE = "Mirai is thinking…"; var READ_MORE_LABEL = "Read more"; var READ_MORE_LABEL_LESS = "Show less"; var READ_MORE_SKIP_REPLY_RE = [ /^Finding wines for you/i, /^Thinking/i, /^Just a moment/i, /^One moment/i, /^Sorry, something went wrong/i, /couldn't find/i, /could not find/i, /no specific wine/i, /checked our current selection/i, /^暫時未/i, /^今次未/i, /^現在、ご希望/i, /^今回のご希望/i, ]; function buildReadMoreHtml(previewText, extraClass, fullText, previewHtml) { var preview = String(previewText || "").trim(); var full = String(fullText || preview).trim(); var cls = "sens-ai-read-more" + (extraClass ? " " + extraClass : ""); var inner = previewHtml != null ? previewHtml : escapeHtml(preview); return ( '
' + '
' + inner + "
" + '
" ); } function textLooksTruncated(text) { return /(?:…|\.\.\.)[\s]*$/.test(String(text || "").trim()); } function isReadMoreExcludedReply(text) { var t = String(text || "").trim(); for (var i = 0; i < READ_MORE_SKIP_REPLY_RE.length; i++) { if (READ_MORE_SKIP_REPLY_RE[i].test(t)) return true; } return false; } function shouldUseReadMoreReply(previewText, fullText) { if (isReadMoreExcludedReply(previewText)) return false; var preview = String(previewText || "").trim(); var full = String(fullText || preview).trim(); if (!preview) return false; if (full.length > preview.length + 8) return true; if (textLooksTruncated(preview)) return true; if (preview.length >= 360) return true; return false; } function shouldUseReadMoreCard(previewText, fullText) { var preview = String(previewText || "").trim(); var full = String(fullText || preview).trim(); if (!preview) return false; if (full.length > preview.length + 8) return true; if (textLooksTruncated(preview)) return true; if (preview.length >= 120) return true; return false; } function extractBoldWineNames(text) { var names = []; var re = /\*\*([^*]{3,140})\*\*/g; var match; while ((match = re.exec(String(text || ""))) !== null) { var name = match[1].replace(/\s+/g, " ").trim(); if (name && !/^(red|white|wine|wines|紅酒|白酒)$/i.test(name)) names.push(name); } return names; } function normalizeTitleKey(title) { return String(title || "") .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fff]+/gi, " ") .replace(/\s+/g, " ") .trim(); } function titleMatchesCandidate(wineTitle, candidate) { var a = normalizeTitleKey(wineTitle); var b = normalizeTitleKey(candidate); if (!a || !b) return false; if (a === b || a.indexOf(b) >= 0 || b.indexOf(a) >= 0) return true; var words = a.split(" ").filter(function (w) { return w.length > 2 || /^\d{4}$/.test(w); }); if (!words.length) return false; var hits = 0; for (var i = 0; i < words.length; i++) { if (b.indexOf(words[i]) >= 0) hits += 1; } return hits >= Math.min(3, Math.max(2, Math.ceil(words.length * 0.45))); } function findWineForName(name, wines) { if (!wines || !wines.length) return null; for (var i = 0; i < wines.length; i++) { if (titleMatchesCandidate(wines[i].title, name)) return wines[i]; } return null; } function isWineToFoodIntroBlock(block) { return /你問|You asked|what to pair with|可以配什么|に合う料理/i.test(block); } function isPairingTheoryBlock(block) { return ( /With that in mind, these SENS bottles stand out/i.test(block) || /在這個基礎上,SENS 酒窖/i.test(block) || /I start with wine traits/i.test(block) || /我會先從菜式需要嘅酒質入手/i.test(block) || /sweetness is the key — the wine should be as sweet or sweeter/i.test(block) || /甜度係關鍵 — 酒應同甜品一樣甜或更甜/i.test(block) ); } function isLikelyWinePickBlock(block) { if (!/\*\*[^*]+\*\*/.test(block)) return false; if (isWineToFoodIntroBlock(block)) return false; if (isPairingTheoryBlock(block)) return false; if (/HK\$|HKD|(\s*HK| at HK/i.test(block)) return true; if ( /^(?:First|Next|Another option|For red|For white|首先|另外|第三支|第四支|第五支|第六支|第七支|第八支|紅酒方面|白酒我會揀)/im.test( block ) ) { return true; } return false; } function isGenericPairingAdviceComment(text) { var cleaned = String(text || "").trim(); if (!cleaned) return false; return ( /With that in mind, these SENS bottles stand out/i.test(cleaned) || /在這個基礎上,SENS 酒窖/i.test(cleaned) || /I start with wine traits/i.test(cleaned) || /我會先從菜式需要嘅酒質入手/i.test(cleaned) || /In practice:\s*\*\*/i.test(cleaned) || /按呢個思路:/i.test(cleaned) || (/\*\*(?:Pinot Grigio|Vermentino|Albariño|Chablis|Chianti|Barolo)\*\*/i.test(cleaned) && !/HK\$|HKD| at HK/i.test(cleaned)) ); } function isProvenanceOnlyComment(text) { var cleaned = String(text || "").trim(); if (!cleaned) return false; return ( /^from\s+.+(?:\s*[·•]\s*)?(?:\d{4}\s*)?vintage\s*[—–-]\s*drinking well now\.?$/i.test(cleaned) || /^from\s+.+\s*[—–-]\s*drinking well now\.?$/i.test(cleaned) || /^產區係\s+.+(?:,已進入適飲期)?。?$/u.test(cleaned) ); } function isBudgetOnlyComment(text) { var cleaned = String(text || "").trim(); if (!cleaned) return false; if (!/within your budget|fits your search|matches your search|在你預算|風格貼合今次搜尋|符合你今次搜尋/i.test(cleaned)) { return false; } var stripped = cleaned .replace(/\bat\s+HK\$[\d,]+/gi, "") .replace(/HK\$[\d,]+/gi, "") .replace(/HKD\s*[\d,]+/gi, "") .replace(/within your budget and fits your search\.?/gi, "") .replace(/within your budget\.?/gi, "") .replace(/fits your search\.?/gi, "") .replace(/matches your search\.?/gi, "") .replace(/在你預算之內[,,]?/g, "") .replace(/風格貼合今次搜尋\.?/g, "") .replace(/符合你今次搜尋[^。]*\.?/g, "") .replace(/^[,,。.!!?\s—–-]+|[,,。.!!?\s—–-]+$/g, "") .trim(); return stripped.length < 16; } function stripProvenanceTail(text) { return String(text || "") .replace(/\s+from\s+[^.]+?\s*[—–-]\s*drinking well now\.?\s*$/i, "") .replace(/\s+產區係\s+[^。]+?(?:,已進入適飲期)?。?\s*$/u, "") .trim(); } function cleanWineReplyComment(block, wineTitle) { var text = String(block || "").trim(); text = text.replace(/\*\*([^*]+)\*\*/g, function (_match, inner) { return titleMatchesCandidate(wineTitle, inner) ? "" : inner; }); text = text.replace(/[((]\s*HK\$[\d,]+[^))]*[))]/gi, " "); text = text.replace(/[((]\s*HKD\s*[\d,]+[^))]*[))]/gi, " "); text = text.replace(/\bat\s+HK\$[\d,]+/gi, " "); text = text.replace( /^(?:First|Next|Another option|For red[^,—–-]*[,—–-]|For white[^,—–-]*[,—–-]|首先[,,]?|另外[,,]?|第三支[,,]?|第四支[,,]?|紅酒方面[,,][^,,—–-]*[,,—–-]?|白酒我會揀)\s*/i, "" ); text = text.replace(/^[—–\-:,,。\s]+/, "").replace(/\s+/g, " ").trim(); text = stripProvenanceTail(text); return text; } function prepareReplyAndWines(reply, wines) { if (!wines || !wines.length) { return { bubbleText: reply, cardWines: [], wineComments: {} }; } var blocks = String(reply || "") .split(/\n{2,}/) .map(function (block) { return block.trim(); }) .filter(Boolean); var featured = []; var wineComments = {}; var pickBlockIndexes = {}; var seenTitles = {}; blocks.forEach(function (block, index) { if (!isLikelyWinePickBlock(block)) return; var names = extractBoldWineNames(block); var matchedWine = null; for (var i = 0; i < names.length; i++) { var candidate = findWineForName(names[i], wines); if (candidate && !seenTitles[candidate.title]) { matchedWine = candidate; break; } } if (!matchedWine) return; var comment = cleanWineReplyComment(block, matchedWine.title); if (comment.length < 8 || isBudgetOnlyComment(comment)) return; wineComments[matchedWine.title] = comment; pickBlockIndexes[index] = true; featured.push(matchedWine); seenTitles[matchedWine.title] = true; }); if (!featured.length) { wines.forEach(function (wine) { var reason = sanitizeCardReason(wine.match_reason || wine.pairing_reason || ""); if (!reason) return; wineComments[wine.title] = reason; featured.push(wine); }); if (!featured.length) { return { bubbleText: reply, cardWines: wines, wineComments: {} }; } } var bubbleParts = []; blocks.forEach(function (block, index) { if (pickBlockIndexes[index]) return; bubbleParts.push(block); }); return { bubbleText: bubbleParts.join("\n\n") || reply, cardWines: wines, wineComments: wineComments, }; } function storefrontRoot() { return String(CONFIG.shopUrl || window.location.origin || "").replace(/\/+$/, ""); } function wineProductUrl(wine) { var root = storefrontRoot(); var url = String((wine && wine.url) || "").trim(); var handle = String((wine && wine.handle) || "").trim(); if (url && url !== "#") { url = url.replace(/\/apps\/sens-ai\/products\//i, "/products/"); if (/^https?:\/\//i.test(url)) { var absoluteMatch = url.match(/^(https?:\/\/[^/]+)(\/products\/[^/?#]+)/i); if (absoluteMatch) return absoluteMatch[1] + absoluteMatch[2]; return url; } if (url.charAt(0) === "/") return root + url; var relativeMatch = url.match(/\/products\/([^/?#]+)/i); if (relativeMatch) return root + "/products/" + relativeMatch[1]; return url; } if (handle) return root + "/products/" + handle.replace(/^\/+|\/+$/g, ""); return "#"; } function wineVariantId(wine) { var raw = wine && wine.variant_id; if (raw == null || raw === "") return null; var parsed = Number(raw); if (!Number.isFinite(parsed) || parsed <= 0) return null; return parsed; } function formatReplyWithReadMore(text) { var trimmed = String(text || "").trim(); if (!trimmed) return ""; var formatted = formatReply(trimmed); if (!shouldUseReadMoreReply(trimmed, trimmed)) return formatted; return buildReadMoreHtml(trimmed, "sens-ai-read-more--reply", trimmed, formatted); } function defaultWineComment(wine) { var notesPreview = String(wine.tasting_notes || "").trim(); if (notesPreview) return notesPreview; var reason = sanitizeCardReason(wine.match_reason || wine.pairing_reason || ""); if (reason) return reason; var notesFull = String(wine.tasting_notes_full || "").trim(); if (notesFull) return notesFull; var title = String(wine.title || "this bottle").trim(); return "A curated pick from the SENS cellar — " + title + "."; } function cardCommentPreviewFull(wine, replyComment) { var desc = wineCardDescription(wine, replyComment); var preview = desc.preview || defaultWineComment(wine); var full = desc.full || preview; var notesPreview = String(wine.tasting_notes || "").trim(); var notesFull = String(wine.tasting_notes_full || "").trim(); if (notesFull && notesFull.length > preview.length + 8) { var previewStem = preview.replace(/(?:…|\.\.\.)[\s]*$/, "").trim().toLowerCase(); var notesStem = notesPreview.replace(/(?:…|\.\.\.)[\s]*$/, "").trim().toLowerCase(); var matchesNotes = (notesPreview && (preview === notesPreview || (notesStem && previewStem === notesStem))) || (previewStem.length >= 16 && notesFull.toLowerCase().indexOf(previewStem.slice(0, Math.min(56, previewStem.length))) === 0); if (matchesNotes || (textLooksTruncated(preview) && !desc.fromReply)) { preview = notesPreview || preview; full = notesFull; } } return { preview: preview, full: full }; } function wineCardDescription(wine, replyComment) { var comment = String(replyComment || "").trim(); if (comment) { comment = stripProvenanceTail(comment); } if (comment && isBudgetOnlyComment(comment)) comment = ""; var reason = sanitizeCardReason(wine.match_reason || wine.pairing_reason || ""); var notesPreview = String(wine.tasting_notes || "").trim(); var notesFull = String(wine.tasting_notes_full || "").trim(); if ( comment && !isProvenanceOnlyComment(comment) && !isGenericPairingAdviceComment(comment) && (!reason || comment.length >= Math.min(reason.length, 40)) ) { return { preview: comment, full: comment, fromReply: true }; } if (reason) { if (notesFull && notesFull.length > reason.length + 8 && textLooksTruncated(reason)) { return { preview: notesPreview || reason, full: notesFull, fromReply: false }; } return { preview: reason, full: reason, fromReply: false }; } if (notesPreview) { return { preview: notesPreview, full: notesFull || notesPreview, fromReply: false }; } if (comment) { return { preview: comment, full: comment, fromReply: true }; } var fallback = defaultWineComment(wine); return { preview: fallback, full: fallback, fromReply: false }; } function toggleReadMore(btn) { var wrap = btn.closest("[data-read-more]"); if (!wrap) return; var content = wrap.querySelector(".sens-ai-read-more__content"); var expanded = wrap.classList.toggle("is-expanded"); var preview = wrap.getAttribute("data-read-more-preview") || ""; var full = wrap.getAttribute("data-read-more-full") || preview; if (content && full !== preview) { content.textContent = expanded ? full : preview; } btn.setAttribute("aria-expanded", expanded ? "true" : "false"); btn.textContent = expanded ? READ_MORE_LABEL_LESS : READ_MORE_LABEL; } function formatPrice(value) { if (value == null || value === "") return ""; var n = Number(value); if (isNaN(n)) return ""; return "HK$" + n.toLocaleString("en-HK", { maximumFractionDigits: 0 }); } function scrollMessages() { messagesEl.scrollTop = messagesEl.scrollHeight; } function criticScoresFromTitle(title) { var re = /\b(RP|JS|WS|BH|WE|WH|NM|VN|Vinous)\s*:?\s*(\d{2})/gi; var parts = []; var seen = {}; var match; while ((match = re.exec(title || "")) !== null) { var src = match[1].toUpperCase(); if (src === "VINOUS") src = "VN"; var label = src + ":" + match[2]; if (seen[label]) continue; seen[label] = true; parts.push(label); } return parts.join(" · "); } function wineProfileText(wine, cardComment) { return [ wine.tasting_notes, cardComment, wine.match_reason, wine.pairing_reason, wine.title, ] .filter(Boolean) .join(" "); } var MIN_NOTES_FOR_RADAR = 50; var MIN_NOTES_STRONG = 80; var MIN_PROFILE_SPREAD = 0.35; function profileSpread(profile) { if (!profile || !profile.length) return 0; var vals = profile.map(function (d) { return Number(d.value); }); return Math.max.apply(null, vals) - Math.min.apply(null, vals); } function profileFromServer(wine) { if (wine.show_tasting_profile === false) return null; var raw = wine.tasting_profile; if (!raw || !Array.isArray(raw) || raw.length < 5) return null; return raw.map(function (d) { return { key: d.key, label: d.label || d.key, value: clampProfileScore(d.value), }; }); } function shouldShowTastingRadar(wine, profile) { return !!(profile && profile.length >= 5); } function resolveWineProfile(wine, cardComment) { var server = profileFromServer(wine); if (server) return server; return buildWineProfileDimensions(wine, cardComment); } function clampProfileScore(value) { var n = Number(value); if (isNaN(n)) n = 3.2; return Math.max(1.8, Math.min(4.9, Math.round(n * 10) / 10)); } function keywordScore(text, patterns, base, boost) { var hits = 0; for (var i = 0; i < patterns.length; i++) { if (patterns[i].test(text)) hits += 1; } return clampProfileScore(base + Math.min(hits * (boost || 0.22), 1.35)); } function extractVintageYear(text) { var match = String(text || "").match(/\b(19|20)\d{2}\b/); return match ? parseInt(match[0], 10) : null; } function criticScoreAverage(text) { var re = /\b(RP|JS|WS|BH|WE|WH|NM|VN|Vinous)\s*:?\s*(\d{2})/gi; var total = 0; var count = 0; var match; while ((match = re.exec(text || "")) !== null) { total += parseInt(match[2], 10); count += 1; } if (!count) return null; return total / count; } function buildWineProfileDimensions(wine, cardComment) { var text = wineProfileText(wine, cardComment).toLowerCase(); var vintage = wine.vintage || extractVintageYear(wine.title || ""); var vintageNum = vintage ? parseInt(String(vintage), 10) : null; var nowYear = new Date().getFullYear(); var vintageScore = 3.1; if (vintageNum && vintageNum >= 1950 && vintageNum <= nowYear) { var age = nowYear - vintageNum; if (age <= 3) vintageScore = 3.4; else if (age <= 8) vintageScore = 3.8; else if (age <= 18) vintageScore = 4.2; else if (age <= 30) vintageScore = 4.0; else vintageScore = 3.6; } var criticAvg = criticScoreAverage(text); var rankingScore = criticAvg ? clampProfileScore(1.8 + (criticAvg - 80) * 0.08) : 3.0; return [ { key: "body", label: "Body", value: keywordScore( text, [ /full[\s-]?bod/i, /rich/i, /bold/i, /concentrated/i, /dense/i, /heavy/i, /酒體|醇厚|豐滿|濃郁/, ], /light|delicate|elegant|thin|crisp|轻盈|輕盈|清爽/.test(text) ? 2.8 : 3.35, 0.2 ), }, { key: "acidity", label: "Acidity", value: keywordScore( text, [ /acid/i, /crisp/i, /fresh/i, /bright/i, /zesty/i, /vibrant/i, /酸度|清脆|爽脆|明亮/, ], 3.0, 0.24 ), }, { key: "aroma", label: "Aroma", value: keywordScore( text, [ /aroma/i, /nose/i, /bouquet/i, /berry|cherry|plum|citrus|floral|spice|oak|vanilla|mineral/i, /香氣|芳香|果香|花香|礦物/, ], 3.15, 0.21 ), }, { key: "aging", label: "Cellaring", value: keywordScore( text, [ /age/i, /cellar/i, /cellaring/i, /tannin|structure|grip|backbone/i, /long[\s-]?term/i, /陳年|適飲|潛力|結構|單寧/, ], vintageScore - 0.15, 0.2 ), }, { key: "finish", label: "Finish", value: keywordScore( text, [ /finish/i, /aftertaste/i, /persistent/i, /lingering/i, /length/i, /long/i, /餘韻|尾韻|回甘/, ], 3.05, 0.22 ), }, ].map(function (dim) { if (dim.key === "aging" && criticAvg) { dim.value = clampProfileScore((dim.value + rankingScore) / 2); } return dim; }); } var PROFILE_BAR_BASELINE = 3.0; var PROFILE_BAR_CONTRAST = 1.5; function profileBarWidth(value) { var score = clampProfileScore(value); var amplified = clampProfileScore( PROFILE_BAR_BASELINE + (score - PROFILE_BAR_BASELINE) * PROFILE_BAR_CONTRAST ); return Math.max(6, Math.min(100, Math.round(((amplified - 1.8) / 3.1) * 100))); } function buildWineProfileBarsHtml(profile) { var parts = ['"); return parts.join(""); } function buildWineProfileHtml(wine, cardComment) { var profile = resolveWineProfile(wine, cardComment); var showRadar = shouldShowTastingRadar(wine, profile); var critics = String(wine.critic_scores || "").trim(); if (!critics && wine.title) critics = criticScoresFromTitle(wine.title); var html = '
'; if (showRadar) { html += '

Tasting profile · 品飲印象

'; html += '
' + buildWineProfileBarsHtml(profile) + "
"; } html += '
'; html += 'Wine Critics'; if (critics) { html += '' + escapeHtml(critics) + ""; } else { html += 'N/A'; } html += "
"; return html; } function sanitizeCardReason(text) { var cleaned = String(text || "").trim(); if (!cleaned) return ""; if (isBudgetOnlyComment(cleaned)) return ""; var genericPrefixes = [ /^符合你今次搜尋的風格與條件[。.\s]*/u, /^紅酒風格符合你今次搜尋[。.\s]*/u, /^白酒風格符合你今次搜尋[。.\s]*/u, /^Matches the style and criteria for your search[.:\s]*/i, /^Red wine style matches what you asked for[.:\s]*/i, /^White wine style matches what you asked for[.:\s]*/i, /^At HK\$[\d,]+,?\s*within your budget and fits your search[.:\s]*/i, /^within your budget and fits your search[.:\s]*/i, /^HK\$[\d,]+[,,]?\s*在你預算之內[,,]?\s*風格貼合今次搜尋[。.\s]*/u, ]; for (var i = 0; i < genericPrefixes.length; i++) { cleaned = cleaned.replace(genericPrefixes[i], ""); } cleaned = cleaned.trim(); if (isBudgetOnlyComment(cleaned)) return ""; return cleaned; } function buildWineMetaRowHtml(wine, price) { if (!price) return ""; return '

' + price + "

"; } function buildWineGridHtml(wines, wineComments) { if (!wines || !wines.length) return ""; wineComments = wineComments || {}; var html = '

From our cellar · 酒窖精選

'; wines.forEach(function (wine) { var title = escapeHtml(wine.title || "Wine"); var url = wineProductUrl(wine); var price = formatPrice(wine.price != null ? wine.price : wine.price_hkd); var image = wine.image || ""; var variantId = wineVariantId(wine); var commentText = cardCommentPreviewFull(wine, wineComments[wine.title]); var preview = commentText.preview; var full = commentText.full; html += '
'; if (image) { html += '' + title + ''; } else { html += ''; } html += '

' + title + "

"; html += buildWineMetaRowHtml(wine, price); html += '

Comments:

'; if (shouldUseReadMoreCard(preview, full)) { html += buildReadMoreHtml(preview, "sens-ai-read-more--card", full); } else { html += '

' + escapeHtml(preview) + "

"; } html += buildWineProfileHtml(wine, full || preview); html += '
'; if (variantId) { html += ''; } else { html += ''; } html += 'View
'; }); html += "
"; return html; } function removeStaleWineCards() { messagesEl.querySelectorAll(".sens-ai-msg__wines").forEach(function (el) { var msg = el.closest(".sens-ai-msg"); if (msg) msg.classList.remove("sens-ai-msg--with-wines"); el.remove(); }); } function appendBotReply(text, wines) { removeStaleWineCards(); var prepared = prepareReplyAndWines(text, wines); var cardWines = prepared.cardWines; var wrap = document.createElement("div"); wrap.className = "sens-ai-msg sens-ai-msg--bot"; if (cardWines && cardWines.length) wrap.className += " sens-ai-msg--with-wines"; wrap.appendChild(createButlerSender()); var stack = document.createElement("div"); stack.className = "sens-ai-msg__stack"; var bubble = document.createElement("div"); bubble.className = "sens-ai-msg__bubble"; bubble.innerHTML = formatReplyWithReadMore(prepared.bubbleText); stack.appendChild(bubble); if (cardWines && cardWines.length) { stack.insertAdjacentHTML("beforeend", buildWineGridHtml(cardWines, prepared.wineComments)); } wrap.appendChild(stack); messagesEl.appendChild(wrap); scrollMessages(); saveChatSession(); return wrap; } function appendMessage(role, text, extraClass) { var wrap = document.createElement("div"); wrap.className = "sens-ai-msg sens-ai-msg--" + (role === "user" ? "user" : "bot"); if (extraClass) wrap.className += " " + extraClass; if (role !== "user") { wrap.appendChild(createButlerSender()); } var bubble = document.createElement("div"); bubble.className = "sens-ai-msg__bubble"; if (role === "user") bubble.textContent = text; else if (extraClass === "sens-ai-msg--loading") bubble.innerHTML = formatReply(text); else bubble.innerHTML = formatReplyWithReadMore(text); wrap.appendChild(bubble); messagesEl.appendChild(wrap); scrollMessages(); if (!extraClass || extraClass !== "sens-ai-msg--loading") { saveChatSession(); } return wrap; } function showToast(message) { toastEl.textContent = message; toastEl.hidden = false; toastEl.classList.add("is-visible"); window.clearTimeout(showToast._timer); showToast._timer = window.setTimeout(function () { toastEl.classList.remove("is-visible"); window.setTimeout(function () { toastEl.hidden = true; }, 260); }, 2600); } function startNewChat() { if (busy) { showToast("Please wait for the current reply…"); return; } history = []; clearChatSession(); messagesEl.innerHTML = ""; var welcome = document.createElement("div"); welcome.className = "sens-ai-msg sens-ai-msg--bot"; welcome.appendChild(createButlerSender()); var stack = document.createElement("div"); stack.className = "sens-ai-msg__stack"; var bubble = document.createElement("div"); bubble.className = "sens-ai-msg__bubble"; bubble.textContent = WELCOME_MESSAGE; stack.appendChild(bubble); welcome.appendChild(stack); messagesEl.appendChild(welcome); inputEl.value = ""; resetInputHeight(); if (!isMinimized) inputEl.focus(); showToast("New chat started"); } messagesEl.addEventListener("click", function (event) { var readMoreBtn = event.target.closest(".sens-ai-read-more__btn"); if (readMoreBtn) { event.preventDefault(); toggleReadMore(readMoreBtn); return; } var btn = event.target.closest("[data-variant-id]"); if (!btn || btn.disabled) return; var variantId = btn.getAttribute("data-variant-id"); btn.disabled = true; btn.textContent = "Adding…"; fetch("/cart/add.js", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, credentials: "same-origin", body: JSON.stringify({ items: [{ id: Number(variantId), quantity: 1 }] }) }) .then(function (res) { if (!res.ok) throw new Error("cart"); return res.json(); }) .then(function () { btn.textContent = "Added ✓"; showToast("Added to cart"); document.dispatchEvent(new CustomEvent("cart:refresh")); }) .catch(function () { btn.disabled = false; btn.textContent = "Add"; showToast("Could not add to cart."); }); }); formEl.addEventListener("submit", function (event) { event.preventDefault(); if (busy) return; var message = (inputEl.value || "").trim(); if (message.length < 2) return; if (isMinimized) openPanel(); appendMessage("user", message); inputEl.value = ""; resetInputHeight(); busy = true; sendEl.disabled = true; var loadingNode = appendMessage("bot", LOADING_MESSAGE, "sens-ai-msg--loading"); fetch(CONFIG.apiUrl, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, credentials: "same-origin", body: JSON.stringify({ message: message, history: history.slice(-8) }) }) .then(function (res) { return res.json().then(function (data) { if (!res.ok) { var detail = data && data.detail ? data.detail : "Request failed"; throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail)); } return data; }); }) .then(function (data) { loadingNode.remove(); var reply = data.reply || "No reply received."; appendBotReply(reply, data.wines || []); history.push({ role: "user", text: message }); history.push({ role: "assistant", text: reply }); saveChatSession(); }) .catch(function (err) { loadingNode.remove(); appendMessage("bot", "Sorry, something went wrong. Please try again."); showToast(err && err.message ? String(err.message).slice(0, 100) : "Network error"); }) .finally(function () { busy = false; sendEl.disabled = false; inputEl.focus(); }); }); inputEl.addEventListener("keydown", function (event) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); formEl.requestSubmit(); } }); inputEl.addEventListener("input", growInputHeight); window.addEventListener("pagehide", saveChatSession); document.addEventListener("click", handleWineProductNavigation, true); syncMobileMode(); syncMaximizeButton(); initPanelPosition(); })();