1/* ============================================ 2 chef-guides.js â 大å¨ç¹é¥ªæå 3 è谱页ç¹å»å¤§å¨å¤´åæ¶å±ç¤ºï¼ 4 1. éç¨æå·§ï¼æèè°±åæ³+æ ¸å¿é£æå¹é ï¼ 5 2. æ¬è详ç»åæ³ï¼data/guides/recipes/{èå}.mdï¼ 6 ============================================ */ 7 8const ChefGuides = { 9 _generalMd: null, 10 _sections: null, 11 _index: null, 12 _recipeCache: {}, 13 _annotate: false, 14 _termRegex: null, 15 16 // å ³é®ç¹é¥ªæ¯è¯ï¼è¯¦ç»åæ³ä¸ä¼è¢«"ç»çº¿"æ æ³¨ï¼åæç§ä¹¦ç¬è®°ï¼ 17 _getTermRegex() { 18 if (this._termRegex) return this._termRegex; 19 const terms = [ 20 "çé åæ²¹", "ä¸å°ç«", "ä¸å¤§ç«", "çç³è²", 21 "ç¯æ°´", "è å¶", "å»è ¥", "å»è»", "飿°´", 22 "大ç«", "å°ç«", "ä¸ç«", "ç«å", "油温", 23 "å¾è¡", "è¡æ±", "å¤ç¸", "䏿µ", "æç³", 24 "æ¶æ±", "çé¦", "å®å", "ç¿»é¢", 25 "æ»å«©", "é ¥ç", "ç¦é»", "ç¦è", 26 ]; 27 terms.sort((a, b) => b.length - a.length); 28 this._termRegex = new RegExp("(" + terms.join("|") + ")", "g"); 29 return this._termRegex; 30 }, 31 32 // å 载并ç¼åéç¨æå·§ markdown 33 async _loadGeneral() { 34 if (this._generalMd) return this._generalMd; 35 const resp = await fetch("data/guides/general-techniques.md"); 36 if (!resp.ok) throw new Error("éç¨æå·§å 载失败"); 37 this._generalMd = await resp.text(); 38 return this._generalMd; 39 }, 40 41 // å è½½èè°± id â æåæä»¶æ å° 42 async _loadIndex() { 43 if (this._index) return this._index; 44 const resp = await fetch("data/guides/index.json"); 45 if (!resp.ok) throw new Error("æåç´¢å¼å 载失败"); 46 this._index = await resp.json(); 47 return this._index; 48 }, 49 50 // æéç¨æå·§ markdown æäºçº§æ é¢ååä¸ºç« è 51 _parseSections() { 52 if (this._sections) return this._sections; 53 const md = this._generalMd || ""; 54 const sections = []; 55 const parts = md.split(/^## /m); 56 for (let i = 1; i < parts.length; i++) { 57 const nl = parts[i].indexOf("\n"); 58 const title = parts[i].slice(0, nl).trim(); 59 const body = parts[i].slice(nl + 1).trim(); 60 sections.push({ title, body }); 61 } 62 this._sections = sections; 63 return sections; 64 }, 65 66 // æ ¹æ®èè°±åæ³ + 飿å¹é éç¨ç« è 67 _matchSections(recipe) { 68 const sections = this._parseSections(); 69 const matched = []; 70 71 // 1. ææ³ç« èï¼æ methodLabel 精确å¹é 72 const methodLabel = recipe.methodLabel || ""; 73 if (methodLabel) { 74 const sec = sections.find((s) => s.title === `ææ³ï¼${methodLabel}`); 75 if (sec) matched.push(sec); 76 } 77 78 // 2. 飿å¤çç« èï¼ææ ¸å¿é£æå ³é®è¯å¹é 79 const text = [ 80 recipe.title || "", 81 ...(recipe.coreIngredients || []), 82 ...(recipe.requiredIngredients || []), 83 ].join(" "); 84 85 const ingredientRules = [ 86 { section: "飿å¤çï¼çªè", re: /çªè|çªéª¨|çªæ|çªéè|çªè|çªè¹|çªäºè±|çªè ©|çªé¢|çªè|çªè³|çªè |çªè|çªè °|çªå¿|çªèº|çªæ¿æ²¹|çªç½æ²¹|äºè±|æéª¨|éè|èæ«|èé¦ |èå|è¹é«|è¹è±|广 ¹|ç«è ¿|è è |åé¤è|è è|å¸è/ }, 87 { section: "飿å¤çï¼çè", re: /çè|çè ©|è¥ç|çæ|çè ±|çéè|çç¾å¶|çè|çç|çå°¾|ç骨|çè|çæ/ }, 88 { section: "飿å¤çï¼é¸¡è", re: /鸡(?!è|ç²¾|汤|æ²¹)|é¸¡ç¿ |é¸¡è ¿|鸡è¸|鸡çª|鸡è|鸡è|鸡å¿|鸡æ¶|å鸡|ä¸é»é¸¡|ä¹é¸¡|ç«¥å鸡|èæ¯é¸¡/ }, 89 { section: "飿å¤çï¼é±¼è¾æ°´äº§", re: /é±¼(?!é²)|è¾|è¹|è´|è¤|è|鱿|æµ·å|é³|é²|æ¡é±¼|鲫|鲤|é³|ç¿å´|çè|å°é¾è¾|ç°èº|墨鱼|ç« é±¼|鱿鱼|带鱼|é»è±é±¼|å¤å®é±¼|ç³æ|é²é±¼|䏿鱼|éæªé±¼|é³é±¼|æ³¥é³ |é»é³|ç²é±¼|é¾|çè|ç°é¸¡/ }, 90 { section: "飿å¤çï¼è¬è", re: /éè|ç½è|è è|è¹è|éè|çè|油麦è|空å¿è|èè|è¥è|èå¿|è¥¿å °è±|è±è|å·å¿è|ç´«çè|å¨å¨è|è¼è¿|é¦è|馿¤¿|è½è|è±è½|è±è§|åå£è±|è±è±|è·å °è±|æ¯è±|èè±|è±è±|é»ç|å¬ç|åç|ä¸ç|è¦ç|èå|çªè|西红æ¿|åè±|å±±è¯|è头|红è¯|ç´«è¯|è²è|èå|è¡èå|ç½èå|éèå|竹ç¬|è¦ç¬|è´ç¬|èç½|çç±³|鿤|红æ¤|彩æ¤|å°æ¤|线æ¤|èºä¸æ¤|æ´è±|大è±|å°è±|èè|èè|éé»|è|è|é¦è|å¹³è|ééè|æé²è|å£è|æ¨è³|é¶è³|海带|ç´«è|è±è |è±å¹²|è±ç®|è 竹|åå¼ |ç´ é¸¡/ }, 91 { section: "飿å¤çï¼ç±³é¥é¢é£", re: /ç±³é¥|çé¥|ç©é¥|çé¥|ç ²ä»é¥|ç²¥|颿¡|æé¢|ååé¢|ææé¢|æé¢|æé¢|ç±³ç²|æ²³ç²|ç²ä¸|ç²æ¡|åç²|åç®|米线|å¹´ç³|é¦å¤´|å å|饺å|é¦é¥¨|汤å|ç§å|è±å·|ç饼|ç 饼|é¦|åå¸|é¢å |ç麦|糯米|ç²½å|汤å|麻å¢|æ²¹æ¡|ç§é¥¼|é ç/ }, 92 ]; 93 for (const rule of ingredientRules) { 94 if (rule.re.test(text)) { 95 const sec = sections.find((s) => s.title === rule.section); 96 if (sec) matched.push(sec); 97 } 98 } 99 100 // 3. 油温ä¸å¾è¡ï¼ç/ç¸/ç§/ç ç±»èè°±éå 101 if (["ç", "ç¸", "ç§", "ç "].includes(methodLabel)) { 102 const sec = sections.find((s) => s.title.startsWith("éç¨ï¼æ²¹æ¸©ä¸å¾è¡")); 103 if (sec) matched.push(sec); 104 } 105 106 return matched; 107 }, 108 109 // è½»é markdown â HTMLï¼æ é¢/å ç²/å表/è¡¨æ ¼/å¼ç¨/åé线/段è½ï¼ 110 renderMarkdown(md) { 111 const esc = (s) => 112 s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 113 const inline = (s) => { 114 let out = esc(s) 115 .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") 116 .replace(/`([^`]+)`/g, "<code>$1</code>"); 117 if (this._annotate) { 118 out = out.replace(this._getTermRegex(), '<u class="cg-mark">$1</u>'); 119 } 120 return out; 121 }; 122 123 const lines = md.split("\n"); 124 let html = ""; 125 let i = 0; 126 while (i < lines.length) { 127 const line = lines[i]; 128 const trimmed = line.trim(); 129 130 // ç©ºè¡ 131 if (!trimmed) { i++; continue; } 132 133 // åé线 134 if (/^-{3,}$/.test(trimmed)) { html += "<hr>"; i++; continue; } 135 136 // æ é¢ 137 const h = trimmed.match(/^(#{1,4})\s+(.*)$/); 138 if (h) { 139 const level = h[1].length + 2; // # â h3, ## â h4, ### â h5 140 html += `<h${level}>${inline(h[2])}</h${level}>`; 141 i++; continue; 142 } 143 144 // è¡¨æ ¼ 145 if (trimmed.startsWith("|")) { 146 const rows = []; 147 while (i < lines.length && lines[i].trim().startsWith("|")) { 148 rows.push(lines[i].trim()); 149 i++; 150 } 151 const cells = (r) => 152 r.replace(/^\||\|$/g, "").split("|").map((c) => c.trim()); 153 if (rows.length >= 2) { 154 html += "<table><thead><tr>"; 155 for (const c of cells(rows[0])) html += `<th>${inline(c)}</th>`; 156 html += "</tr></thead><tbody>"; 157 for (let r = 2; r < rows.length; r++) { // è·³è¿åéè¡ 158 html += "<tr>"; 159 for (const c of cells(rows[r])) html += `<td>${inline(c)}</td>`; 160 html += "</tr>"; 161 } 162 html += "</tbody></table>"; 163 } 164 continue; 165 } 166 167 // æ åºå表 168 if (/^[-*]\s+/.test(trimmed)) { 169 html += "<ul>"; 170 while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) { 171 html += `<li>${inline(lines[i].trim().replace(/^[-*]\s+/, ""))}</li>`; 172 i++; 173 } 174 html += "</ul>"; 175 continue; 176 } 177 178 // æåºåè¡¨ï¼æ¯æåµå¥æ åºå表ï¼ä¿æç¼å·è¿ç»ï¼ 179 if (/^\d+[.ã]\s*/.test(trimmed)) { 180 html += "<ol>"; 181 while (i < lines.length) { 182 const rawLine = lines[i]; 183 const t = rawLine.trim(); 184 // æåºæ¡ç® 185 if (/^\d+[.ã]\s*/.test(t)) { 186 html += `<li>${inline(t.replace(/^\d+[.ã]\s*/, ""))}`; 187 // æ¶éåé¢ç缩è¿é¡¹ï¼ååè¡¨ï¼ 188 let j = i + 1; 189 let subItems = []; 190 while (j < lines.length) { 191 const nextRaw = lines[j]; 192 const nextTrimmed = nextRaw.trim(); 193 if (!nextTrimmed) { j++; continue; } 194 // è¡é¦æç©ºç½ + 以 -/* å¼å¤´ â åå表项 195 if (/^\s+[-*]\s+/.test(nextRaw)) { 196 subItems.push(nextTrimmed.replace(/^[-*]\s+/, "")); 197 j++; 198 continue; 199 } 200 break; 201 } 202 if (subItems.length > 0) { 203 html += "<ul>" + subItems.map(s => `<li>${inline(s)}</li>`).join("") + "</ul>"; 204 } 205 html += "</li>"; 206 i = j; 207 continue; 208 } 209 // 空è¡ï¼è·³è¿ï¼ä½ä¸ç»æå表 210 if (!t) { 211 i++; 212 continue; 213 } 214 // é缩è¿çé¡¶å± - å表项ï¼ä¸åºåºç°å¨æåºå表ä¸é´ï¼ç»æï¼ 215 if (/^[-*]\s+/.test(t) && !/^\s+/.test(rawLine)) { 216 break; 217 } 218 break; 219 } 220 html += "</ol>"; 221 continue; 222 } 223 224 // å¼ç¨ 225 if (trimmed.startsWith(">")) { 226 html += `<blockquote>${inline(trimmed.replace(/^>\s*/, ""))}</blockquote>`; 227 i++; continue; 228 } 229 230 // æ®éæ®µè½ 231 html += `<p>${inline(trimmed)}</p>`; 232 i++; 233 } 234 return html; 235 }, 236 237 /** 238 * å°AIçæç3+2+1æ ¼å¼å¤§å¨æ»ç»æ¸²æä¸ºç¾åå¡çæ ·å¼ 239 * ä¸ä¸ªåºåï¼ð¡è¦ç¹ï¼ç»¿ï¼/â ï¸åç¹ï¼æ©ï¼/â¨å级ï¼ç´«ï¼ 240 */ 241 renderAISummaryCards(md) { 242 if (!md || !md.trim()) return ''; 243 244 const esc = (s) => 245 s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 246 const inlineBold = (s) => esc(s).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"); 247 248 // è§£æmarkdownä¸ºç»æååºå 249 const sections = []; 250 const lines = md.split('\n').map(l => l.trim()).filter(l => l);
251 let currentSection = null; 252 253 for (const line of lines) { 254 const h2Match = line.match(/^##\s+(.+)$/); 255 if (h2Match) { 256 const title = h2Match[1].trim(); 257 let type = 'key', icon = 'ð¡', color = '#3cb371', lightColor = '#e8f5e9'; 258 if (title.includes('失败') || title.includes('å')) { 259 type = 'pitfall'; icon = 'â ï¸'; color = '#e67e22'; lightColor = '#fff3e0'; 260 } else if (title.includes('å级') || title.includes('è¿é¶')) { 261 type = 'upgrade'; icon = 'â¨'; color = '#9b59b6'; lightColor = '#f3e5f5'; 262 } 263 currentSection = { title, type, icon, color, lightColor, items: [] }; 264 sections.push(currentSection); 265 continue; 266 } 267 const numMatch = line.match(/^(\d+)[.ã)ï¼]\s*(.+)$/); 268 if (numMatch && currentSection) { 269 currentSection.items.push(inlineBold(numMatch[2].trim())); 270 continue; 271 } 272 const bulletMatch = line.match(/^[-*]\s+(.+)$/); 273 if (bulletMatch && currentSection) { 274 currentSection.items.push(inlineBold(bulletMatch[1].trim())); 275 } 276 } 277 278 if (sections.length === 0) { 279 // å¦æè§£æå¤±è´¥ï¼éåæ®émarkdown渲æ 280 return `<div class="cg-ai-summary-content">${this.renderMarkdown(md)}</div>`; 281 } 282 283 // ç»è®¡æ°é 284 const keyCount = sections.find(s => s.type === 'key')?.items.length || 0; 285 const pitCount = sections.find(s => s.type === 'pitfall')?.items.length || 0; 286 const upCount = sections.find(s => s.type === 'upgrade')?.items.length || 0; 287 288 let html = ''; 289 290 // 大卿 ç¾ 291 html += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px;padding-left:4px;"> 292 <span style="font-size:16px;">ð¨âð³</span> 293 <span style="font-size:15px;font-weight:600;color:#3cb371;">é»è®¤å¤§å¨æ»ç»</span> 294 <span style="margin-left:4px;padding:2px 6px;border-radius:4px;background:rgba(60,179,113,0.15);color:#3cb371;font-size:11px;font-weight:500;">AIçæ</span> 295 </div>`; 296 297 // æ¸²ææ¯ä¸ªåºå 298 for (const sec of sections) { 299 html += `<div style="margin-bottom:16px;"> 300 <div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;padding-left:4px;"> 301 <div style="width:28px;height:28px;border-radius:50%;background:${sec.lightColor};display:flex;align-items:center;justify-content:center;font-size:14px;">${sec.icon}</div> 302 <div style="font-size:16px;font-weight:700;color:${sec.color};">${sec.title}</div> 303 </div> 304 <div style="display:flex;flex-direction:column;gap:8px;">`; 305 306 sec.items.forEach((item, idx) => { 307 html += `<div style="display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:12px;background:${sec.lightColor}80;"> 308 <div style="flex-shrink:0;width:24px;height:24px;border-radius:50%;background:${sec.color};color:white;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;">${idx + 1}</div> 309 <div style="font-size:14px;line-height:1.6;color:#333;flex:1;">${item}</div> 310 </div>`; 311 }); 312 313 html += `</div></div>`; 314 } 315 316 return html; 317 }, 318 319 // å è½½å个èè°±çè¯¦ç»æåï¼å¸¦ç¼åï¼ 320 async _loadRecipeGuide(file) { 321 if (this._recipeCache[file]) return this._recipeCache[file]; 322 const resp = await fetch(`data/guides/recipes/${encodeURIComponent(file)}`); 323 if (!resp.ok) return null; 324 const md = await resp.text(); 325 this._recipeCache[file] = md; 326 return md; 327 }, 328 329 // æå»ºé¢æ¿ HTMLï¼éç¨ææ³ + èè°±ç¬è®°ï¼åæ¥éª¤ + markdown æ æ³¨ï¼ 330 async buildGuideHTML(recipeTitle) { 331 try { 332 await Promise.all([this._loadGeneral(), this._loadIndex()]); 333 334 const recipe = 335 (typeof allRecipes !== "undefined" && 336 allRecipes.find((r) => r.title === recipeTitle)) || 337 { title: recipeTitle }; 338 339 // è·ååèè°±æ¥éª¤ï¼ä¼å allRecipesï¼å ¶æ¬¡ DOMï¼ 340 let steps = recipe.steps || []; 341 if (steps.length === 0 && window.ChefAgent) { 342 const domRecipe = ChefAgent.readCurrentRecipe(); 343 if (domRecipe) steps = domRecipe.steps || []; 344 } 345 346 let html = ""; 347 348 // å¤´é¨ 349 html += `<div class="cg-notebook-header">
350 <span class="cg-notebook-icon">ð</span> 351 <div> 352 <div class="cg-notebook-title">${recipe.title} · åèç¬è®°</div> 353 <div class="cg-notebook-sub">åèè°±æ¥éª¤ + 大卿 注ç¬è®°</div> 354 </div> 355 </div>`; 356 357 // åèè®°å¿ 358 if (window.ChefMemory) { 359 const cooked = ChefMemory.getCookedRecipes(); 360 const mem = cooked.find((r) => r.title === recipe.title); 361 if (mem) { 362 const date = new Date(mem.lastCooked).toLocaleDateString(); 363 html += `<div class="cg-memory-note"> 364 <span class="cg-memory-icon">ð§ </span> 365 <span>ä½ åè¿è¿éè ${mem.count} 次ï¼ä¸æ¬¡ ${date}ã</span> 366 </div>`; 367 } 368 } 369 370 // éç¨ææ³ï¼3 ç»´åº¦æ¹æ³¨ï¼ 371 const sections = this._matchSections(recipe); 372 const ingredientSecs = sections.filter((s) => s.title.startsWith("飿å¤ç")); 373 const techniqueSecs = sections.filter((s) => s.title.startsWith("ææ³")); 374 const generalSecs = sections.filter((s) => s.title.startsWith("éç¨")); 375 this._annotate = false; 376 if (ingredientSecs.length || techniqueSecs.length || generalSecs.length) { 377 html += `<div class="cg-annotations"> 378 <div class="cg-annotations-label">ð éç¨ææ³</div>`; 379 for (const sec of ingredientSecs) 380 html += this._renderAnnotationCard("ð¥©", sec.title.replace("飿å¤çï¼", ""), sec.body, "ingredient"); 381 for (const sec of techniqueSecs) 382 html += this._renderAnnotationCard("ð¥", sec.title.replace("ææ³ï¼", ""), sec.body, "technique"); 383 for (const sec of generalSecs) 384 html += this._renderAnnotationCard("ð§", sec.title.replace("éç¨ï¼", ""), sec.body, "general"); 385 html += `</div>`; 386 } 387 388 // èè°±ç¬è®°ï¼åèè°±æ¥éª¤ï¼ä¹¦æ¬ï¼+ markdown æ æ³¨ï¼ç¬è®°ï¼ 389 const entry = this._index[recipe.id] || 390 Object.values(this._index).find((e) => e.title === recipeTitle); 391 if (entry && steps.length > 0) { 392 const md = await this._loadRecipeGuide(entry.file); 393 if (md) { 394 const parsed = this._parseRecipeMarkdown(md); 395 const pool = [ 396 ...parsed.prep.map((t) => ({ text: t, section: "飿åå¤" })), 397 ...parsed.steps.map((t) => ({ text: t, section: "详ç»åæ³" })), 398 ...parsed.tips.map((t) => ({ text: t, section: "å ³é®æå·§" })), 399 ...parsed.problems.map((t) => ({ text: t, section: "常è§é®é¢" })), 400 ]; 401 402 // å ¨å±å¹é ï¼æå ³é®è¯éå度è¯åï¼è´ªå¿åé 403 const allPairs = []; 404 for (let si = 0; si < steps.length; si++) { 405 const stepKw = new Set(this._extractKeywords(steps[si])); 406 if (stepKw.size === 0) continue; 407 for (let pi = 0; pi < pool.length; pi++) { 408 const noteKw = this._extractKeywords(pool[pi].text); 409 const overlap = noteKw.filter((k) => stepKw.has(k)); 410 if (overlap.length > 0) 411 allPairs.push({ si, pi, score: overlap.length }); 412 } 413 } 414 allPairs.sort((a, b) => b.score - a.score); 415 416 const stepNotes = steps.map(() => []); 417 const usedIndices = new Set(); 418 const maxPerStep = 3; 419 for (const pair of allPairs) { 420 if (usedIndices.has(pair.pi)) continue; 421 if (stepNotes[pair.si].length >= maxPerStep) continue; 422 stepNotes[pair.si].push(pool[pair.pi]); 423 usedIndices.add(pair.pi); 424 } 425 426 // 渲æï¼åæ¥éª¤ï¼å¸¦çº¢è²ä¸å线éç¹ï¼+ 绿è²å¼çº¿æ¹æ³¨ 427 html += `<div class="cg-recipe-notes"> 428 <div class="cg-recipe-notes-label">ç¹é¥ªæ¥éª¤</div>`; 429 for (let si = 0; si < steps.length; si++) { 430 const notes = stepNotes[si]; 431 // 为æ¥éª¤ææ¬ä¸çéç¹è¯å 红è²ä¸å线 432 const highlightedStep = this._highlightStepText(steps[si], notes); 433 html += `<div class="cg-step-block"> 434 <div class="cg-step-text">
435 <span class="cg-step-num">${si + 1}</span> 436 <span class="cg-step-content">${highlightedStep}</span> 437 </div>`; 438 if (notes.length > 0) { 439 html += `<div class="cg-notes-wrapper">`; 440 for (const note of notes) { 441 html += `<div class="cg-annotation-bubble"> 442 <span class="cg-anno-dot"></span> 443 <span class="cg-step-note-tag">${note.section}</span> 444 <span class="cg-anno-text">${this.renderMarkdownInline(note.text)}</span> 445 </div>`; 446 } 447 html += `</div>`; 448 } 449 html += `</div>`; 450 } 451 html += `</div>`; 452 453 // è¡¥å ç¬è®° 454 const remaining = pool.filter((_, idx) => !usedIndices.has(idx)).slice(0, 8); 455 if (remaining.length > 0) { 456 html += `<div class="cg-supplementary"> 457 <div class="cg-supplementary-label">éå å 容</div>`; 458 for (const note of remaining) { 459 html += `<div class="cg-supplementary-note"> 460 <span class="cg-step-note-tag">${note.section}</span> 461 <span>${this.renderMarkdownInline(note.text)}</span> 462 </div>`; 463 } 464 html += `</div>`; 465 } 466 // Bç«è§é¢é¾æ¥ 467 const biliKw = recipeTitle; 468 const safeBiliKw = JSON.stringify(biliKw); 469 html += `<a class="cg-bili-link" href="javascript:void(0)" onclick='openBiliSearch(${safeBiliKw});return false;'> 470 <svg class="cg-bili-icon" viewBox="0 0 48 48" width="20" height="20" xmlns="http://www.w3.org/2000/svg"> 471 <path d="M14 11L9 4M34 11l5-7" stroke="#00AEEC" stroke-width="3" stroke-linecap="round" fill="none"/> 472 <rect x="5" y="11" width="38" height="29" rx="8" fill="#00AEEC"/> 473 <rect x="10" y="17" width="28" height="17" rx="4" fill="#fff"/> 474 <ellipse cx="19" cy="25.5" rx="3" ry="3.5" fill="#00AEEC"/> 475 <ellipse cx="29" cy="25.5" rx="3" ry="3.5" fill="#00AEEC"/> 476 </svg> 477 <span>å¨Bç«ç${recipeTitle}è§é¢æå¦</span> 478 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7"/><path d="M7 7h10v10"/></svg> 479 </a>`; 480 } 481 } 482 483 return html || `<div class="chef-guide-empty">ææ è¿éèçç¹é¥ªæå</div>`; 484 } catch (e) { 485 return `<div class="chef-guide-empty">ç¹é¥ªæåå 载失败ï¼${e.message}</div>`; 486 } 487 }, 488 489 // 渲æåå¼ æ¹æ³¨å¡çï¼å·¦ä¾§è²æ¡ + 徿 + æ é¢ + æ£æï¼ 490 _renderAnnotationCard(icon, name, body, type) { 491 return `<div class="cg-anno-card ${type}"> 492 <div class="cg-anno-card-head"><span>${icon}</span><span>${name}</span></div> 493 <div class="cg-anno-card-body">${this.renderMarkdown(body)}</div> 494 </div>`; 495 }, 496 497 // HTML è½¬ä¹ 498 _escapeHtml(s) { 499 return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 500 }, 501 502 // æ¾æ¥éª¤ææ¬ä¸çéç¹è¯ï¼ä»ç¬è®°ä¸æ¾å¨æ¥éª¤éåºç°çæé¿ææä¹çè¯ 503 _findAnchorPhrase(stepText, noteText) { 504 const badTail = new Set([ 505 "æ","让","ç¨","å¨","æ","ä»","å","å°","ç»","对","为","ä¸","å","ç", 506 "ç","è¿","ç","äº","ä¸","ä¸","ä¹","å°±","æ","å¾","æ","å","å","è¿", 507 "å°","被","æ¯","æ","ç §","æ®","沿","顺","ç»","é ", 508 "è½»","è½»","大","å°","å¤","å°","好","å¿«","æ ¢","é«","ä½","é¿","ç","å","è", 509 "太","é","æª","å·²","æ£","å","æ°","ç¨","ç¥","äº","ç¹", 510 ]); 511 const badHead = new Set(["ç","äº","å¨","æ¯","å","ä¸","æ","è","ä½","å¦","以","为","对","ä»","å°","被","æ","å°","让","å","æ","ç»","è·","æ¯","æ","ç §"]); 512 // æç¬è®°ææ ç¹/ç©ºæ ¼ååï¼æåææä¸æçæ®µ 513 const segments = noteText.split(/[ï¼ãï¼ï¼ï¼ï¼ã\s\d\.a-zA-Z,;:!?""'()ï¼ï¼\[\]ãã%â~\-ââ¦Â·]+/).filter(Boolean); 514 const candidates = new Set(); 515 for (const seg of segments) { 516 const chinese = seg.replace(/[^\u4e00-\u9fa5]/g, ""); 517 if (chinese.length < 2) continue; 518 // å 2-5 åçå串ï¼é¿å è¿é¿æ æ³¨ï¼ 519 for (let len = Math.min(5, chinese.length); len >= 2; len--) { 520 for (let i = 0; i <= chinese.length - len; i++) { 521 const sub = chinese.slice(i, i + len); 522 if (!badTail.has(sub[sub.length - 1]) && !badHead.has(sub[0])) { 523 candidates.add(sub); 524 } 525 } 526 } 527 } 528 // æé¿åº¦éåºï¼æ¾ç¬¬ä¸ä¸ªå¨æ¥éª¤ä¸åºç°ç 529 const sorted = [...candidates].sort((a, b) => b.length - a.length); 530 for (const cand of sorted) { 531 if (stepText.includes(cand)) return cand; 532 } 533 return ""; 534 }, 535 536 // 卿¥éª¤ææ¬ä¸ç»éç¹è¯å 红è²ä¸å线ï¼åå¹¶éå /ç¸é»çéç¹ï¼ 537 _highlightStepText(stepText, notes) { 538 const anchors = []; 539 for (const note of notes) { 540 const anchor = this._findAnchorPhrase(stepText, note.text); 541 if (anchor && anchor.length >= 2) anchors.push(anchor); 542 } 543 if (anchors.length === 0) return this._escapeHtml(stepText); 544 // å¨åææ¬ä¸æ¾å°æ¯ä¸ªéç¹çä½ç½®ï¼æ¯ä¸ªéç¹åªåç¬¬ä¸æ¬¡åºç°ï¼ 545 const positions = []; 546 const used = new Set(); 547 for (const a of anchors) { 548 const idx = stepText.indexOf(a); 549 if (idx !== -1 && !used.has(a)) { 550 positions.push({ start: idx, end: idx + a.length }); 551 used.add(a); 552 } 553 } 554 if (positions.length === 0) return this._escapeHtml(stepText); 555 // æèµ·å§ä½ç½®æåºï¼åå¹¶éå /ç¸é»ä½ä¸è·¨æ ç¹çåºé´ 556 positions.sort((a, b) => a.start - b.start); 557 const merged = [positions[0]]; 558 const punctRE = /[ï¼ãï¼ï¼ï¼ï¼ã,;:!?""'()ï¼ï¼\[\]ãã\s]/; 559 for (let i = 1; i < positions.length; i++) { 560 const last = merged[merged.length - 1]; 561 const cur = positions[i]; 562 // æ£æ¥é´é䏿¯å¦ææ ç¹ 563 const gap = stepText.slice(last.end, cur.start);
564 if (cur.start <= last.end + 2 && !punctRE.test(gap)) { 565 last.end = Math.max(last.end, cur.end); 566 } else { 567 merged.push(cur); 568 } 569 } 570 // ä»åå¾ååçï¼è½¬ä¹åæ®µåæ¼æ¥ u æ ç¾ 571 let result = ""; 572 let cursor = stepText.length; 573 for (let i = merged.length - 1; i >= 0; i--) { 574 const { start, end } = merged[i]; 575 const after = stepText.slice(end, cursor); // éç¹åé¢çææ¬ 576 const marked = stepText.slice(start, end); // éç¹æ¬èº« 577 result = 578 '<u class="cg-underline">' + this._escapeHtml(marked) + "</u>" + 579 this._escapeHtml(after) + 580 result; 581 cursor = start; 582 } 583 result = this._escapeHtml(stepText.slice(0, cursor)) + result; 584 return result; 585 }, 586 587 // å è markdown 渲æï¼ç¨äºæ¹æ³¨æ°æ³¡å ï¼ä¸äº§çå级å ç´ ï¼ 588 renderMarkdownInline(text) { 589 const esc = (s) => 590 s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 591 let out = esc(text); 592 out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"); 593 out = out.replace(/`([^`]+)`/g, "<code>$1</code>"); 594 // æåå·åçå 容ç¥ååæ®µå¤ç 595 out = out.replace(/ï¼/g, "ï¼"); 596 return out; 597 }, 598 599 // è§£æèè°± markdownï¼æå为åç« èçç¬è®°åå 600 _parseRecipeMarkdown(md) { 601 const result = { prep: [], steps: [], tips: [], problems: [], variations: [] }; 602 const body = md.replace(/^#\s+.*\n/, ""); 603 const sections = body.split(/^## /m); 604 for (let i = 1; i < sections.length; i++) { 605 const nl = sections[i].indexOf("\n"); 606 const title = sections[i].slice(0, nl).trim(); 607 const content = sections[i].slice(nl + 1).trim(); 608 let bucket = null; 609 if (/飿|åå¤|å¤ç/.test(title)) bucket = result.prep; 610 else if (/详ç»åæ³|åæ³|æ¥éª¤/.test(title)) bucket = result.steps; 611 else if (/å ³é®æå·§|æå·§|è¦ç¹/.test(title)) bucket = result.tips; 612 else if (/常è§é®é¢|é®é¢|注æ/.test(title)) bucket = result.problems; 613 else if (/åååæ³|åå|延伸|æå±/.test(title)) bucket = result.variations; 614 if (!bucket) continue; 615 616 const lines = content.split("\n"); 617 let current = ""; 618 for (const line of lines) { 619 const trimmed = line.trim(); 620 if (!trimmed) { if (current) { bucket.push(current); current = ""; } continue; } 621 if (/^\*\*.+\*\*$/.test(trimmed)) { if (current) { bucket.push(current); current = ""; } continue; } 622 if (/^[-*]\s+/.test(trimmed) || /^\d+[.ã]\s*/.test(trimmed)) { 623 if (current) { bucket.push(current); } 624 current = trimmed.replace(/^[-*]\s+/, "").replace(/^\d+[.ã]\s*/, ""); 625 } else { 626 current = current ? current + " " + trimmed : trimmed; 627 } 628 } 629 if (current) { bucket.push(current); } 630 } 631 return result; 632 }, 633 634 _notesActive: false, 635 _originalStepHtml: null, 636 637 // å ±äº«é»è¾ï¼ä¸ºæ¥éª¤æ°ç»å¹é ç¬è®°ï¼è¿å stepNotes äºç»´æ°ç» 638 async _matchNotesToSteps(steps, recipe) { 639 await Promise.all([this._loadGeneral(), this._loadIndex()]); 640 const entry = this._index[recipe.id] || 641 Object.values(this._index).find((e) => e.title === recipe.title); 642 if (!entry) return steps.map(() => []); 643 const md = await this._loadRecipeGuide(entry.file); 644 if (!md) return steps.map(() => []); 645 const parsed = this._parseRecipeMarkdown(md); 646 const pool = [ 647 ...parsed.prep.map((t) => ({ text: t, section: "飿åå¤" })), 648 ...parsed.steps.map((t) => ({ text: t, section: "详ç»åæ³" })), 649 ...parsed.tips.map((t) => ({ text: t, section: "å ³é®æå·§" })), 650 ...parsed.problems.map((t) => ({ text: t, section: "常è§é®é¢" })), 651 ]; 652 const allPairs = []; 653 for (let si = 0; si < steps.length; si++) { 654 const stepKw = new Set(this._extractKeywords(steps[si])); 655 if (stepKw.size === 0) continue; 656 for (let pi = 0; pi < pool.length; pi++) { 657 const noteKw = this._extractKeywords(pool[pi].text); 658 const overlap = noteKw.filter((k) => stepKw.has(k)); 659 if (overlap.length > 0) 660 allPairs.push({ si, pi, score: overlap.length }); 661 } 662 } 663 allPairs.sort((a, b) => b.score - a.score); 664 const stepNotes = steps.map(() => []); 665 const usedIndices = new Set(); 666 const maxPerStep = 3; 667 for (const pair of allPairs) { 668 if (usedIndices.has(pair.pi)) continue; 669 if (stepNotes[pair.si].length >= maxPerStep) continue; 670 stepNotes[pair.si].push(pool[pair.pi]); 671 usedIndices.add(pair.pi); 672 } 673 return stepNotes; 674 }, 675 676 // æå»ºã大卿»ç»ã颿¿ HTMLï¼é»è®¤å¨å¸AIæ»ç» + èªå®ä¹å¨å¸æ»ç»ï¼ 677 async buildSummaryHTML(recipeTitle) { 678 // æ£æ¥æ¯å¦æå¯ç¨çå¨å¸ 679 const enabledChefs = ChefManager.getEnabled(); 680 if (enabledChefs.length === 0) { 681 return `<div class="chef-guide-empty">å½å大å¨åè½è¿æªå¯ç¨</div>`; 682 } 683 try { 684 const recipe = 685 (typeof allRecipes !== "undefined" && 686 allRecipes.find((r) => r.title === recipeTitle)) || 687 { title: recipeTitle }; 688 let html = ""; 689 html += `<div class="cg-notebook-header">
690 <span class="cg-notebook-icon">ð</span> 691 <div> 692 <div class="cg-notebook-title">${recipe.title} · 大卿»ç»</div> 693 <div class="cg-notebook-sub">3个è¦ç¹ + 2个å + 1个å级</div> 694 </div> 695 </div>`; 696 if (window.ChefMemory) { 697 const cooked = ChefMemory.getCookedRecipes(); 698 const mem = cooked.find((r) => r.title === recipe.title); 699 if (mem) { 700 const date = new Date(mem.lastCooked).toLocaleDateString(); 701 html += `<div class="cg-memory-note"> 702 <span class="cg-memory-icon">ð§ </span> 703 <span>ä½ åè¿è¿éè ${mem.count} 次ï¼ä¸æ¬¡ ${date}ã</span> 704 </div>`; 705 } 706 } 707 708 let hasContent = false; 709 710 // 1. é»è®¤å¤§å¨æ»ç»ï¼AIé对该èè°±çæï¼3+2+1æ ¼å¼ï¼ï¼å¸¦ç¼å 711 if (ChefManager.isEnabled(ChefManager.DEFAULT_CHEF_ID)) { 712 hasContent = true; 713 const cacheKey = `chef_summary_${recipe.id || recipe.title}`; 714 let cached = null; 715 try { 716 cached = JSON.parse(localStorage.getItem(cacheKey) || "null"); 717 } catch (e) { cached = null; } 718 719 if (cached && cached.text) { 720 // è§£æç»è®¡æ°éç¨äºå¯æ é¢ 721 const keyMatch = cached.text.match(/##\s*æéè¦ç[\d个]+ç¹[\s\S]*?(?=##|$)/); 722 const pitMatch = cached.text.match(/##\s*æå®¹æå¤±è´¥ç[\d个]+ç¹[\s\S]*?(?=##|$)/); 723 const upMatch = cached.text.match(/##\s*䏿¬¡å¯ä»¥å级ç[\d个]+ç¹[\s\S]*?(?=##|$)/); 724 const keyN = keyMatch ? (keyMatch[0].match(/^\d+[.ã]/gm) || []).length : 3; 725 const pitN = pitMatch ? (pitMatch[0].match(/^\d+[.ã]/gm) || []).length : 2; 726 const upN = upMatch ? (upMatch[0].match(/^\d+[.ã]/gm) || []).length : 1; 727 // æ´æ°å¯æ é¢ 728 const subEl = document.querySelector('.cg-notebook-sub'); 729 if (subEl) subEl.textContent = `${keyN}个è¦ç¹ + ${pitN}个å + ${upN}个å级`; 730 731 html += `<div class="cg-ai-summary" style="margin-top:12px;"> 732 ${this.renderAISummaryCards(cached.text)} 733 </div>`; 734 } else { 735 // æ¾ç¤ºå è½½ä¸ï¼åæ¶è§¦å弿¥çæ 736 const loadingId = `ai_summary_loading_${Date.now()}`; 737 html += `<div class="cg-ai-summary" id="${loadingId}" style="margin-top:12px;"> 738 <div style="display:flex;align-items:center;gap:6px;margin-bottom:12px;padding-left:4px;"> 739 <span style="font-size:16px;">ð¨âð³</span> 740 <span style="font-size:15px;font-weight:600;color:#3cb371;">é»è®¤å¤§å¨æ»ç»</span> 741 </div> 742 <div style="color:var(--text-muted);font-size:14px;display:flex;align-items:center;gap:8px;padding:16px;background:#f8f9fa;border-radius:12px;"> 743 <span class="loading-spinner" style="width:16px;height:16px;border-width:2px;border-color:#3cb371;border-top-color:transparent;"></span> 744 大卿£å¨æ»ç»è¿éèçè¦ç¹... 745 </div> 746 </div>`; 747 // 弿¥çæAIæ»ç» 748 setTimeout(async () => { 749 try { 750 const ingredients = (recipe.coreIngredients || recipe.ingredients || []).join("ã"); 751 const steps = (recipe.steps || []).join("ï¼"); 752 const prompt = `ä½ æ¯ä¸ä½ç»éªä¸°å¯çä¸é¤å¤§å¨ï¼ç°å¨è¦ç»ç¨æ·æ»ç»ã${recipe.title}ãè¿éèçå ³é®è¦ç¹ã 753 754è¿éèç飿ï¼${ingredients} 755è¿éèçæ¥éª¤ï¼${steps} 756 757è¯·ä¸¥æ ¼æç §ä»¥ä¸æ ¼å¼è¾åºæ»ç»ï¼åªé对è¿éèï¼ä¸è¦è¾åºæ å ³å 容ï¼ä¸è¦æå ¶ä»èãä¸è¦æå¶è/åè±/å¾è¡/䏿µçä¸ç¸å ³ææ³ï¼é¤éè¿éèç¡®å®ç¨å°ï¼ï¼ 758 759## æéè¦ç3ä¸ªç¹ 7601. ï¼ç¬¬ä¸ç¹ï¼ä¸å¥è¯è¯´æ¸ æ¥æ ¸å¿å ³é®ï¼ 7612. ï¼ç¬¬äºç¹ï¼ 7623. ï¼ç¬¬ä¸ç¹ï¼ 763 764## æå®¹æå¤±è´¥ç2ä¸ªç¹ 7651. ï¼ç¬¬ä¸ä¸ªåï¼å ·ä½è¯´æ¯ä»ä¹é®é¢ä»¥åå¦ä½é¿å ï¼ 7662. ï¼ç¬¬äºä¸ªåï¼ 767 768## 䏿¬¡å¯ä»¥å级ç1ä¸ªç¹ 7691. ï¼å ·ä½å¯æä½çä¸ä¸ªæ¹è¿æ¹åï¼æ¯å¦è°å³ãç«åãæé çï¼ä¸è¦æ³æ³èè°ï¼ 770 771è¦æ±ï¼è¯è¨ç®æ´æ¥å°æ°ï¼å大å¨è¯´è¯ï¼æ¯ç¹ä¸è¦è¶ è¿2è¡ã`; 772 const aiText = await callAI(prompt, "chef_summary"); 773 // ç¼åç»æ 774 localStorage.setItem(cacheKey, JSON.stringify({ text: aiText, time: Date.now() })); 775 // æ´æ°å¯æ é¢ç»è®¡
776 const keyMatch = aiText.match(/##\s*æéè¦ç[\d个]+ç¹[\s\S]*?(?=##|$)/); 777 const pitMatch = aiText.match(/##\s*æå®¹æå¤±è´¥ç[\d个]+ç¹[\s\S]*?(?=##|$)/); 778 const upMatch = aiText.match(/##\s*䏿¬¡å¯ä»¥å级ç[\d个]+ç¹[\s\S]*?(?=##|$)/); 779 const keyN = keyMatch ? (keyMatch[0].match(/^\d+[.ã]/gm) || []).length : 3; 780 const pitN = pitMatch ? (pitMatch[0].match(/^\d+[.ã]/gm) || []).length : 2; 781 const upN = upMatch ? (upMatch[0].match(/^\d+[.ã]/gm) || []).length : 1; 782 const subEl = document.querySelector('.cg-notebook-sub'); 783 if (subEl) subEl.textContent = `${keyN}个è¦ç¹ + ${pitN}个å + ${upN}个å级`; 784 // æ´æ°DOM 785 const el = document.getElementById(loadingId); 786 if (el) { 787 el.innerHTML = this.renderAISummaryCards(aiText); 788 } 789 } catch (e) { 790 console.error("AIæ»ç»çæå¤±è´¥", e); 791 const el = document.getElementById(loadingId); 792 if (el) { 793 el.innerHTML = `<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px;padding-left:4px;"> 794 <span style="font-size:16px;">ð¨âð³</span> 795 <span style="font-size:15px;font-weight:600;color:#3cb371;">é»è®¤å¤§å¨æ»ç»</span> 796 </div> 797 <div style="color:#999;font-size:13px;padding:12px;background:#fff5f5;border-radius:8px;">æ»ç»çæå¤±è´¥ï¼è¯·ç¨åéè¯</div> 798 <button onclick="this.parentElement.remove();ChefGuides.buildSummaryHTML('${recipeTitle}').then(html=>{this.closest('.chef-guide-section').querySelector('.chef-guide-content').innerHTML=html;})" style="margin-top:8px;padding:6px 14px;border:1px solid #3cb371;border-radius:8px;background:white;color:#3cb371;font-size:12px;cursor:pointer;">éè¯</button>`; 799 } 800 } 801 }, 100); 802 } 803 } 804 805 // 2. èªå®ä¹å¤§å¨æ»ç» 806 const customSummaries = ChefManager.getEnabledChefSummariesForRecipe(recipeTitle); 807 for (const { chef, summary } of customSummaries) { 808 if (!chef.isDefault && summary) { 809 hasContent = true; 810 const avatarHtml = chef.avatar 811 ? `<img src="${chef.avatar}" style="width:22px;height:22px;border-radius:50%;object-fit:cover;vertical-align:middle;" />` 812 : "ð§âð³"; 813 html += `<div class="cg-custom-summary-block" style="margin-top:20px;padding-top:16px;border-top:1px dashed #e0e0d8;">`; 814 html += `<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;padding-left:4px;"> 815 ${avatarHtml} 816 <span style="font-size:15px;font-weight:600;color:${chef.color};">${chef.name}çæ»ç»</span> 817 </div>`; 818 // 润è²åå 容 819 html += `<div class="cg-custom-note-content" style="padding:0;background:transparent;">${this.renderMarkdown(this._fillEmptySections(summary))}</div>`; 820 // åå§å å®¹ï¼æ¶¦è²åï¼ 821 const note = chef.recipes.find(r => r.title === recipeTitle); 822 if (note && note.rawSummary && note.rawSummary.trim() && note.rawSummary.trim() !== summary.trim()) { 823 html += `<div class="cg-raw-content-block" style="margin-top:12px;padding:12px;background:#f5f5f0;border-radius:10px;">`; 824 html += `<div class="cg-raw-content-label" style="font-size:12px;color:#999;margin-bottom:6px;">ð åå§è¾å ¥</div>`; 825 html += `<div class="cg-raw-content-text" style="font-size:13px;color:#888;line-height:1.6;">${this.renderMarkdown(this._fillEmptySections(note.rawSummary))}</div>`; 826 html += `</div>`; 827 } 828 html += `</div>`; 829 } 830 } 831 832 if (!hasContent) { 833 html += `<div class="chef-guide-empty">ææ è¿éèç大卿»ç»</div>`; 834 } 835 return html; 836 } catch (e) { 837 return `<div class="chef-guide-empty">大卿»ç»å 载失败ï¼${e.message}</div>`; 838 } 839 }, 840 841 // ã大å¨ç¬è®°ãï¼å¨åèè°±é¡µé¢æ³¨å ¥ææå¯ç¨å¨å¸çç¬è®°ï¼é»è®¤ç»¿è² + èªå®ä¹å½©è²ï¼
842 async injectNotesToPage(recipeTitle) { 843 // æ£æ¥æ¯å¦æå¯ç¨çå¨å¸ 844 const enabledChefs = ChefManager.getEnabled(); 845 if (enabledChefs.length === 0) return false; 846 847 if (this._notesActive) this.clearNotesFromPage(); 848 const recipe = 849 (typeof allRecipes !== "undefined" && 850 allRecipes.find((r) => r.title === recipeTitle)) || 851 { title: recipeTitle }; 852 const stepItems = document.querySelectorAll(".recipe-detail .step-item, .recipe-detail-body .step-item"); 853 if (stepItems.length === 0) return false; 854 const steps = []; 855 stepItems.forEach((item) => { 856 const textEl = item.querySelector(".step-text"); 857 steps.push(textEl ? textEl.textContent.trim() : ""); 858 }); 859 860 // ä¿ååå§HTMLç¨äºæ¢å¤ 861 this._originalStepHtml = []; 862 stepItems.forEach((item) => { 863 const textEl = item.querySelector(".step-text"); 864 if (!textEl) return; 865 this._originalStepHtml.push({ item, textEl, originalHtml: textEl.innerHTML, afterEls: [] }); 866 }); 867 868 let hasAnyNotes = false; 869 870 // 1. é»è®¤å¤§å¨ç¬è®°ï¼è·é大å¨é¢è²ï¼ä»markdownæä»¶å è½½ï¼ 871 if (ChefManager.isEnabled(ChefManager.DEFAULT_CHEF_ID)) { 872 const defaultChef = ChefManager.getDefault(); 873 const defaultColor = (defaultChef && defaultChef.color) || "#3cb371"; 874 const stepNotes = await this._matchNotesToSteps(steps, recipe); 875 stepItems.forEach((item, si) => { 876 const textEl = item.querySelector(".step-text"); 877 if (!textEl) return; 878 const notes = stepNotes[si]; 879 if (notes && notes.length > 0) { 880 hasAnyNotes = true; 881 // é«äº®æ¥éª¤ææ¬ 882 const currentHtml = textEl.innerHTML; 883 textEl.innerHTML = this._highlightStepText(steps[si], notes); 884 // æå»ºæ¹æ³¨å®¹å¨ï¼è·é大å¨é¢è²ï¼ 885 const wrapper = this._buildNotesWrapper(notes, defaultColor); 886 const notesContainer = document.createElement("div"); 887 notesContainer.className = "cg-page-notes-container"; 888 notesContainer.appendChild(wrapper); 889 item.parentNode.insertBefore(notesContainer, item.nextSibling); 890 const entry = this._originalStepHtml.find(e => e.item === item); 891 if (entry) entry.afterEls.push(notesContainer); 892 } 893 }); 894 } 895 896 // 2. èªå®ä¹å¤§å¨ç¬è®°ï¼åå¨å¸èªæé¢è²ï¼ä»ç¨æ·è¾å ¥å 容å¹é ï¼ 897 const customNotes = ChefManager.getEnabledChefNotesForRecipe(recipeTitle); 898 console.log("[ChefGuides] èªå®ä¹å¤§å¨ç¬è®°:", customNotes.length, "æ¡", "for recipe:", recipeTitle); 899 for (const { chef, note } of customNotes) { 900 if (!chef.isDefault && note.content) { 901 console.log("[ChefGuides] å¤çå¨å¸:", chef.name, "contenté¿åº¦:", note.content.length, "contentå80å:", note.content.substring(0, 80)); 902 const customStepNotes = this._matchCustomNotesToSteps(steps, note.content); 903 const totalMatches = customStepNotes.reduce((s, n) => s + n.length, 0); 904 console.log("[ChefGuides] å¹é ç»æ:", totalMatches, "æ¡ç¬è®°"); 905 stepItems.forEach((item, si) => { 906 const textEl = item.querySelector(".step-text"); 907 if (!textEl) return; 908 const notes = customStepNotes[si]; 909 if (notes && notes.length > 0) { 910 hasAnyNotes = true; 911 // 妿é»è®¤å¨å¸æ²¡æé«äº®è¿ï¼éè¦é«äº® 912 const currentText = textEl.textContent.trim(); 913 if (!textEl.querySelector("u.cg-underline")) { 914 textEl.innerHTML = this._highlightStepText(currentText, notes); 915 } 916 // æå»ºæ¹æ³¨å®¹å¨ï¼å¨å¸èªæé¢è²ï¼ 917 const wrapper = this._buildNotesWrapper(notes, chef.color, chef.name); 918 const notesContainer = document.createElement("div"); 919 notesContainer.className = "cg-page-notes-container"; 920 notesContainer.appendChild(wrapper); 921 item.parentNode.insertBefore(notesContainer, item.nextSibling); 922 const entry = this._originalStepHtml.find(e => e.item === item); 923 if (entry) entry.afterEls.push(notesContainer); 924 } 925 }); 926 927 // åå§å å®¹åªæ¾ç¤ºä¸æ¬¡ï¼æ¾å¨æåä¸ä¸ªæ¥éª¤åé¢ 928 const hasRaw = note.rawContent && note.rawContent.trim() && note.rawContent.trim() !== note.content.trim(); 929 if (hasRaw && stepItems.length > 0) { 930 const lastItem = stepItems[stepItems.length - 1]; 931 const rawBlock = document.createElement("div"); 932 rawBlock.className = "cg-page-notes-container chef-notes-custom"; 933 rawBlock.style.setProperty("--cg-color", chef.color); 934 const rawInner = document.createElement("div"); 935 rawInner.className = "cg-page-notes-wrapper"; 936 rawInner.style.setProperty("--cg-color", chef.color); 937 rawInner.innerHTML = 938 '<div class="cg-page-annotation-bubble" style="color:' + chef.color + '">' + 939 '<span class="cg-step-note-tag">' + chef.name + ' · åå§è¾å ¥</span>' + 940 '<div class="cg-raw-content-text">' + this.renderMarkdown(this._fillEmptySections(note.rawContent)) + '</div>' + 941 '</div>'; 942 rawBlock.appendChild(rawInner); 943 // æ¾å°æåä¸ä¸ªæ¥éª¤çä¸ä¸ä¸ªå å¼å ç´ ï¼æå ¥å°å®åé¢ 944 const afterLast = lastItem.nextElementSibling; 945 if (afterLast) { 946 lastItem.parentNode.insertBefore(rawBlock, afterLast); 947 } else { 948 lastItem.parentNode.appendChild(rawBlock); 949 } 950 const entry = this._originalStepHtml.find(e => e.item === lastItem); 951 if (entry) entry.afterEls.push(rawBlock); 952 } 953 } 954 } 955 956 this._notesActive = true; 957 document.body.classList.add("chef-notes-active"); 958 return hasAnyNotes; 959 }, 960 961 // æå»ºæ¹æ³¨å®¹å¨ï¼æ¯æèªå®ä¹é¢è²åå¨å¸åï¼ 962 _buildNotesWrapper(notes, color, chefName) { 963 const wrapper = document.createElement("div"); 964 wrapper.className = "cg-page-notes-wrapper"; 965 if (chefName) wrapper.classList.add("cg-page-notes-custom"); 966 // å§ç»è®¾ç½® --cg-colorï¼ä½¿é»è®¤å¤§å¨ç¬è®°ä¹è½è·é大å¨é¢è² 967 wrapper.style.setProperty("--cg-color", color); 968 for (const note of notes) { 969 const bubble = document.createElement("div"); 970 bubble.className = "cg-page-annotation-bubble"; 971 bubble.style.setProperty("--cg-color", color); 972 const tagText = chefName ? `${chefName}·${note.section}` : note.section; 973 bubble.innerHTML = 974 '<span class="cg-anno-dot"></span>' + 975 '<span class="cg-step-note-tag">' + tagText + "</span>" + 976 '<span class="cg-anno-text">' + this.renderMarkdownInline(note.text) + "</span>"; 977 wrapper.appendChild(bubble); 978 } 979 return wrapper; 980 }, 981 982 // å¡«å 空æ é¢é¨å为"ææ "ï¼ç¨äºèªå®ä¹å¤§å¨å 容渲æï¼ 983 _fillEmptySections(md) { 984 const lines = md.split("\n"); 985 const result = []; 986 for (let i = 0; i < lines.length; i++) { 987 result.push(lines[i]); 988 // å¦ææ¯ ## æ é¢è¡ï¼æ£æ¥å颿¯å¦æå 容 989 if (/^##\s+/.test(lines[i].trim())) { 990 let j = i + 1; 991 let hasContent = false;
992 while (j < lines.length && !/^##\s+/.test(lines[j].trim())) { 993 if (lines[j].trim()) { hasContent = true; break; } 994 j++; 995 } 996 if (!hasContent) { 997 result.push("ææ "); 998 } 999 } 1000 } 1001 return result.join("\n"); 1002 }, 1003 1004 // å¹é èªå®ä¹å¨å¸ç¬è®°å°æ¥éª¤ï¼ä»ç¨æ·è¾å ¥çmarkdownææ¬ï¼ 1005 // åç±»æ¥éª¤ç±»åï¼prep=夿, cook=ç¹é¥ª, other=å ¶ä» 1006 _classifyStepType(stepText) { 1007 const prepKw = /å|ä¸|ç|ä¸|å|æ¡|æ«|è |æ¸|泡|æ´|å|å¥|å|å|ç¢|æ¹|æ¶|裹|æ²¾|è¸|æ|ç»|æ|æ©|ç|浸泡|ç¯æ°´|å»ç®|å»éª¨|å»è ¥/; 1008 const cookKw = /ç|ç |ç¸|è¸|ç ®|ç|ç|ç§|ç¤|æ|ç|ç|ç ¸|ç|ç©|ç|ç¬|æ±|ç«|ç¯|ç ¸|é |æ²¹|ç«|大ç«|å°ç«|ä¸ç«|çé |温油|çé¦|ç¿»ç|æ¶æ±|å¾è¡|æ·æ²¹|åºé |è£ ç/; 1009 if (prepKw.test(stepText) && !cookKw.test(stepText)) return "prep"; 1010 if (cookKw.test(stepText)) return "cook"; 1011 return "other"; 1012 }, 1013 1014 _matchCustomNotesToSteps(steps, noteContent) { 1015 const parsed = this._parseRecipeMarkdown(noteContent); 1016 let pool = [ 1017 ...parsed.prep.map((t) => ({ text: t, section: "飿åå¤" })), 1018 ...parsed.steps.map((t) => ({ text: t, section: "详ç»åæ³" })), 1019 ...parsed.tips.map((t) => ({ text: t, section: "å ³é®æå·§" })), 1020 ...parsed.problems.map((t) => ({ text: t, section: "常è§é®é¢" })), 1021 ]; 1022 1023 // åéï¼å¦æ markdown è§£æç»æä¸ºç©ºï¼çº¯ææ¬å 容æªç»AIå¤çï¼ï¼ 1024 // æè¡åå²ï¼æ¯è¡ä½ä¸ºä¸æ¡ç¬è®° 1025 if (pool.length === 0) { 1026 const lines = noteContent.split("\n") 1027 .map((l) => l.trim()) 1028 .filter((l) => l && !/^#{1,6}\s/.test(l) && l.length > 3); 1029 pool = lines.map((text) => ({ text, section: "ç¬è®°" })); 1030 } 1031 1032 // æç»ä»ä¸ºç©ºåè¿å空 1033 if (pool.length === 0) return steps.map(() => []); 1034 1035 // åç±»æ¯ä¸ªæ¥éª¤çç±»å 1036 const stepTypes = steps.map((s) => this._classifyStepType(s)); 1037 const firstPrepIdx = stepTypes.indexOf("prep"); 1038 const firstCookIdx = stepTypes.indexOf("cook"); 1039 const lastStepIdx = steps.length - 1; 1040 1041 // ååºå¹é ï¼ä¸åsectionçç¬è®°åªå¹é 对åºç±»åçæ¥éª¤ 1042 const stepNotes = steps.map(() => []); 1043 const usedIndices = new Set(); 1044 const maxPerStep = 3; 1045 1046 // æsectionåç» 1047 const sections = ["飿åå¤", "详ç»åæ³", "å ³é®æå·§", "常è§é®é¢", "ç¬è®°"]; 1048 for (const section of sections) { 1049 const sectionNotes = pool 1050 .map((n, idx) => ({ ...n, idx })) 1051 .filter((n) => n.section === section && !usedIndices.has(n.idx)); 1052 if (sectionNotes.length === 0) continue; 1053 1054 // ç¡®å®è¯¥sectionå¯å¹é çæ¥éª¤èå´ 1055 let candidateSteps; 1056 if (section === "飿åå¤") { 1057 candidateSteps = firstPrepIdx >= 0 ? [firstPrepIdx] : [0]; 1058 } else if (section === "详ç»åæ³") { 1059 candidateSteps = firstCookIdx >= 0 ? stepTypes.map((t, i) => t === "cook" ? i : -1).filter(i => i >= 0) : [lastStepIdx]; 1060 } else { 1061 // å ³é®æå·§/常è§é®é¢/ç¬è®° â å¹é æææ¥éª¤ 1062 candidateSteps = steps.map((_, i) => i); 1063 } 1064 1065 // å å°è¯å ³é®è¯å¹é 1066 const pairs = []; 1067 for (const si of candidateSteps) { 1068 const stepKw = new Set(this._extractKeywords(steps[si])); 1069 if (stepKw.size === 0) continue; 1070 for (const n of sectionNotes) { 1071 const noteKw = this._extractKeywords(n.text); 1072 const overlap = noteKw.filter((k) => stepKw.has(k)); 1073 if (overlap.length > 0) 1074 pairs.push({ si, note: n, score: overlap.length }); 1075 } 1076 } 1077 1078 pairs.sort((a, b) => b.score - a.score); 1079 for (const pair of pairs) { 1080 if (usedIndices.has(pair.note.idx)) continue; 1081 if (stepNotes[pair.si].length >= maxPerStep) continue; 1082 stepNotes[pair.si].push(pair.note); 1083 usedIndices.add(pair.note.idx); 1084 } 1085 1086 // æªå¹é çç¬è®°ï¼åé å°è¯¥sectionçé»è®¤æ¥éª¤ 1087 for (const n of sectionNotes) { 1088 if (usedIndices.has(n.idx)) continue; 1089 let targetIdx; 1090 if (section === "飿åå¤") { 1091 targetIdx = firstPrepIdx >= 0 ? firstPrepIdx : 0; 1092 } else if (section === "详ç»åæ³") { 1093 targetIdx = firstCookIdx >= 0 ? firstCookIdx : lastStepIdx; 1094 } else { 1095 targetIdx = lastStepIdx; 1096 } 1097 if (stepNotes[targetIdx].length < maxPerStep) { 1098 stepNotes[targetIdx].push(n); 1099 usedIndices.add(n.idx); 1100 } 1101 } 1102 } 1103 1104 return stepNotes; 1105 }, 1106 1107 // æ¸ é¤é¡µé¢ä¸ç大å¨ç¬è®°ï¼æ¢å¤åæ · 1108 clearNotesFromPage() { 1109 if (!this._notesActive || !this._originalStepHtml) return; 1110 for (const { textEl, originalHtml, afterEls } of this._originalStepHtml) { 1111 if (textEl) textEl.innerHTML = originalHtml; 1112 if (afterEls) { 1113 for (const el of afterEls) { 1114 if (el && el.parentNode) el.parentNode.removeChild(el); 1115 } 1116 } 1117 } 1118 this._originalStepHtml = null; 1119 this._notesActive = false;
1120 document.body.classList.remove("chef-notes-active"); 1121 }, 1122 1123 // å½åæ¯å¦å¤äºç¬è®°æ¨¡å¼ 1124 isNotesActive() { 1125 return this._notesActive; 1126 }, 1127 1128 // æåä¸æå ³é®è¯ï¼2-gram æ»å¨çªå£ï¼ä¸ææ ç©ºæ ¼ï¼éææäºå è¯ç»ï¼ 1129 _extractKeywords(text) { 1130 const chinese = text.replace(/[^\u4e00-\u9fa5]/g, ""); 1131 const stop = new Set([ 1132 "å¤ç¨","ç¶å","è¿ä¸","ä¸è¦","å¯ä»¥","ä¸ä¸","轻轻","å°±æ¯","è¿æ ·", 1133 "ä¸ä¸ª","åå¤","è¿è¡","ç´æ¥","åæ¶","æå","é¦å ","å·²ç»","æ¶å", 1134 "æ¶é´","éé","å°è®¸","大约","éè¦","主è¦","æ¹ä¾¿","æ¾å ¥","é ä¸", 1135 "åå","åé","å·¦å³","å³å¯","ä¸å¤","è®°å¾","å¦å¤","表é¢","åºæ¥", 1136 "æ¥ç","没æ","ä¸è½","è¿æ¯","æè ","æ¯å¦","ä¾å¦","ç°å¨","å¼å§", 1137 "ä¸ç´","é¨å","éé¢","ä¸é¢","ä¸é¢","åé¢","åé¢","ä¸é´","䏿¬¡", 1138 "以å","以å","ä¹å","ä¹å","å 为","æä»¥","使¯","ä¸è¿","妿", 1139 "å°½é","æå¥½","建议","æ¨è","æ¯è¾","é常","ç¹å«","ç¨å¾®","ç¥å¾®", 1140 // 常è§éç¹é¥ª 2-gramï¼å®¹æäº§ç误å¹é 1141 "ä¸è¶ ","è¶ è¿","ä¸å°","ä¸å¯","ä¸å¤ª","ä¸ä¼","ä¸ä¸","ä¸è¶³", 1142 "大å°","å¤å°","以ä¸","以ä¸","以å¤","以å ","æ¤æ¶","æ¤å»", 1143 "è¿ç§","é£ç§","åç§","è¿ç§","é£ç§","ä»ä¹","æä¹","为ä»ä¹", 1144 "为äº","ä»ä¹","å¦ä½","æ¯å¦","æ¯å¦","åºè¯¥","åºå½","å¿ é¡»", 1145 "å¯ä»¥","è½å¤","使å¾","使ç¨","å©ç¨","éè¿","è¿ç§","æ¹å¼", 1146 "æ¶å","æ åµ","é®é¢","æ¹é¢","æ¹æ³","è¿ç¨","ç»æ","ææ", 1147 "导è´","å¼èµ·","é æ","åºç°","åç","åæ","æä¸º","å½¢æ", 1148 "åæ ·","ä¸å","ç¸å","类似","大æ¦","ä¹è®¸","å¯è½","ä¼¼ä¹", 1149 "å¾å°","åå°","宿","ç»æ","ç»§ç»","忢","å¼å§","ä¿æ", 1150 "å å ¥","åå ¥","æ¾å ¥","çç","åºé ","å ³ç«","å¼ç«", // å¨ä½å¤ªæ³ï¼æææ¥éª¤é½æ 1151 ]); 1152 const terms = new Set(); 1153 for (let i = 0; i < chinese.length - 1; i++) { 1154 const t = chinese.slice(i, i + 2); 1155 if (!stop.has(t)) terms.add(t); 1156 } 1157 return [...terms]; 1158 }, 1159}; 1160 1161if (typeof window !== "undefined") { 1162 window.ChefGuides = ChefGuides; 1163}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.