1// js/ai-service.js â IA via backend (Groq / OpenRouter / Google), chaves no servidor 2 3const AiService = { 4 _cache: new Map(), 5 _endpoint: 'ai.php', 6 7 url() { 8 return apiResolve(this._endpoint); 9 }, 10 11 async complete(task, payload, opts = {}) { 12 const lang = opts.lang || getLang(); 13 const cacheKey = `${task}:${lang}:${JSON.stringify(payload)}`; 14 const ttl = opts.ttl ?? (task === 'translate' ? 3600000 : 300000); 15 const hit = this._cache.get(cacheKey); 16 if (hit && Date.now() - hit.ts < ttl) return hit.data; 17 18 let data; 19 try { 20 const res = await fetch(this.url(), { 21 method: 'POST', 22 headers: { 'Content-Type': 'application/json' }, 23 body: JSON.stringify({ task, lang, payload }) 24 }); 25 const text = await res.text(); 26 data = text ? JSON.parse(text) : {}; 27 if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`); 28 } catch (err) { 29 if (opts.optional) return null; 30 throw err; 31 } 32 33 this._cache.set(cacheKey, { ts: Date.now(), data }); 34 return data; 35 }, 36 37 async translate(text, opts = {}) { 38 if (!text || !AiPreview?.needsTranslation?.(text)) { 39 return AiPreview?.fixOrthography?.(text, getLang()) || text; 40 } 41 try { 42 const res = await this.complete('translate', { text }, { ...opts, optional: true, ttl: 3600000 }); 43 if (res?.text) return AiPreview.fixOrthography(AiPreview.stripMarkdown(res.text), getLang()); 44 } catch (_) { /* fallback */ } 45 return AiPreview.translateChunks(text); 46 }, 47 48 leagueName(match) { 49 if (typeof AiPreview?.leagueName === 'function') return AiPreview.leagueName(match); 50 return match.league_name || ''; 51 }, 52 53 compactContext(match, bundles = {}) { 54 const { event, meta, prediction, odds, espn, s365, sofa, sm } = bundles; 55 const m = event || match; 56 const mr = prediction?.markets?.match_result; 57 const ctx = { 58 home: m.home_team, 59 away: m.away_team, 60 league: this.leagueName(m), 61 date: m.event_date, 62 status: m.status, 63 score: `${m.home_score ?? '-'} x ${m.away_score ?? '-'}`, 64 venue: m.venue_name || event?.venue?.name || s365?.venue?.name || sofa?.venue?.name, 65 weather: m.weather?.description || null, 66 derby: Boolean(m.is_local_derby || event?.is_local_derby) 67 }; 68 69 if (mr) { 70 ctx.model = { 71 probHome: mr.prob_home, 72 probDraw: mr.prob_draw, 73 probAway: mr.prob_away, 74 favorite: mr.predicted || prediction?.recommendations?.favorite, 75 likelyScore: prediction?.markets?.score?.most_likely, 76 xg: prediction?.markets?.expected_goals 77 }; 78 } 79 80 const h2h = event?.head_to_head || m.head_to_head; 81 if (h2h?.total_matches >= 2) { 82 ctx.h2h = { 83 total: h2h.total_matches, 84 homeWins: h2h.home_wins, 85 draws: h2h.draws, 86 awayWins: h2h.away_wins, 87 avgGoals: h2h.avg_total_goals 88 }; 89 } 90 91 if (odds) { 92 ctx.odds = { 93 home: odds.home_win, 94 draw: odds.draw, 95 away: odds.away_win, 96 over25: odds.over_25_goals, 97 bttsYes: odds.btts_yes 98 }; 99 } 100 101 if (meta?.funfacts?.length) { 102 ctx.funfacts = meta.funfacts.slice(0, 3).map(f => f.sentence || f.text).filter(Boolean); 103 } 104 105 if (espn?.summary) { 106 const leaders = EspnAPI?.parseLeaders?.(espn.summary) || []; 107 ctx.espn = { leaders: leaders.slice(0, 4).map(l => `${l.label}: ${l.name}`) }; 108 } 109 110 if (s365) { 111 ctx.scores365 = { 112 venue: s365.venue?.name, 113 stages: s365.stages?.map(s => `${s.shortName || s.name}: ${s.homeCompetitorScore}-${s.awayCompetitorScore}`), 114 poll: s365.predictions?.[0]?.options?.map(o => `${o.name} ${o.vote?.percentage}%`) 115 }; 116 } 117 118 if (sofa) { 119 ctx.sofascore = { 120 tournament: sofa.tournament, 121 odds1x2: sofa.odds1x2, 122 stats: (sofa.statistics || []).slice(0, 4).map(s => `${s.name}: ${s.home}-${s.away}`) 123 }; 124 } 125 126 if (sm) { 127 ctx.sportmonks = { 128 league: sm.league, 129 stats: (sm.stats || []).slice(0, 4).map(s => `${s.name}: ${s.home}-${s.away}`) 130 }; 131 } 132 133 return ctx; 134 }, 135
136 async gatherBundles(match) { 137 const id = match.id; 138 return Promise.all([ 139 BzzoiroAPI.getEvent(id).catch(() => match), 140 BzzoiroAPI.getEventMetadata(id).catch(() => null), 141 BzzoiroAPI.getEventPrediction(id).catch(() => null), 142 BzzoiroAPI.getEventOdds(id).catch(() => null), 143 typeof getEspnEnrichment === 'function' ? getEspnEnrichment(match).catch(() => null) : null, 144 typeof getScores365Enrichment === 'function' ? getScores365Enrichment(match).catch(() => null) : null, 145 typeof getSofascoreEnrichment === 'function' ? getSofascoreEnrichment(match).catch(() => null) : null, 146 typeof getSportmonksEnrichment === 'function' ? getSportmonksEnrichment(match).catch(() => null) : null 147 ]).then(([event, meta, prediction, oddsData, espn, s365, sofa, sm]) => ({ 148 event, meta, prediction, 149 odds: oddsData?.odds || null, 150 espn, s365, sofa, sm 151 })); 152 }, 153 154 async matchPreview(match, opts = {}) { 155 try { 156 const bundles = await this.gatherBundles(match); 157 const payload = this.compactContext(match, bundles); 158 const res = await this.complete('match_preview', payload, { ...opts, optional: true }); 159 if (res?.text) return AiPreview.stripMarkdown(res.text); 160 } catch (_) { /* fallback */ } 161 return null; 162 }, 163 164 oddsPayload(pick) { 165 return { 166 home: pick.event?.home_team, 167 away: pick.event?.away_team, 168 league: OddsIntel?.leagueDisplayName?.(pick.event) || pick.event?.league_name, 169 pick: pick.shortPick, 170 modelProb: pick.modelProb, 171 marketOdd: pick.marketOdd, 172 marketProb: pick.marketProb, 173 edge: pick.edge, 174 confidence: pick.confidence, 175 bookmaker: pick.bookmakerName, 176 ouPick: pick.ouPick?.label 177 }; 178 }, 179 180 async oddsInsight(pick, opts = {}) { 181 try { 182 const res = await this.complete('odds_insight', this.oddsPayload(pick), { ...opts, optional: true }); 183 if (res?.text) return AiPreview.stripMarkdown(res.text); 184 } catch (_) { /* fallback */ } 185 return null; 186 }, 187 188 async oddsSummary(picks, opts = {}) { 189 try { 190 const payload = { 191 picks: (picks || []).slice(0, 8).map(p => this.oddsPayload(p)) 192 }; 193 const res = await this.complete('odds_summary', payload, { ...opts, optional: true }); 194 if (res?.text) return AiPreview.stripMarkdown(res.text); 195 } catch (_) { /* fallback */ } 196 return null; 197 } 198};
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.