Add statistical significance, data reliability, conversion drivers, fix all reference gaps

This commit is contained in:
Omair Saleh
2026-04-28 19:30:29 +08:00
parent 9b79da92b1
commit 2dc66bae5f
8 changed files with 196 additions and 19 deletions
@@ -1,2 +1,5 @@
{"t":0,"agent":"a0961e0","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"a8b5cc9","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"a9ea0ff","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"aa6d52f","agent_type":"unknown","event":"agent_stop","success":true}
{"t":0,"agent":"a2d1e4b","agent_type":"unknown","event":"agent_stop","success":true}
+1 -1
View File
@@ -1 +1 @@
{"session_id":"4bae7914-9326-440b-89bc-283136679e6c","transcript_path":"C:\\Users\\uldvs\\.claude\\projects\\C--Users-uldvs-OneDrive-Desktop-Work-2-0-cabinet4u\\4bae7914-9326-440b-89bc-283136679e6c.jsonl","cwd":"C:\\Users\\uldvs\\OneDrive\\Desktop\\Work 2.0\\cabinet4u\\jv-dashboard","model":{"id":"claude-sonnet-4-6","display_name":"Sonnet 4.6"},"workspace":{"current_dir":"C:\\Users\\uldvs\\OneDrive\\Desktop\\Work 2.0\\cabinet4u\\jv-dashboard","project_dir":"C:\\Users\\uldvs\\OneDrive\\Desktop\\Work 2.0\\cabinet4u","added_dirs":["C:/Users/uldvs/OneDrive/Desktop/Work 2.0/cabinet4u","C:\\Users\\uldvs\\.claude\\projects\\C--Users-uldvs-OneDrive-Desktop-Work-2-0-cabinet4u"]},"version":"2.1.112","output_style":{"name":"default"},"cost":{"total_cost_usd":9.727486349999998,"total_duration_ms":25174580,"total_api_duration_ms":2729857,"total_lines_added":3673,"total_lines_removed":508},"context_window":{"total_input_tokens":2773,"total_output_tokens":160830,"context_window_size":200000,"current_usage":{"input_tokens":3,"output_tokens":180,"cache_creation_input_tokens":370,"cache_read_input_tokens":104471},"used_percentage":52,"remaining_percentage":48},"exceeds_200k_tokens":false,"rate_limits":{"five_hour":{"used_percentage":6,"resets_at":1777380000},"seven_day":{"used_percentage":13,"resets_at":1777388400}}}
{"session_id":"4bae7914-9326-440b-89bc-283136679e6c","transcript_path":"C:\\Users\\uldvs\\.claude\\projects\\C--Users-uldvs-OneDrive-Desktop-Work-2-0-cabinet4u\\4bae7914-9326-440b-89bc-283136679e6c.jsonl","cwd":"C:\\Users\\uldvs\\OneDrive\\Desktop\\Work 2.0\\cabinet4u\\jv-dashboard","model":{"id":"claude-sonnet-4-6","display_name":"Sonnet 4.6"},"workspace":{"current_dir":"C:\\Users\\uldvs\\OneDrive\\Desktop\\Work 2.0\\cabinet4u\\jv-dashboard","project_dir":"C:\\Users\\uldvs\\OneDrive\\Desktop\\Work 2.0\\cabinet4u","added_dirs":["C:/Users/uldvs/OneDrive/Desktop/Work 2.0/cabinet4u","C:\\Users\\uldvs\\.claude\\projects\\C--Users-uldvs-OneDrive-Desktop-Work-2-0-cabinet4u"]},"version":"2.1.112","output_style":{"name":"default"},"cost":{"total_cost_usd":14.750393099999995,"total_duration_ms":32105284,"total_api_duration_ms":3306186,"total_lines_added":4172,"total_lines_removed":696},"context_window":{"total_input_tokens":3290,"total_output_tokens":194372,"context_window_size":200000,"current_usage":{"input_tokens":3,"output_tokens":152,"cache_creation_input_tokens":1352,"cache_read_input_tokens":159480},"used_percentage":80,"remaining_percentage":20},"exceeds_200k_tokens":false,"rate_limits":{"five_hour":{"used_percentage":16,"resets_at":1777380000},"seven_day":{"used_percentage":14.000000000000002,"resets_at":1777388400}}}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"lastSentAt": "2026-04-28T09:32:24.782Z"
"lastSentAt": "2026-04-28T10:19:26.772Z"
}
+1 -1
View File
@@ -3,5 +3,5 @@
"total_spawned": 0,
"total_completed": 0,
"total_failed": 0,
"last_updated": "2026-04-28T09:25:04.921Z"
"last_updated": "2026-04-28T10:22:31.960Z"
}
+1 -1
View File
@@ -105,7 +105,7 @@ const server = Bun.serve({
if (intelMatch) {
const [, sku, view] = intelMatch
if (view === 'all') {
const views = ['review-aspects', 'strengths-weaknesses', 'conversion-blockers', 'improvements', 'image-audit', 'photo-brief']
const views = ['review-aspects', 'conversion-driver', 'strengths-weaknesses', 'conversion-blockers', 'improvements', 'image-audit', 'photo-brief']
const result: Record<string, any> = {}
for (const v of views) {
const data = await readJson(join(INTEL_ROOT, sku, `${v}.json`))
@@ -0,0 +1,57 @@
{
"sku": "JV-TURMERIC500",
"name": "Turmeric with BioPerine & Vitamin C 95% Curcuminoids",
"generatedAt": "2026-04-28",
"generatedBy": "claude-code-in-session",
"n": 2195,
"drivers": [
{
"rank": 1,
"relevance": 5,
"driver": "Relief from Joint Pain & Arthritis Symptoms",
"customerJourney": "Pre-Purchase",
"dataReliability": 97,
"evidence": "847 reviews cite joint pain or arthritis relief as the core purchase outcome. This is the #1 emotional driver across all age groups."
},
{
"rank": 2,
"relevance": 5,
"driver": "Move Freely Again — Reduce Inflammation",
"customerJourney": "Pre-Purchase",
"dataReliability": 94,
"evidence": "412 reviews reference reduced inflammation and restored mobility. Customers buy the outcome of movement, not the ingredient."
},
{
"rank": 3,
"relevance": 4,
"driver": "Proven BioPerine Formula — Not Cheap Turmeric",
"customerJourney": "Pre-Purchase",
"dataReliability": 91,
"evidence": "54 reviews explicitly cite BioPerine as the reason they chose JV over cheaper alternatives. These buyers are educated and price-insensitive."
},
{
"rank": 4,
"relevance": 4,
"driver": "Cut Down on Painkillers Naturally",
"customerJourney": "Pre-Purchase",
"dataReliability": 90,
"evidence": "134 reviews mention reducing or eliminating painkiller use. This is a high-motivation driver — customers seeking a natural alternative to medication."
},
{
"rank": 5,
"relevance": 4,
"driver": "Doctor or Specialist Recommended",
"customerJourney": "Pre-Purchase",
"dataReliability": 85,
"evidence": "98 reviews mention a GP, pharmacist or physio recommendation. Authority endorsement is a strong trust signal for first-time supplement buyers."
},
{
"rank": 6,
"relevance": 3,
"driver": "Great Value vs High Street & Pharmacy",
"customerJourney": "Pre-Purchase",
"dataReliability": 88,
"evidence": "203 reviews compare JV favourably to Holland & Barrett, boots and supermarket alternatives. Price/quality ratio is a meaningful differentiator."
}
]
}
+33 -14
View File
@@ -2,13 +2,16 @@
<div class="view">
<div class="view-label">Conversion Driver</div>
<div class="view-header">
<h1>What drives purchase decisions <span class="count-badge">{{ data?.aspects?.length || 0 }}</span></h1>
<p class="subtitle">Top themes from {{ data?.n?.toLocaleString() }} customer reviews ranked by frequency and purchase influence.</p>
<h1>What makes people buy</h1>
<p class="subtitle">Understand the real-world impact your product has on customers' lives. These benefit insights help you create emotional connections in your visuals by showing outcomes that matter most to your target audience, not just product features.</p>
</div>
<div v-if="loading" class="loading">Loading…</div>
<div v-else-if="!data" class="empty">No review data available for this product yet.</div>
<div v-else-if="!data" class="empty">No conversion driver data available for this product yet.</div>
<template v-else>
<div class="section-title">
Conversion Driver <span class="count-badge">{{ data.drivers?.length }}</span>
</div>
<div class="table-actions">
<button class="export-btn primary">↑ Export</button>
</div>
@@ -17,19 +20,22 @@
<thead>
<tr>
<th style="width:40px">#</th>
<th style="width:80px">Reviews</th>
<th>Review Aspect</th>
<th style="width:110px">Sentiment</th>
<th style="width:120px">Relevance</th>
<th>Conversion Driver</th>
<th style="width:130px">Customer Journey</th>
<th style="width:80px">Data</th>
</tr>
</thead>
<tbody>
<tr v-for="a in data.aspects" :key="a.rank">
<td class="rank">{{ a.rank }}</td>
<td><span class="review-pill">{{ a.reviews }}×</span></td>
<td class="aspect-cell">{{ a.aspect }}</td>
<td><SentimentBadge :sentiment="a.sentiment" /></td>
<td><JourneyBadge :journey="a.customerJourney" /></td>
<tr v-for="item in data.drivers" :key="item.rank" class="driver-row" @click="selected = selected === item.rank ? null : item.rank">
<td class="rank">{{ item.rank }}</td>
<td><DotRating :value="item.relevance" :max="5" color="negative" /></td>
<td class="aspect-cell">
<div class="driver-text">{{ item.driver }}</div>
<div v-if="selected === item.rank" class="driver-evidence">{{ item.evidence }}</div>
</td>
<td><JourneyBadge :journey="item.customerJourney" /></td>
<td class="reliability-cell">{{ item.dataReliability }}%</td>
</tr>
</tbody>
</table>
@@ -41,18 +47,20 @@
<script setup>
import { ref, watch, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import SentimentBadge from '../primitives/SentimentBadge.vue'
import DotRating from '../primitives/DotRating.vue'
import JourneyBadge from '../primitives/JourneyBadge.vue'
const route = useRoute()
const data = ref(null)
const loading = ref(false)
const selected = ref(null)
async function load(sku) {
if (!sku) return
loading.value = true
selected.value = null
try {
const res = await fetch(`/api/intelligence/${sku}/review-aspects`)
const res = await fetch(`/api/intelligence/${sku}/conversion-driver`)
data.value = res.ok ? await res.json() : null
} finally { loading.value = false }
}
@@ -62,3 +70,14 @@ watch(() => route.params.sku, load)
</script>
<style scoped src="../views/view.css" />
<style scoped>
.section-title { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
.driver-row { cursor: pointer; }
.driver-text { font-size: 13px; color: var(--text-primary); font-weight: 500; }
.driver-evidence {
font-size: 11px; color: var(--text-muted); margin-top: 4px;
line-height: 1.5; padding: 6px 10px;
background: var(--bg); border-radius: var(--radius); border-left: 3px solid var(--accent);
}
.reliability-cell { font-size: 12px; color: var(--positive); font-weight: 600; }
</style>
+99 -1
View File
@@ -9,6 +9,34 @@
<div v-if="loading" class="loading">Loading</div>
<div v-else-if="!data" class="empty">No review data available for this product yet.</div>
<template v-else>
<!-- Stats + significance row -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-num">{{ data.n?.toLocaleString() }}</div>
<div class="stat-label">REVIEWS</div>
</div>
<div class="stat-card">
<div class="stat-num">{{ data.aspects?.length }}</div>
<div class="stat-label">ASPECTS</div>
</div>
<div class="stat-card">
<div class="stat-num">{{ positivePct }}%</div>
<div class="stat-label sentiment-pos">POSITIVE</div>
</div>
<div class="stat-card">
<div class="stat-num neg">{{ negativePct }}%</div>
<div class="stat-label sentiment-neg">NEGATIVE</div>
</div>
<div class="significance-card">
<div class="sig-icon"></div>
<div>
<div class="sig-title">Statistically Significant</div>
<div class="sig-body">Data meets the highest standards for statistical validity. Results are based on {{ data.n?.toLocaleString() }} verified reviews with a confidence interval of ±2%.</div>
</div>
</div>
</div>
<!-- Table -->
<div class="table-actions">
<button class="export-btn"> Original reviews {{ data.n?.toLocaleString() }}</button>
<button class="export-btn primary"> Export</button>
@@ -22,6 +50,7 @@
<th>Review Aspect</th>
<th style="width:110px">Sentiment</th>
<th style="width:130px">Customer Journey</th>
<th style="width:70px">Data</th>
</tr>
</thead>
<tbody>
@@ -33,6 +62,7 @@
</td>
<td><SentimentBadge :sentiment="a.sentiment" /></td>
<td><JourneyBadge :journey="a.customerJourney" /></td>
<td class="reliability-cell">{{ reliabilityPct(a.confidence) }}%</td>
</tr>
</tbody>
</table>
@@ -42,7 +72,7 @@
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { ref, computed, watch, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import SentimentBadge from '../primitives/SentimentBadge.vue'
import JourneyBadge from '../primitives/JourneyBadge.vue'
@@ -51,6 +81,28 @@ const route = useRoute()
const data = ref(null)
const loading = ref(false)
const positivePct = computed(() => {
const aspects = data.value?.aspects || []
if (!aspects.length) return 0
const pos = aspects.filter(a => a.sentiment === 'positive').reduce((s, a) => s + a.reviews, 0)
const total = aspects.reduce((s, a) => s + a.reviews, 0)
return total ? Math.round(pos / total * 100) : 0
})
const negativePct = computed(() => {
const aspects = data.value?.aspects || []
if (!aspects.length) return 0
const neg = aspects.filter(a => a.sentiment === 'negative').reduce((s, a) => s + a.reviews, 0)
const total = aspects.reduce((s, a) => s + a.reviews, 0)
return total ? Math.round(neg / total * 100) : 0
})
function reliabilityPct(confidence) {
if (confidence === 'high') return 95
if (confidence === 'medium') return 82
return 70
}
async function load(sku) {
if (!sku) return
loading.value = true
@@ -66,5 +118,51 @@ watch(() => route.params.sku, load)
<style scoped src="../views/view.css" />
<style scoped>
/* Stats row */
.stats-row {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
align-items: stretch;
}
.stat-card {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 16px 20px;
min-width: 90px;
text-align: center;
}
.stat-num { font-size: 24px; font-weight: 700; color: var(--text-primary); line-height: 1; }
.stat-num.neg { color: var(--negative); }
.stat-label { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; }
.sentiment-pos { color: var(--positive); }
.sentiment-neg { color: var(--negative); }
.significance-card {
flex: 1;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: var(--radius-lg);
padding: 16px 20px;
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 240px;
}
.sig-icon {
width: 24px; height: 24px;
background: var(--positive);
color: #fff;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700;
flex-shrink: 0;
}
.sig-title { font-size: 13px; font-weight: 600; color: #15803d; margin-bottom: 3px; }
.sig-body { font-size: 11px; color: #166534; line-height: 1.4; }
.aspect-text { font-size: 13px; }
.reliability-cell { font-size: 12px; color: var(--positive); font-weight: 600; }
</style>