logo/header and other changes
This commit is contained in:
parent
e3ea23c594
commit
870051839c
10 changed files with 5142 additions and 1143 deletions
615
App.tsx
615
App.tsx
|
|
@ -1,14 +1,64 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Search, FileDown, Info, LayoutGrid, Zap, Sparkles, Loader2,
|
||||
TrendingUp, TrendingDown, Minus, Target, Eye, Layers,
|
||||
Newspaper, BarChart3, Cloud, MoreHorizontal, Activity,
|
||||
Play, Users, Copy, RefreshCw, Quote, ArrowUpRight, ShieldCheck, Globe, Instagram, Video, Clapperboard, MonitorPlay, CheckCircle2
|
||||
BarChart3, Cloud, Activity, Globe, Rocket, ShieldCheck,
|
||||
Target as TargetIcon, UserCheck, MessageSquare, Copy,
|
||||
ArrowUpRight, CheckCircle2, Lightbulb, AlertTriangle, Clock,
|
||||
Users, Newspaper, Smile, Frown, Meh, Star, Compass
|
||||
} from 'lucide-react';
|
||||
import { DashboardData, SearchState, CompetitorClaim, MarketMetric } from './types';
|
||||
import { DashboardData, SearchState, CompetitorClaim, MarketMetric, WhitespaceItem, Creator, NewsSentiment, RadarMetric } from './types';
|
||||
import { fetchMarketInsights } from './geminiService';
|
||||
|
||||
const VerifiedSourceBadge: React.FC<{ source?: string; methodology?: string }> = ({ source, methodology }) => {
|
||||
if (!source) return null;
|
||||
return (
|
||||
<div className="relative group/badge inline-block">
|
||||
<div className="flex items-center gap-1.5 px-2 py-0.5 bg-green-50 text-green-700 text-[8px] font-black uppercase tracking-widest rounded-full border border-green-100 cursor-help transition-all hover:bg-green-100">
|
||||
<CheckCircle2 size={10} />
|
||||
Verified Source
|
||||
</div>
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-56 p-3 bg-black text-white text-[9px] rounded-xl opacity-0 invisible group-hover/badge:opacity-100 group-hover/badge:visible transition-all shadow-2xl z-[100] pointer-events-none border border-white/10">
|
||||
<div className="font-black text-[#FFD700] mb-1 uppercase tracking-tighter">Primary Source:</div>
|
||||
<div className="mb-2 font-medium opacity-90">{source}</div>
|
||||
<div className="font-black text-[#FFD700] mb-1 uppercase tracking-tighter">Methodology:</div>
|
||||
<div className="font-medium opacity-90 leading-tight">{methodology || "Grounded Strategic Synthesis"}</div>
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 border-8 border-transparent border-t-black"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WhitespaceCard: React.FC<{ item: WhitespaceItem }> = ({ item }) => {
|
||||
const isOpportunity = item.type === 'OPPORTUNITY';
|
||||
return (
|
||||
<div className={`p-5 rounded-[2rem] border-2 transition-all hover:scale-[1.02] h-full ${
|
||||
isOpportunity
|
||||
? 'bg-[#FFD700]/5 border-[#FFD700] text-black shadow-[0_10px_30px_-15px_rgba(255,215,0,0.2)]'
|
||||
: 'bg-gray-50 border-black/10 text-gray-500'
|
||||
}`}>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
{isOpportunity ? <Lightbulb size={16} className="text-black" /> : <AlertTriangle size={16} className="text-black/40" />}
|
||||
<span className={`text-[8px] font-black uppercase tracking-widest ${isOpportunity ? 'text-black' : 'text-black/40'}`}>
|
||||
{item.type}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="text-[11px] font-black mb-2 uppercase tracking-tight text-black line-clamp-1">{item.title}</h4>
|
||||
<p className="text-[10px] leading-relaxed font-medium opacity-80 line-clamp-3">{item.description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Logo = () => (
|
||||
<div className="relative flex items-center justify-center w-10 h-10 bg-black rounded-xl group-hover:bg-[#FFD700] transition-all duration-300 shadow-lg shrink-0">
|
||||
<Compass size={22} className="text-[#FFD700] group-hover:text-black transition-colors" />
|
||||
<div className="absolute top-1.5 right-1.5">
|
||||
<div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [search, setSearch] = useState<SearchState & { country: string }>({
|
||||
product: '',
|
||||
|
|
@ -20,13 +70,14 @@ const App: React.FC = () => {
|
|||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [hoveredClaim, setHoveredClaim] = useState<CompetitorClaim | null>(null);
|
||||
const [loadMessage, setLoadMessage] = useState('Initiating Audit...');
|
||||
const [isRefreshingBrief, setIsRefreshingBrief] = useState(false);
|
||||
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
|
||||
|
||||
const handleSearch = async (e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
if (!search.product || !search.category || search.loading) return;
|
||||
|
||||
setSearch(prev => ({ ...prev, loading: true, error: null }));
|
||||
setData(null);
|
||||
setLoadMessage('Grounded Search Active...');
|
||||
|
||||
const messages = [
|
||||
|
|
@ -45,11 +96,12 @@ const App: React.FC = () => {
|
|||
const locationContext = search.country ? `in ${search.country}` : "across the APAC region";
|
||||
const result = await fetchMarketInsights(`${search.product} ${locationContext}`, search.category);
|
||||
setData(result);
|
||||
setLastUpdated(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }));
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setSearch(prev => ({
|
||||
...prev,
|
||||
error: 'Network Busy: The AI Search service is temporarily overloaded. Please try again.'
|
||||
error: err.message || 'System busy. Please try again.'
|
||||
}));
|
||||
} finally {
|
||||
clearInterval(interval);
|
||||
|
|
@ -57,32 +109,13 @@ const App: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleRefreshBrief = async () => {
|
||||
if (!data || isRefreshingBrief) return;
|
||||
setIsRefreshingBrief(true);
|
||||
try {
|
||||
const result = await fetchMarketInsights(search.product, search.category);
|
||||
setData(prev => prev ? { ...prev, agencyBrief: result.agencyBrief, summary: result.summary, socialHighlights: result.socialHighlights } : result);
|
||||
} catch (err) {
|
||||
console.error("Failed to refresh brief", err);
|
||||
} finally {
|
||||
setIsRefreshingBrief(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyBrief = () => {
|
||||
if (!data) return;
|
||||
const text = `Objective: ${data.agencyBrief?.objective}\nAudience: ${data.agencyBrief?.targetAudience}\nMessage: ${data.agencyBrief?.keyMessage}`;
|
||||
navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
||||
const handleExportHtml = () => {
|
||||
if (!data) return;
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Oho! Report - ${search.product}</title>
|
||||
<title>APAC Strategy & Insights Engine Report - ${search.product}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
|
||||
<style>body { font-family: 'Inter', sans-serif; }</style>
|
||||
|
|
@ -96,16 +129,20 @@ const App: React.FC = () => {
|
|||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `Oho_Report_${search.product.replace(/\s+/g, '_')}.html`;
|
||||
link.download = `APAC_Strategy_Insights_Engine_${search.product.replace(/\s+/g, '_')}.html`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const dynamicQualityScore = data?.radarMetrics?.length
|
||||
? Math.round(data.radarMetrics.reduce((acc, m) => acc + m.score, 0) / data.radarMetrics.length)
|
||||
: 78;
|
||||
const getSentimentIcon = (sentiment: string) => {
|
||||
switch (sentiment) {
|
||||
case 'positive': return <Smile className="text-green-500" size={16} />;
|
||||
case 'negative': return <Frown className="text-red-500" size={16} />;
|
||||
default: return <Meh className="text-gray-400" size={16} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-black selection:bg-[#FFD700] pb-20">
|
||||
|
|
@ -125,23 +162,24 @@ const App: React.FC = () => {
|
|||
|
||||
<header className="sticky top-0 z-50 bg-white border-b-2 border-black/5 px-6 py-5">
|
||||
<div className="max-w-[1600px] mx-auto flex flex-col md:flex-row items-center gap-6">
|
||||
<div className="flex items-center gap-4 shrink-0">
|
||||
<div className="flex items-baseline gap-0.5">
|
||||
<span className="font-black tracking-tight text-3xl text-black">OHO</span>
|
||||
<span className={`font-black text-3xl text-[#FFD700] ${search.loading ? 'exclamation-active' : ''}`}>!</span>
|
||||
<div className="flex items-center gap-4 shrink-0 group cursor-pointer" onClick={() => window.location.reload()}>
|
||||
<Logo />
|
||||
<div className="flex flex-col justify-center">
|
||||
<span className="font-black tracking-tight text-[15px] leading-[1.1] text-black uppercase">APAC Strategy</span>
|
||||
<span className="font-black tracking-tight text-[15px] leading-[1.1] text-black uppercase">& Insights Engine</span>
|
||||
</div>
|
||||
<div className="h-8 w-[1px] bg-gray-200 no-print" />
|
||||
<div className="h-8 w-[1px] bg-gray-200 no-print ml-2" />
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch} className="flex-grow flex gap-3 w-full no-print">
|
||||
<input
|
||||
placeholder="Search Product"
|
||||
placeholder="Brand/Product (e.g. Dyson)"
|
||||
value={search.product}
|
||||
onChange={e => setSearch(p => ({...p, product: e.target.value}))}
|
||||
className="flex-grow px-4 py-3 bg-gray-50 border-2 border-gray-100 rounded-xl focus:border-black outline-none text-sm font-bold transition-all"
|
||||
/>
|
||||
<input
|
||||
placeholder="Search Category"
|
||||
placeholder="Category (e.g. Hair Care)"
|
||||
value={search.category}
|
||||
onChange={e => setSearch(p => ({...p, category: e.target.value}))}
|
||||
className="flex-grow px-4 py-3 bg-gray-50 border-2 border-gray-100 rounded-xl focus:border-black outline-none text-sm font-bold transition-all"
|
||||
|
|
@ -149,7 +187,7 @@ const App: React.FC = () => {
|
|||
<div className="relative flex-grow max-w-[200px]">
|
||||
<Globe className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={16} />
|
||||
<input
|
||||
placeholder="Country (Optional)"
|
||||
placeholder="Market"
|
||||
value={search.country}
|
||||
onChange={e => setSearch(p => ({...p, country: e.target.value}))}
|
||||
className="w-full pl-10 pr-4 py-3 bg-gray-50 border-2 border-gray-100 rounded-xl focus:border-black outline-none text-sm font-bold transition-all"
|
||||
|
|
@ -166,43 +204,85 @@ const App: React.FC = () => {
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-[1600px] mx-auto px-6 py-8 space-y-10">
|
||||
<main className="max-w-[1600px] mx-auto px-6 py-8 space-y-16">
|
||||
{search.error && (
|
||||
<div className="bg-amber-50 text-amber-900 p-5 rounded-2xl font-black text-center border-2 border-amber-200">{search.error}</div>
|
||||
<div className="bg-amber-50 text-amber-900 p-8 rounded-2xl font-black text-center border-2 border-amber-200 animate-in fade-in slide-in-from-top-4">
|
||||
<p className="mb-4">{search.error}</p>
|
||||
<button onClick={() => handleSearch()} className="px-6 py-2 bg-black text-white rounded-lg text-xs uppercase tracking-widest">Retry Audit</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{search.loading && !data && (
|
||||
<div className="flex flex-col items-center justify-center py-48 animate-in fade-in duration-700">
|
||||
<div className="relative mb-8">
|
||||
<div className="text-6xl font-black text-black">OHO<span className="text-[#FFD700] exclamation-active">!</span></div>
|
||||
<div className="relative mb-8 group">
|
||||
<Logo />
|
||||
</div>
|
||||
<h2 className="text-2xl font-black uppercase tracking-[0.4em]">{loadMessage}</h2>
|
||||
<h2 className="text-2xl font-black uppercase tracking-[0.4em] text-center">{loadMessage}</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data && (
|
||||
<>
|
||||
{/* Metric Strip - Styled like Cultural Nuance Boxes */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{data.marketMetrics && [data.marketMetrics.roas, data.marketMetrics.engagement, data.marketMetrics.fatigue, data.marketMetrics.sov].map((m, i) => m && (
|
||||
<div key={i} className="bg-white border border-gray-200 rounded-3xl overflow-hidden flex flex-col hover:shadow-lg transition-all border-t-8 border-t-[#FFD700] group min-h-[160px]">
|
||||
<div className="p-6 flex-grow flex flex-col">
|
||||
<span className="text-[10px] font-black text-gray-400 group-hover:text-black uppercase tracking-widest mb-3 block">{m.label}</span>
|
||||
<div className="text-3xl font-black flex items-center justify-between mb-3">
|
||||
{m.value}
|
||||
{m.trend === 'up' ? <TrendingUp size={20} className="text-green-500" /> : <TrendingDown size={20} className="text-red-500" />}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500 font-bold leading-relaxed">{m.explanation}</p>
|
||||
</div>
|
||||
{/* 1. Header: Status & Metrics */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-500">Live Market Pulse</span>
|
||||
</div>
|
||||
))}
|
||||
{lastUpdated && (
|
||||
<div className="flex items-center gap-2 text-[10px] font-black text-gray-300 uppercase tracking-widest">
|
||||
<Clock size={12} />
|
||||
Audit Sync: {lastUpdated}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Metric Strip */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
{(Object.values(data.marketMetrics) as MarketMetric[]).map((m, i) => m && (
|
||||
<div key={i} className="bg-white border border-gray-200 rounded-3xl flex flex-col hover:shadow-lg transition-all border-t-8 border-t-[#FFD700] group min-h-[160px] relative">
|
||||
<div className="p-6 flex-grow flex flex-col">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<span className="text-[10px] font-black text-gray-400 group-hover:text-black uppercase tracking-widest block">{m.label}</span>
|
||||
<VerifiedSourceBadge source={m.source} methodology={m.methodology} />
|
||||
</div>
|
||||
<div className="text-3xl font-black flex items-center justify-between mb-3">
|
||||
{m.value}
|
||||
{m.trend === 'up' ? <TrendingUp size={20} className="text-green-500" /> : <TrendingDown size={20} className="text-red-500" />}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500 font-bold leading-relaxed">{m.explanation}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cultural Velocity & Share of Search */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<section className="bg-white border-2 border-black rounded-[2rem] p-8 group hover:border-[#FFD700] transition-all">
|
||||
{/* 2. SOS & Velocity Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<section className="bg-white border-2 border-black rounded-[2.5rem] p-8 group hover:border-[#FFD700] transition-all h-full overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-8 border-b border-gray-100 pb-4">
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">Share of Search</h2>
|
||||
<BarChart3 size={16} className="text-black group-hover:text-[#FFD700]" />
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{data.shareOfSearch?.map((item, i) => (
|
||||
<div key={i}>
|
||||
<div className="flex justify-between text-[11px] font-black mb-2">
|
||||
<span className="uppercase">{item.competitor}</span>
|
||||
<span className="bg-black text-white px-2 py-0.5 rounded text-[8px] group-hover:bg-[#FFD700] group-hover:text-black transition-colors">{item.percentage}%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-gray-100 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-black group-hover:bg-[#FFD700] transition-all" style={{ width: `${item.percentage}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-white border-2 border-black rounded-[2.5rem] p-8 group hover:border-[#FFD700] transition-all h-full overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-10 border-b border-gray-100 pb-4">
|
||||
<h2 className="text-xs font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">Cultural Velocity</h2>
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">Cultural Velocity</h2>
|
||||
<Activity size={18} className="text-black group-hover:text-[#FFD700]" />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
|
|
@ -219,263 +299,268 @@ const App: React.FC = () => {
|
|||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-white border-2 border-black rounded-[2rem] p-8 group hover:border-[#FFD700] transition-all">
|
||||
<div className="flex justify-between items-center mb-10 border-b border-gray-100 pb-4">
|
||||
<h2 className="text-xs font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">Share of Search</h2>
|
||||
<BarChart3 size={18} className="text-black group-hover:text-[#FFD700]" />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{data.shareOfSearch?.map((item, i) => (
|
||||
<div key={i}>
|
||||
<div className="flex justify-between text-[11px] font-black mb-3">
|
||||
<span className="uppercase">{item.competitor}</span>
|
||||
<span className="bg-black text-white px-2 py-0.5 rounded text-[9px] group-hover:bg-[#FFD700] group-hover:text-black transition-colors">{item.percentage}%</span>
|
||||
</div>
|
||||
<div className="h-3 w-full bg-gray-100 rounded-sm overflow-hidden">
|
||||
<div className="h-full bg-black group-hover:bg-[#FFD700] transition-all" style={{ width: `${item.percentage}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Inverted Claim Density Word Cloud */}
|
||||
<section className="bg-black text-white border-2 border-black rounded-[2rem] p-8 relative overflow-hidden flex flex-col items-center">
|
||||
<div className="w-full flex items-center justify-between mb-6 border-b border-white/10 pb-4">
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-500">Claim Density</h2>
|
||||
<Cloud size={16} className="text-[#FFD700]" />
|
||||
<section className="space-y-8">
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<Cloud size={18} className="text-[#FFD700]" />
|
||||
<h2 className="text-[11px] font-black uppercase tracking-[0.4em] text-gray-400">Grounded Competitor Claim Density</h2>
|
||||
</div>
|
||||
<div className="w-full flex flex-wrap gap-4 justify-center items-center py-4 min-h-[80px]">
|
||||
{data.competitorClaims?.map((claim, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onMouseEnter={() => setHoveredClaim(claim)}
|
||||
onMouseLeave={() => setHoveredClaim(null)}
|
||||
className="font-black hover:text-[#FFD700] hover:scale-105 transition-all uppercase tracking-tighter outline-none leading-none"
|
||||
style={{ fontSize: `${9 + (claim.frequency * 0.4)}px`, opacity: 0.5 + (claim.frequency / 40) }}
|
||||
>
|
||||
{claim.keyword}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full mt-4 pt-4 border-t border-white/5 h-[60px] flex items-center justify-center">
|
||||
{hoveredClaim ? (
|
||||
<div className="text-center animate-in fade-in slide-in-from-bottom-1 duration-200">
|
||||
<span className="text-[9px] font-black text-[#FFD700] mr-2 uppercase">{hoveredClaim.keyword}:</span>
|
||||
<span className="text-[10px] font-bold italic text-gray-400 leading-tight">"{hoveredClaim.explanation}"</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[9px] font-black text-gray-600 uppercase tracking-widest italic">Hover over keywords for market insights</span>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Efficacy Benchmark & Creators */}
|
||||
<div className="grid grid-cols-12 gap-8 items-stretch">
|
||||
<section className="col-span-12 lg:col-span-8 bg-gray-50 border-2 border-gray-200 rounded-[2.5rem] p-10 flex flex-col md:flex-row gap-10 items-center relative group/bench">
|
||||
<div className="absolute top-4 right-10 opacity-0 group-hover/bench:opacity-100 transition-opacity bg-black text-white text-[9px] font-bold p-3 rounded-xl border border-[#FFD700] w-48 z-10 pointer-events-none">
|
||||
The Efficacy Benchmark aggregates content resonance, sentiment alignment, and conversion velocity against category leaders.
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6">
|
||||
<div className="inline-block px-3 py-1 bg-black text-[#FFD700] text-[9px] font-black uppercase tracking-[0.2em] rounded flex items-center gap-2">
|
||||
<Target size={10} /> Efficacy Benchmark
|
||||
</div>
|
||||
<h2 className="text-2xl font-black uppercase leading-none">Content Index</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{data.radarMetrics?.slice(0, 4).map((rm, i) => (
|
||||
<div key={i} className="border-l-4 border-[#FFD700] pl-4">
|
||||
<div className="text-[9px] font-black text-gray-400 uppercase">{rm.label}</div>
|
||||
<div className="text-lg font-black">{rm.score}%</div>
|
||||
</div>
|
||||
<div className="bg-black border-2 border-[#FFD700]/20 rounded-[3rem] p-10 relative overflow-hidden shadow-2xl">
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-4 justify-center items-center py-4 relative z-10">
|
||||
{data.competitorClaims.map((claim, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onMouseEnter={() => setHoveredClaim(claim)}
|
||||
onMouseLeave={() => setHoveredClaim(null)}
|
||||
className="font-black text-white/80 hover:text-[#FFD700] transition-all uppercase tracking-tighter leading-none whitespace-nowrap"
|
||||
style={{ fontSize: `${16 + (claim.frequency * 0.4)}px`, opacity: 0.6 + (claim.frequency / 70) }}
|
||||
>
|
||||
{claim.keyword}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{hoveredClaim && (
|
||||
<div className="mt-8 bg-white/5 backdrop-blur-md p-6 rounded-[2rem] border-2 border-[#FFD700]/30 animate-in fade-in slide-in-from-top-2 shadow-2xl text-white">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[10px] font-black text-black bg-[#FFD700] px-2 py-0.5 rounded uppercase">{hoveredClaim.keyword}</span>
|
||||
<VerifiedSourceBadge source={hoveredClaim.source} methodology={hoveredClaim.methodology} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[12px] font-bold text-gray-300 italic">"{hoveredClaim.explanation}"</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 4. Benchmarking Row: Efficacy Benchmark & High-Impact Creators */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<section className="bg-white border-2 border-black rounded-[2.5rem] p-8 group hover:border-[#FFD700] transition-all h-full overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-8 border-b border-gray-100 pb-4">
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">Efficacy Benchmark</h2>
|
||||
<Target size={16} className="text-black group-hover:text-[#FFD700]" />
|
||||
</div>
|
||||
<div className="relative w-48 h-48 border-[10px] border-white bg-white rounded-full flex items-center justify-center shadow-xl">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-black">{dynamicQualityScore}<span className="text-[#FFD700]">.</span></div>
|
||||
<div className="text-[9px] font-black uppercase text-gray-400">Quality</div>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{data.radarMetrics?.map((metric, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="flex justify-between text-[11px] font-black uppercase">
|
||||
<span>{metric.label}</span>
|
||||
<span>{metric.score}/100</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full bg-gray-100 rounded-full overflow-hidden relative">
|
||||
<div className="absolute top-0 left-0 h-full bg-black group-hover:bg-[#FFD700] transition-all" style={{ width: `${metric.score}%` }} />
|
||||
<div className="absolute top-0 h-full w-[2px] bg-[#FFD700]" style={{ left: `${metric.benchmark}%` }} />
|
||||
</div>
|
||||
<div className="text-[8px] font-black text-gray-400 uppercase tracking-widest">Benchmark: {metric.benchmark}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="col-span-12 lg:col-span-4 bg-white border-2 border-black rounded-[2.5rem] p-10">
|
||||
<div className="flex items-center justify-between mb-8 border-b border-gray-100 pb-4">
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400">Top Creators</h2>
|
||||
<Users size={16} className="text-[#FFD700]" />
|
||||
<section className="bg-white border-2 border-black rounded-[2.5rem] p-8 group hover:border-[#FFD700] transition-all h-full overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-8 border-b border-gray-100 pb-4">
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">High-Impact Creators</h2>
|
||||
<Users size={16} className="text-black group-hover:text-[#FFD700]" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{data.topCreators?.slice(0, 4).map((cr, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-4 bg-gray-50 rounded-2xl border-2 border-transparent hover:border-[#FFD700] transition-all group shadow-sm">
|
||||
<div>
|
||||
<div className="font-black text-xs uppercase tracking-tight">{cr.name}</div>
|
||||
<div className="text-[9px] text-gray-400 font-bold uppercase">{cr.handle}</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{data.topCreators?.slice(0, 3).map((creator, i) => (
|
||||
<div key={i} className="flex items-center gap-4 p-4 bg-gray-50 rounded-2xl border-2 border-transparent hover:border-[#FFD700] transition-all">
|
||||
<div className="w-12 h-12 bg-black text-[#FFD700] rounded-full flex items-center justify-center font-black text-lg group-hover:bg-[#FFD700] group-hover:text-black transition-colors">
|
||||
{creator.name.charAt(0)}
|
||||
</div>
|
||||
<ArrowUpRight size={14} className="text-gray-300 group-hover:text-black" />
|
||||
<div className="flex-grow">
|
||||
<div className="text-[11px] font-black uppercase">{creator.name} <span className="text-gray-400 ml-1">@{creator.handle}</span></div>
|
||||
<div className="text-[9px] font-bold text-gray-500 uppercase tracking-widest">{creator.category}</div>
|
||||
<div className="text-[10px] font-medium text-black mt-1 italic">"{creator.impact}"</div>
|
||||
</div>
|
||||
<Star size={14} className="text-[#FFD700]" fill="#FFD700" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Executive Intelligence + Brief */}
|
||||
<section className="bg-black text-white rounded-[3rem] overflow-hidden border-4 border-black shadow-2xl">
|
||||
<div className="bg-[#FFD700] text-black px-12 py-8 flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<ShieldCheck size={28} />
|
||||
<h2 className="text-2xl font-black uppercase tracking-tighter">Executive Intelligence + Brief</h2>
|
||||
</div>
|
||||
<div className="flex gap-3 no-print">
|
||||
<button
|
||||
onClick={handleRefreshBrief}
|
||||
disabled={isRefreshingBrief}
|
||||
className="p-3 bg-black text-[#FFD700] rounded-xl flex items-center gap-2 text-[10px] font-black uppercase transition-all hover:scale-105 active:scale-95"
|
||||
>
|
||||
{isRefreshingBrief ? <Loader2 size={16} className="animate-spin" /> : <RefreshCw size={16} />}
|
||||
Refresh Ideas
|
||||
</button>
|
||||
<button onClick={copyBrief} className="p-3 bg-black/10 rounded-xl hover:bg-black/20 transition-all">
|
||||
<Copy size={20} />
|
||||
</button>
|
||||
</div>
|
||||
{/* 5. Whitespace Analysis Row */}
|
||||
<section className="space-y-8">
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<TargetIcon size={18} className="text-[#FFD700]" />
|
||||
<h2 className="text-[11px] font-black uppercase tracking-[0.4em] text-gray-400">Strategic Whitespace Analysis</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-12 grid grid-cols-1 lg:grid-cols-12 gap-12">
|
||||
<div className="lg:col-span-8 space-y-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{data.whitespaceAnalysis?.map((item, i) => (
|
||||
<WhitespaceCard key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 6. Intelligence Core */}
|
||||
<section className="bg-black text-white border-2 border-black rounded-[3rem] p-10 lg:p-16 relative overflow-hidden shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-[#FFD700]/5 blur-[120px] rounded-full -mr-32 -mt-32" />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20">
|
||||
<div className="space-y-12">
|
||||
<div>
|
||||
<h3 className="text-[11px] font-black text-[#FFD700] uppercase tracking-[0.5em] mb-4">Strategic Overview</h3>
|
||||
<p className="text-2xl font-bold leading-tight text-white border-l-4 border-[#FFD700] pl-6 italic">{data.summary?.overview}</p>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<ShieldCheck size={20} className="text-[#FFD700]" />
|
||||
<h2 className="text-[10px] font-black tracking-[0.4em] uppercase text-gray-400">Executive Intelligence</h2>
|
||||
</div>
|
||||
<p className="text-xl font-bold tracking-tight leading-relaxed text-gray-300 italic border-l-2 border-[#FFD700]/30 pl-8">
|
||||
{data.summary.overview}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-[11px] font-black text-[#FFD700] uppercase tracking-[0.4em] mb-2">Core Objective</h3>
|
||||
<p className="text-lg font-black">{data.agencyBrief?.objective}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[11px] font-black text-[#FFD700] uppercase tracking-[0.4em] mb-2">Target Demographic</h3>
|
||||
<p className="text-md font-bold text-gray-400">{data.agencyBrief?.targetAudience}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 border border-white/10 p-8 rounded-[2rem]">
|
||||
<h3 className="text-[10px] font-black text-[#FFD700] uppercase tracking-[0.4em] mb-4">Market Proposition</h3>
|
||||
<p className="text-xl font-black text-white tracking-tight leading-snug">{data.agencyBrief?.keyMessage}</p>
|
||||
<div className="mt-6 flex flex-wrap gap-2">
|
||||
{data.agencyBrief?.creativeHooks?.map((hook, i) => (
|
||||
<span key={i} className="px-3 py-1.5 bg-white/10 text-white text-[9px] font-black uppercase rounded flex items-center gap-2">
|
||||
<Zap size={10} fill="#FFD700" className="text-[#FFD700]" /> {hook}
|
||||
<div className="space-y-8">
|
||||
<h3 className="text-[10px] font-black text-[#FFD700] uppercase tracking-[0.4em] border-b border-[#FFD700]/10 pb-4 flex items-center gap-2">
|
||||
<Zap size={14} fill="#FFD700" /> Strategic Takeaways
|
||||
</h3>
|
||||
<ul className="space-y-6">
|
||||
{data.summary.strategicTakeaways.map((point, idx) => (
|
||||
<li key={idx} className="flex gap-6 items-start group">
|
||||
<span className="text-[#FFD700] font-black bg-[#FFD700]/10 w-8 h-8 rounded-xl flex items-center justify-center flex-shrink-0 text-xs border border-[#FFD700]/20">
|
||||
{idx + 1}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm font-medium leading-relaxed text-gray-300 group-hover:text-white transition-colors">{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 border-l border-white/10 lg:pl-12">
|
||||
<h3 className="text-[11px] font-black text-[#FFD700] uppercase tracking-[0.4em] mb-8 flex items-center gap-3">
|
||||
<Clapperboard size={18} /> Tactical Options
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{data.agencyBrief?.tacticalContent?.map((tactical, i) => (
|
||||
<div key={i} className="bg-white/5 border border-white/10 p-5 rounded-2xl group hover:bg-white/10 transition-all">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-[10px] font-black text-[#FFD700] uppercase tracking-widest">{tactical.channel}</span>
|
||||
<MonitorPlay size={14} className="text-gray-500 group-hover:text-[#FFD700]" />
|
||||
</div>
|
||||
<h4 className="text-xs font-black uppercase mb-1 tracking-tight">{tactical.format}</h4>
|
||||
<p className="text-[10px] text-gray-400 leading-relaxed font-medium">{tactical.description}</p>
|
||||
</div>
|
||||
<div className="space-y-12 bg-white/5 p-10 rounded-[2.5rem] border border-white/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Rocket size={20} className="text-[#FFD700]" />
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.4em] text-gray-400">Tactical Brief</h2>
|
||||
</div>
|
||||
<button className="text-white/40 hover:text-[#FFD700] transition-colors no-print">
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="flex gap-6 items-start">
|
||||
<TargetIcon className="text-[#FFD700] shrink-0 mt-1" size={20} />
|
||||
<div>
|
||||
<div className="text-[10px] font-black uppercase text-gray-500 mb-1 tracking-widest">Core Objective</div>
|
||||
<p className="text-sm font-bold text-white">{data.agencyBrief.objective}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-6 items-start">
|
||||
<UserCheck className="text-[#FFD700] shrink-0 mt-1" size={20} />
|
||||
<div>
|
||||
<div className="text-[10px] font-black uppercase text-gray-500 mb-1 tracking-widest">Target Persona</div>
|
||||
<p className="text-sm font-bold text-white">{data.agencyBrief.targetAudience}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8 pt-8 border-t border-white/10">
|
||||
<div className="text-[10px] font-black uppercase tracking-[0.4em] text-[#FFD700] flex items-center gap-2">
|
||||
<Sparkles size={14} fill="#FFD700" /> Actionable Strategies
|
||||
</div>
|
||||
<ul className="grid grid-cols-1 gap-4">
|
||||
{data.agencyBrief.creativeHooks.map((hook, i) => (
|
||||
<li key={i} className="group flex gap-4 items-start bg-white/5 p-4 rounded-2xl border border-transparent hover:border-[#FFD700]/30 transition-all">
|
||||
<span className="text-[#FFD700] font-black text-lg leading-none">/</span>
|
||||
<p className="text-xs font-bold leading-relaxed text-gray-300 group-hover:text-white transition-colors">{hook}</p>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Cultural Nuance Section */}
|
||||
<section className="space-y-6">
|
||||
{/* 7. Cultural Nuance Section (Always 4 items requested via service) */}
|
||||
<section className="space-y-8">
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<Globe size={18} className="text-[#FFD700]" />
|
||||
<h2 className="text-[11px] font-black uppercase tracking-[0.4em] text-gray-400">Cultural Nuance</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{data.culturalNuances?.map((nuance, i) => (
|
||||
<div key={i} className="bg-white border border-gray-200 rounded-3xl overflow-hidden flex flex-col hover:shadow-lg transition-all border-t-8 border-t-[#FFD700]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{data.culturalNuances?.slice(0, 4).map((nuance, i) => (
|
||||
<div key={i} className="bg-white border border-gray-200 rounded-3xl flex flex-col hover:shadow-lg transition-all border-t-8 border-t-[#FFD700] relative">
|
||||
<div className="p-8 flex-grow flex flex-col space-y-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<h3 className="text-2xl font-black tracking-tight text-black">{nuance.term}</h3>
|
||||
<a
|
||||
href={nuance.sourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 bg-green-50 text-green-700 text-[9px] font-bold uppercase tracking-widest rounded-full hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<CheckCircle2 size={10} />
|
||||
Verified
|
||||
</a>
|
||||
<h3 className="text-xl font-black tracking-tight text-black uppercase">{nuance.term}</h3>
|
||||
<VerifiedSourceBadge source={nuance.sourceTitle} methodology="Verified Regional Anthro-Data" />
|
||||
</div>
|
||||
|
||||
<p className="text-sm font-medium text-gray-600 leading-relaxed">
|
||||
{nuance.insight}
|
||||
</p>
|
||||
|
||||
<div className="bg-[#FFD700] p-4 rounded-2xl mt-auto">
|
||||
<div className="text-[9px] font-black uppercase tracking-[0.1em] text-black/50 mb-1">Oho! Tip</div>
|
||||
<p className="text-[11px] font-black text-black leading-snug">
|
||||
{nuance.strategyTip}
|
||||
</p>
|
||||
<p className="text-[11px] font-bold text-gray-500 leading-relaxed italic border-l-2 border-gray-100 pl-4">{nuance.insight}</p>
|
||||
<div className="bg-black text-white p-5 rounded-2xl mt-auto border-l-4 border-[#FFD700]">
|
||||
<div className="text-[8px] font-black uppercase tracking-[0.2em] text-[#FFD700] mb-2">Strategic Pivot</div>
|
||||
<p className="text-[10px] font-black leading-snug">{nuance.strategyTip}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex justify-end">
|
||||
<a href={nuance.sourceUrl} target="_blank" rel="noopener noreferrer" className="text-[9px] font-black uppercase text-gray-400 hover:text-black flex items-center gap-1">
|
||||
Source: {nuance.sourceTitle} <ArrowUpRight size={10} />
|
||||
Full Source <ArrowUpRight size={10} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 8. Industry Pulse Section (Moved to End) */}
|
||||
<section className="space-y-8">
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<Newspaper size={18} className="text-[#FFD700]" />
|
||||
<h2 className="text-[11px] font-black uppercase tracking-[0.4em] text-gray-400">Industry Pulse</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{data.newsFeed?.map((news, i) => (
|
||||
<div key={i} className="bg-white border border-gray-100 rounded-3xl p-6 hover:shadow-xl transition-all group flex flex-col min-h-[180px]">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="text-[9px] font-black text-gray-400 uppercase tracking-widest truncate max-w-[120px]">{news.source}</div>
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-full">
|
||||
{getSentimentIcon(news.sentiment)}
|
||||
<span className="text-[10px] font-black">{news.score}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xs font-black mb-4 leading-tight group-hover:text-[#FFD700] transition-colors line-clamp-3">"{news.headline}"</h3>
|
||||
<div className="mt-auto flex justify-end">
|
||||
<ArrowUpRight size={14} className="text-gray-300" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 9. Audit Trail & Reference Hub (Bottom) */}
|
||||
<footer className="pt-20 pb-20 border-t-2 border-gray-100">
|
||||
<div className="bg-gray-50 p-12 rounded-[3.5rem] border-2 border-gray-100 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-80 h-80 bg-black/5 rounded-full blur-[100px] -mr-40 -mt-40" />
|
||||
<h3 className="text-[11px] font-black text-gray-400 uppercase tracking-[0.4em] mb-12 flex items-center gap-3">
|
||||
<Layers size={16} /> Grounded Audit Trail & Reference Hub
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{data.summary?.sources?.map((source, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between p-6 bg-white rounded-[1.5rem] text-[10px] font-black hover:bg-black hover:text-white transition-all group shadow-sm border border-gray-100 uppercase tracking-tighter hover:scale-[1.02]"
|
||||
>
|
||||
<span className="truncate max-w-[220px]">{source.title}</span>
|
||||
<ArrowUpRight size={16} className="text-[#FFD700] group-hover:translate-x-1 transition-transform" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-12 pt-8 border-t border-gray-200/50 text-center">
|
||||
<p className="text-[9px] font-black text-gray-300 uppercase tracking-[0.3em]">System processed via Gemini-3 High-Density Reasoning Platform</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* FOOTER: Audited Sources */}
|
||||
{data && (
|
||||
<footer className="max-w-[1600px] mx-auto px-6 mt-10">
|
||||
<div className="bg-gray-50 p-8 rounded-[2.5rem] border-2 border-gray-100">
|
||||
<h3 className="text-[11px] font-black text-gray-400 uppercase tracking-[0.4em] mb-6 flex items-center gap-2">
|
||||
<Layers size={14} /> Audited Records & Sources
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{data.summary?.sources?.map((source, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between p-4 bg-white rounded-xl text-[10px] font-black hover:bg-black hover:text-white transition-all group shadow-sm border border-transparent"
|
||||
>
|
||||
<span className="truncate uppercase tracking-tight">{source.title}</span>
|
||||
<ArrowUpRight size={14} className="text-[#FFD700]" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
|
||||
{/* Floating Controls */}
|
||||
{/* Persistent Controls */}
|
||||
<div className="fixed bottom-10 left-10 group z-50 no-print">
|
||||
<div className="bg-black text-white p-4 rounded-xl cursor-help hover:bg-[#FFD700] hover:text-black transition-colors shadow-2xl border border-white/10">
|
||||
<Info size={24} />
|
||||
</div>
|
||||
<div className="absolute bottom-full mb-6 left-0 w-80 bg-white border-4 border-black p-8 rounded-3xl shadow-2xl invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all">
|
||||
<h4 className="text-sm font-black uppercase tracking-widest mb-4 border-b-2 border-[#FFD700] pb-2 inline-block">Real-Time Auditing</h4>
|
||||
<div className="absolute bottom-full mb-6 left-0 w-80 bg-white border-4 border-black p-8 rounded-3xl shadow-2xl invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all z-[200]">
|
||||
<h4 className="text-sm font-black uppercase tracking-widest mb-4 border-b-2 border-[#FFD700] pb-2 inline-block">Grounded Intelligence</h4>
|
||||
<p className="text-[12px] leading-relaxed text-gray-600 font-bold">
|
||||
Oho! synthesizes real-time market insights grounded in live global search data via Gemini-3-Flash.
|
||||
The APAC Strategy & Insights Engine utilizes Gemini-3-Pro with Google Search grounding to synthesize real-time audits. Every metric is backed by live market data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -500,8 +585,8 @@ const App: React.FC = () => {
|
|||
)}
|
||||
|
||||
{/* Side Accents */}
|
||||
<div className="fixed top-0 left-0 w-2 h-full bg-[#FFD700] pointer-events-none opacity-20 z-10 no-print" />
|
||||
<div className="fixed top-0 right-0 w-2 h-full bg-black pointer-events-none opacity-5 z-10 no-print" />
|
||||
<div className="fixed top-0 left-0 w-1.5 h-full bg-[#FFD700] pointer-events-none opacity-10 z-10 no-print" />
|
||||
<div className="fixed top-0 right-0 w-1.5 h-full bg-black pointer-events-none opacity-5 z-10 no-print" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,62 +1,3 @@
|
|||
|
||||
import React from 'react';
|
||||
import { ExternalLink, CheckCircle2 } from 'lucide-react';
|
||||
import { SocialContent } from '../types';
|
||||
|
||||
interface InsightCardProps {
|
||||
insight: SocialContent;
|
||||
// Adding index for unique image generation
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const InsightCard: React.FC<InsightCardProps> = ({ insight, index }) => {
|
||||
// Use platform or headline for image placeholder
|
||||
const imageSeed = insight.platform.toLowerCase();
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-100 rounded-[2rem] p-8 transition-all duration-300 hover:shadow-xl hover:border-[#FFD700]/30 flex flex-col h-full group">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<span className="flex items-center gap-1.5 px-3 py-1 bg-green-50 text-green-700 text-[10px] font-bold uppercase tracking-widest rounded-full">
|
||||
<CheckCircle2 size={12} />
|
||||
Verified Source
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<span className="text-[10px] text-gray-400 uppercase tracking-tighter">
|
||||
#{insight.platform}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="aspect-[4/3] w-full bg-gray-50 rounded-2xl mb-6 overflow-hidden relative">
|
||||
<img
|
||||
src={`https://loremflickr.com/800/600/${imageSeed},minimalist?lock=${index}`}
|
||||
alt={insight.headline}
|
||||
className="object-cover w-full h-full opacity-90 group-hover:opacity-100 transition-opacity grayscale hover:grayscale-0 duration-500"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 right-4 bg-white/90 backdrop-blur-sm p-4 rounded-xl text-[10px] font-medium leading-relaxed border border-gray-100">
|
||||
<span className="block font-bold text-gray-400 mb-1 uppercase tracking-widest">Platform:</span>
|
||||
{insight.platform} content strategy
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold mb-3 tracking-tight">{insight.headline}</h3>
|
||||
<p className="text-gray-500 text-[13px] leading-relaxed mb-8 flex-grow">
|
||||
Key social performance indicator: {insight.metrics}
|
||||
</p>
|
||||
|
||||
<div className="mt-auto space-y-4">
|
||||
<div className="flex items-center justify-between py-4 border-y border-gray-50">
|
||||
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">KPI Engagement</span>
|
||||
<span className="text-xs font-bold text-black bg-[#FFD700]/10 px-2 py-1 rounded">{insight.metrics}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="flex items-center justify-between w-full p-4 rounded-2xl border border-gray-100 text-[11px] font-bold hover:border-[#FFD700] hover:bg-[#FFD700]/5 transition-all group/link"
|
||||
>
|
||||
<span className="truncate max-w-[180px]">View Analytics</span>
|
||||
<ExternalLink size={14} className="text-gray-300 group-hover/link:text-black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// This component has been deprecated and removed from the active dashboard layout.
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -1,76 +1,3 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Summary } from '../types';
|
||||
import { ShieldCheck, Info, ArrowUpRight, Target } from 'lucide-react';
|
||||
|
||||
interface StrategicSummaryProps {
|
||||
summary: Summary | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const StrategicSummary: React.FC<StrategicSummaryProps> = ({ summary, loading }) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<aside className="w-full bg-gray-50/50 rounded-[2.5rem] animate-pulse border-2 border-gray-100 p-10 h-[500px]" />
|
||||
);
|
||||
}
|
||||
|
||||
if (!summary) {
|
||||
return (
|
||||
<aside className="w-full h-96 border-4 border-dashed border-gray-50 rounded-[2.5rem] flex flex-col items-center justify-center p-12 text-center text-gray-200">
|
||||
<Target size={48} className="mb-6 opacity-10" />
|
||||
<p className="text-xs font-black uppercase tracking-[0.3em] leading-relaxed">Awaiting parameters to generate APAC regional strategy summary.</p>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="w-full flex flex-col gap-6">
|
||||
{/* High-End Summary Section */}
|
||||
<div className="bg-black text-white p-10 rounded-[2.5rem] shadow-2xl relative overflow-hidden border-t-4 border-[#FFD700]">
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-[#FFD700]/10 blur-[100px] rounded-full -mr-16 -mt-16" />
|
||||
<div className="flex items-center gap-3 mb-10">
|
||||
<ShieldCheck size={20} className="text-[#FFD700]" />
|
||||
<h2 className="text-xs font-black tracking-[0.3em] uppercase">Executive Intelligence</h2>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-400 text-sm leading-relaxed mb-10 font-medium italic border-l-2 border-[#FFD700]/30 pl-6">
|
||||
{summary.overview || "Strategic overview analysis processed for APAC region."}
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<h3 className="text-[10px] font-black text-[#FFD700] uppercase tracking-[0.4em] border-b border-[#FFD700]/10 pb-3">Strategic Levers</h3>
|
||||
<ul className="space-y-6">
|
||||
{(summary.strategicTakeaways || []).map((point, idx) => (
|
||||
<li key={idx} className="flex gap-5 text-xs leading-relaxed items-start group">
|
||||
<span className="text-[#FFD700] font-black bg-[#FFD700]/10 w-6 h-6 rounded-lg flex items-center justify-center flex-shrink-0 text-[10px]">
|
||||
0{idx + 1}
|
||||
</span>
|
||||
<span className="group-hover:text-white transition-colors font-medium text-gray-300">{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sources */}
|
||||
<div className="bg-gray-50 border-2 border-gray-100 p-10 rounded-[2.5rem]">
|
||||
<h3 className="text-[10px] font-black text-gray-300 uppercase tracking-[0.4em] mb-8">Audited Data Sources</h3>
|
||||
<div className="space-y-4">
|
||||
{(summary.sources || []).map((source, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between p-4 bg-white rounded-2xl text-[10px] font-black hover:bg-black hover:text-white transition-all group shadow-sm border border-gray-100 uppercase tracking-tight"
|
||||
>
|
||||
<span className="truncate max-w-[180px]">{source.title}</span>
|
||||
<ArrowUpRight size={14} className="text-[#FFD700] group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
// Deprecated: Strategic Summary functionality is now integrated directly into the unified Executive Intelligence section in App.tsx.
|
||||
export const StrategicSummary = () => null;
|
||||
|
|
|
|||
|
|
@ -2,31 +2,45 @@
|
|||
import { GoogleGenAI } from "@google/genai";
|
||||
import { DashboardData } from "./types";
|
||||
|
||||
const MODEL_NAME = 'gemini-3-flash-preview';
|
||||
const MODEL_NAME = 'gemini-3-pro-preview';
|
||||
|
||||
export const fetchMarketInsights = async (product: string, category: string): Promise<DashboardData> => {
|
||||
const apiKey = process.env.API_KEY;
|
||||
console.log("API Key exists:", !!apiKey, "Length:", apiKey?.length);
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const currentDate = new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
||||
|
||||
const prompt = `Task: APAC Strategic Market Audit for "${product}" in "${category}".
|
||||
Current Date for Context: ${currentDate}.
|
||||
|
||||
Generate a JSON response for a marketing dashboard. Data must be current (2024-2025).
|
||||
You are a Strategic Data Analyst. Use the Google Search tool to conduct a deep-dive audit.
|
||||
Your analysis must be grounded in data available up to ${currentDate}.
|
||||
|
||||
Focus on:
|
||||
1. Real-time Market Metrics: Find the most recent industry benchmarks (2024-2025).
|
||||
2. Competitor Share of Search: Use recent search volume trends to estimate share.
|
||||
3. Efficacy Benchmark: Identify 3-4 key performance radar metrics for this category (e.g. Price, Quality, Innovation, Sustainability).
|
||||
4. High-Impact Creators: Identify the top 3-4 creators/influencers currently dominating the conversation in this space.
|
||||
5. Industry Pulse: Find the most recent (last 30 days) news headlines, sentiment, and sources. Provide AT LEAST 4 items.
|
||||
6. Strategic Whitespace: Identify EXACTLY 2 OPPORTUNITIES and EXACTLY 2 RISKS.
|
||||
7. Competitor Claims: Identify AT LEAST 7 strategic marketing claims. These should be "fuzzy" but verifiable (e.g., instead of just "10% faster", use "Velocity-Driven Performance" or "Heritage-Infused Innovation").
|
||||
8. Cultural Nuance: Identify EXACTLY 4 nuances. At least two MUST be specifically about local traditions, cultural behaviors, or regional etiquette relevant to "${category}" in APAC.
|
||||
|
||||
Generate a strictly valid JSON response.
|
||||
IMPORTANT: Do not include markdown code blocks. Return ONLY the raw JSON string.
|
||||
|
||||
JSON Structure:
|
||||
{
|
||||
"marketMetrics": {
|
||||
"roas": {"label": "ROAS", "value": "string", "trend": "up|down|stable", "explanation": "string"},
|
||||
"engagement": {"label": "Engagement", "value": "string", "trend": "up|down|stable", "explanation": "string"},
|
||||
"fatigue": {"label": "Ad Fatigue", "value": "string", "trend": "up|down|stable", "explanation": "string"},
|
||||
"sov": {"label": "SOV", "value": "string", "trend": "up|down|stable", "explanation": "string"}
|
||||
"roas": {"label": "ROAS", "value": "string", "trend": "up|down|stable", "explanation": "string", "source": "string", "methodology": "string"},
|
||||
"engagement": {"label": "Engagement", "value": "string", "trend": "up|down|stable", "explanation": "string", "source": "string", "methodology": "string"},
|
||||
"fatigue": {"label": "Ad Fatigue", "value": "string", "trend": "up|down|stable", "explanation": "string", "source": "string", "methodology": "string"},
|
||||
"sov": {"label": "SOV", "value": "string", "trend": "up|down|stable", "explanation": "string", "source": "string", "methodology": "string"}
|
||||
},
|
||||
"platformVelocity": [{"platform": "string", "region": "string", "trend": "up|stable|down", "value": number}],
|
||||
"radarMetrics": [{"label": "string", "score": number, "benchmark": number}],
|
||||
"newsFeed": [{"headline": "string", "source": "string", "sentiment": "positive|negative|neutral", "score": number}],
|
||||
"competitorClaims": [{"keyword": "string", "frequency": number, "explanation": "string"}],
|
||||
"competitorClaims": [{"keyword": "string", "frequency": number, "explanation": "string", "source": "string", "methodology": "string"}],
|
||||
"whitespaceAnalysis": [{"type": "OPPORTUNITY|RISK", "title": "string", "description": "string"}],
|
||||
"socialHighlights": [{"platform": "string", "headline": "string", "metrics": "string"}],
|
||||
"socialHighlights": [{"platform": "TikTok|Instagram", "headline": "string", "metrics": "string", "thumbnail": "string", "sourceUrl": "string"}],
|
||||
"topCreators": [{"name": "string", "handle": "string", "category": "string", "impact": "string"}],
|
||||
"agencyBrief": {
|
||||
"objective": "string",
|
||||
|
|
@ -37,25 +51,24 @@ export const fetchMarketInsights = async (product: string, category: string): Pr
|
|||
},
|
||||
"culturalNuances": [
|
||||
{
|
||||
"term": "Local cultural term or practice",
|
||||
"insight": "2-3 sentences explaining the mindset or history behind it",
|
||||
"strategyTip": "Strategic creative advice",
|
||||
"sourceTitle": "Name of credible source",
|
||||
"sourceUrl": "URL to source"
|
||||
"term": "string",
|
||||
"insight": "string",
|
||||
"strategyTip": "string",
|
||||
"sourceTitle": "string",
|
||||
"sourceUrl": "string"
|
||||
}
|
||||
],
|
||||
"summary": {"overview": "string", "strategicTakeaways": ["string"], "sources": [{"title": "string", "url": "string"}]},
|
||||
"shareOfSearch": [{"competitor": "string", "percentage": number}]
|
||||
}
|
||||
|
||||
Rules:
|
||||
1. Use Google Search tool to find real-time APAC trends.
|
||||
2. Return ONLY the JSON object.
|
||||
3. Ensure numeric values for charts (0-100).
|
||||
4. CRITICAL: "competitorClaims" must have between 8 and 12 high-impact keywords.
|
||||
5. CRITICAL: "topCreators" must contain exactly 4 entries.
|
||||
6. CRITICAL: "agencyBrief.tacticalContent" must provide 3-4 specific content execution ideas based on current trending formats in APAC.
|
||||
7. CULTURAL ACCURACY RULE: "culturalNuances" must provide 3 specific insights for the selected category and market. Cross-reference credible sources (ad agencies, global news, etc.). Avoid stereotypes. Only include verified marketing trends.`;
|
||||
Strict Rules:
|
||||
1. GROUNDING: Use Google Search to find ACTUAL names, news, and trends relative to ${currentDate}.
|
||||
2. CREATORS: Find top 3-4 specific real-world creators for this specific category.
|
||||
3. NEWS: Find real, recent headlines with specific sources. At least 4.
|
||||
4. CULTURAL: Precisely 4 nuances. Include local traditions.
|
||||
5. JSON: Return only the JSON object. No markdown.
|
||||
6. CLAIMS: At least 7 strategic, "fuzzy" but grounded claims.`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: MODEL_NAME,
|
||||
|
|
@ -67,18 +80,13 @@ export const fetchMarketInsights = async (product: string, category: string): Pr
|
|||
});
|
||||
|
||||
const text = response.text;
|
||||
console.log("Gemini raw response:", text);
|
||||
|
||||
if (!text) throw new Error("AI returned empty content");
|
||||
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(text.trim());
|
||||
console.log("Parsed dashboard data:", parsed);
|
||||
// API sometimes returns data wrapped in an array
|
||||
const result = Array.isArray(parsed) ? parsed[0] : parsed;
|
||||
return result;
|
||||
const jsonString = text.trim().replace(/^```json\n?/, "").replace(/\n?```$/, "");
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse AI response:", text);
|
||||
throw new Error("Invalid JSON format from AI");
|
||||
throw new Error("Data synthesis failed. Please try a different category or retry.");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "Oho! - APAC Strategy Dashboard",
|
||||
"name": "APAC Strategy & Insights Engine",
|
||||
"description": "A high-end, minimalist creative agency dashboard for real-time market insights using Gemini Google Search Grounding.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
4462
migrated_prompt_history/prompt_2026-02-14T16_57_13.578Z.json
Normal file
4462
migrated_prompt_history/prompt_2026-02-14T16_57_13.578Z.json
Normal file
File diff suppressed because one or more lines are too long
981
package-lock.json
generated
981
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"name": "oho!---apac-strategy-dashboard",
|
||||
"name": "oho.2---apac-strategy-dashboard",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.37.0",
|
||||
|
|
|
|||
5
types.ts
5
types.ts
|
|
@ -4,6 +4,8 @@ export interface MarketMetric {
|
|||
value: string;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
explanation: string;
|
||||
source?: string;
|
||||
methodology?: string;
|
||||
}
|
||||
|
||||
export interface ShareOfSearch {
|
||||
|
|
@ -29,6 +31,7 @@ export interface SocialContent {
|
|||
headline: string;
|
||||
metrics: string;
|
||||
thumbnail: string;
|
||||
sourceUrl?: string;
|
||||
}
|
||||
|
||||
export interface Creator {
|
||||
|
|
@ -49,6 +52,8 @@ export interface CompetitorClaim {
|
|||
keyword: string;
|
||||
frequency: number;
|
||||
explanation: string;
|
||||
source?: string;
|
||||
methodology?: string;
|
||||
}
|
||||
|
||||
export interface WhitespaceItem {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue