diff --git a/apps/mana/apps/web/src/lib/modules/feedback/components/ItemCard.svelte b/apps/mana/apps/web/src/lib/modules/feedback/components/ItemCard.svelte
index 0b8ea9209..57b1896b9 100644
--- a/apps/mana/apps/web/src/lib/modules/feedback/components/ItemCard.svelte
+++ b/apps/mana/apps/web/src/lib/modules/feedback/components/ItemCard.svelte
@@ -30,6 +30,30 @@
let tier = $derived(tierFromKarma(item.karma ?? 0));
let tierCfg = $derived(KARMA_TIER_CONFIG[tier]);
+ // Hide the bold title when it's just a truncated/prefix copy of the
+ // body — pre-rename items have title === feedbackText for short posts.
+ let normalizedTitle = $derived(
+ (item.title ?? '')
+ .toLowerCase()
+ .replace(/[…\.]+$/u, '')
+ .replace(/\s+/g, ' ')
+ .trim()
+ );
+ let normalizedBody = $derived(
+ item.feedbackText
+ .toLowerCase()
+ .replace(/[…\.]+$/u, '')
+ .replace(/\s+/g, ' ')
+ .trim()
+ );
+ let showTitle = $derived(
+ !!item.title &&
+ normalizedTitle.length > 0 &&
+ normalizedTitle !== normalizedBody &&
+ !normalizedBody.startsWith(normalizedTitle) &&
+ !normalizedTitle.startsWith(normalizedBody)
+ );
+
function handleClick() {
if (onClick) onClick(item.id);
}
@@ -72,7 +96,7 @@
{formatDate(item.createdAt)}
- {#if item.title}
+ {#if showTitle}
{item.title}
{/if}
diff --git a/services/mana-analytics/src/services/feedback.ts b/services/mana-analytics/src/services/feedback.ts
index a7f8656dc..611a93a8c 100644
--- a/services/mana-analytics/src/services/feedback.ts
+++ b/services/mana-analytics/src/services/feedback.ts
@@ -129,10 +129,19 @@ export class FeedbackService {
try {
title = await this.generateTitle(data.feedbackText);
} catch {
- title = data.feedbackText.slice(0, 80);
+ // Don't fall back to a feedbackText prefix — it just duplicates
+ // the body in the UI. Leave title null; the card renders the
+ // text on its own.
+ title = undefined;
}
}
+ // If the auto-titler returned something that's effectively the same
+ // as the body (prefix match modulo whitespace + ellipsis), drop it.
+ if (title && isRedundantTitle(title, data.feedbackText)) {
+ title = undefined;
+ }
+
const displayHash = createDisplayHash(userId, this.pseudonymSecret);
const displayName = generateDisplayName(displayHash);
@@ -141,7 +150,7 @@ export class FeedbackService {
.values({
userId,
appId: data.appId,
- title: title || data.feedbackText.slice(0, 80),
+ title: title ?? null,
feedbackText: data.feedbackText,
category: (data.category as any) || 'other',
...(typeof data.isPublic === 'boolean' ? { isPublic: data.isPublic } : {}),
@@ -742,6 +751,24 @@ export class FeedbackService {
}
}
+/**
+ * True if the title is just a truncated/prefixed version of the body
+ * — i.e. would render twice in the card. Compares whitespace-collapsed
+ * lowercase forms, ignoring trailing ellipsis on either side.
+ */
+function isRedundantTitle(title: string, body: string): boolean {
+ const norm = (s: string) =>
+ s
+ .toLowerCase()
+ .replace(/[…\.]+$/u, '')
+ .replace(/\s+/g, ' ')
+ .trim();
+ const t = norm(title);
+ const b = norm(body);
+ if (!t || !b) return false;
+ return b === t || b.startsWith(t) || t.startsWith(b);
+}
+
/** Strips userId / displayHash / deviceInfo from a row. */
function redact(
row: FeedbackRow,