fix: add error handling to analytics getTopics query and fix mascot tooltip positioning
This commit is contained in:
@@ -315,56 +315,61 @@ export class AnalyticsRepository {
|
|||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
const filter = buildTimeFilter(guildId, channelId, hours);
|
const filter = buildTimeFilter(guildId, channelId, hours);
|
||||||
|
|
||||||
const { rows } = await pool.query(
|
try {
|
||||||
`
|
const { rows } = await pool.query(
|
||||||
WITH word_list AS (
|
`
|
||||||
|
WITH word_list AS (
|
||||||
|
SELECT
|
||||||
|
LOWER(TRIM(BOTH '.,!?;:\"()[]{}' FROM word)) AS word,
|
||||||
|
ai_moderation_score
|
||||||
|
FROM messages,
|
||||||
|
LATERAL regexp_split_to_table(content, E'\\\\s+') AS word
|
||||||
|
${filter.where}
|
||||||
|
AND content IS NOT NULL
|
||||||
|
AND content != ''
|
||||||
|
AND LENGTH(TRIM(BOTH '.,!?;:\"()[]{}' FROM word)) >= 3
|
||||||
|
AND word !~ '^<.+:\\d+>$'
|
||||||
|
AND word !~ '^https?://'
|
||||||
|
AND word !~ '^discord\\.(gg|app|com)'
|
||||||
|
AND word !~ '^cdn\\.discord'
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
LOWER(TRIM(BOTH '.,!?;:\"'()[]{}' FROM word)) AS word,
|
word AS topic,
|
||||||
ai_moderation_score
|
COUNT(*)::int AS count,
|
||||||
FROM messages,
|
COALESCE(AVG(ai_moderation_score), 0)::real AS score
|
||||||
LATERAL regexp_split_to_table(content, E'\\\\s+') AS word
|
FROM word_list
|
||||||
${filter.where}
|
WHERE word NOT IN (
|
||||||
AND content IS NOT NULL
|
'yang','dan','di','ke','dari','dengan','untuk','pada','ini','itu',
|
||||||
AND content != ''
|
'ada','akan','telah','sudah','bisa','dapat','tidak','nggak','enggak',
|
||||||
AND LENGTH(TRIM(BOTH '.,!?;:\"()[]{}' FROM word)) >= 3
|
'gak','gk','ga','aku','saya','kamu','dia','kami','kita','mereka',
|
||||||
-- Skip sticker & custom emoji (<:name:id> or <a:name:id>)
|
'iya','ya','yah','oh','ah','eh','lah','pun','juga','masih',
|
||||||
AND word !~ '^<a?:.+:\\d+>$'
|
'saja','hanya','sama','atau','tapi','namun','sedang','sangat',
|
||||||
-- Skip URLs
|
'begitu','karena','sebab','kalau','jika','maka','lalu','setelah',
|
||||||
AND word !~ '^https?://'
|
'seperti','antara','oleh','sebagai','secara','melalui','dalam',
|
||||||
AND word !~ '^discord\\.(gg|app|com)'
|
'the','and','for','are','but','not','you','all','can','has',
|
||||||
-- Skip common Discord embed artifacts
|
'was','were','been','like','just','that','this','with','your',
|
||||||
AND word !~ '^cdn\\.discord'
|
'from','they','have','what','when','where','which','their',
|
||||||
)
|
'about','would','could','should','very','also','than','then'
|
||||||
SELECT
|
)
|
||||||
word AS topic,
|
GROUP BY word
|
||||||
COUNT(*)::int AS count,
|
ORDER BY count DESC
|
||||||
COALESCE(AVG(ai_moderation_score), 0)::real AS score
|
LIMIT 10
|
||||||
FROM word_list
|
`,
|
||||||
WHERE word NOT IN (
|
filter.params,
|
||||||
'yang', 'dan', 'di', 'ke', 'dari', 'dengan', 'untuk', 'pada', 'ini', 'itu',
|
);
|
||||||
'ada', 'akan', 'telah', 'sudah', 'bisa', 'dapat', 'tidak', 'nggak', 'enggak',
|
|
||||||
'gak', 'gk', 'ga', 'aku', 'saya', 'kamu', 'dia', 'kami', 'kita', 'mereka',
|
|
||||||
'iya', 'ya', 'yah', 'oh', 'ah', 'eh', 'lah', 'pun', 'juga', 'masih',
|
|
||||||
'saja', 'hanya', 'sama', 'atau', 'tapi', 'namun', 'sedang', 'sangat',
|
|
||||||
'begitu', 'karena', 'sebab', 'kalau', 'jika', 'maka', 'lalu', 'setelah',
|
|
||||||
'seperti', 'antara', 'oleh', 'sebagai', 'secara', 'melalui', 'dalam',
|
|
||||||
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'has',
|
|
||||||
'was', 'were', 'been', 'like', 'just', 'that', 'this', 'with', 'your',
|
|
||||||
'from', 'they', 'have', 'what', 'when', 'where', 'which', 'their',
|
|
||||||
'about', 'would', 'could', 'should', 'very', 'also', 'than', 'then'
|
|
||||||
)
|
|
||||||
GROUP BY word
|
|
||||||
ORDER BY count DESC
|
|
||||||
LIMIT 10
|
|
||||||
`,
|
|
||||||
filter.params,
|
|
||||||
);
|
|
||||||
|
|
||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
topic: (r.topic as string) ?? "",
|
topic: (r.topic as string) ?? "",
|
||||||
count: Number(r.count ?? 0),
|
count: Number(r.count ?? 0),
|
||||||
score: Number(r.score ?? 0),
|
score: Number(r.score ?? 0),
|
||||||
}));
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ error: error instanceof Error ? error.message : String(error), guildId },
|
||||||
|
"getTopics query failed — returning empty",
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── New endpoints ─────────────────────────────────────────────────────────
|
// ── New endpoints ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export function Sidebar({
|
|||||||
initial={{ opacity: 0, x: -8, y: 6, scale: 0.96 }}
|
initial={{ opacity: 0, x: -8, y: 6, scale: 0.96 }}
|
||||||
animate={{ opacity: 1, x: 0, y: 0, scale: 1 }}
|
animate={{ opacity: 1, x: 0, y: 0, scale: 1 }}
|
||||||
transition={{ type: "spring", stiffness: 280, damping: 24 }}
|
transition={{ type: "spring", stiffness: 280, damping: 24 }}
|
||||||
className="pointer-events-none absolute bottom-full left-1/2 z-[9999] mb-3 w-64 -translate-x-1/2"
|
className="pointer-events-none fixed bottom-[100px] left-[72px] z-[9999] w-64"
|
||||||
>
|
>
|
||||||
<div className="relative rounded-2xl border border-sky-200/80 bg-white/95 px-4 py-3 text-left shadow-xl shadow-sky-100/70 backdrop-blur-md">
|
<div className="relative rounded-2xl border border-sky-200/80 bg-white/95 px-4 py-3 text-left shadow-xl shadow-sky-100/70 backdrop-blur-md">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-primary/70">
|
<p className="text-[11px] font-semibold uppercase tracking-wide text-primary/70">
|
||||||
@@ -126,10 +126,10 @@ export function Sidebar({
|
|||||||
Halo! Saya mascot mu. Klik aku kalau mau tanya soal obrolan
|
Halo! Saya mascot mu. Klik aku kalau mau tanya soal obrolan
|
||||||
atau moderasi ✨
|
atau moderasi ✨
|
||||||
</p>
|
</p>
|
||||||
{/* Outer: border layer — triangle pointing down at center */}
|
{/* Outer: border layer — triangle pointing left at center-right */}
|
||||||
<div className="absolute -bottom-[13px] left-1/2 z-10 h-[21px] w-[21px] -translate-x-1/2 bg-sky-200/80 [clip-path:polygon(0_0,100%_0,50%_100%)]" />
|
<div className="absolute -left-[13px] top-1/2 z-10 h-[21px] w-[21px] -translate-y-1/2 bg-sky-200/80 [clip-path:polygon(0_50%,100%_0,100%_100%)]" />
|
||||||
{/* Inner: fill layer — matches bubble bg */}
|
{/* Inner: fill layer — matches bubble bg */}
|
||||||
<div className="absolute -bottom-3 left-1/2 z-20 h-5 w-5 -translate-x-1/2 bg-white/95 [clip-path:polygon(0_0,100%_0,50%_100%)]" />
|
<div className="absolute -left-3 top-1/2 z-20 h-5 w-5 -translate-y-1/2 bg-white/95 [clip-path:polygon(0_50%,100%_0,100%_100%)]" />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user