logo/header and other changes

This commit is contained in:
shubham.goyal@brandtech.plus 2026-04-13 12:40:00 +05:30
parent e3ea23c594
commit 870051839c
10 changed files with 5142 additions and 1143 deletions

615
App.tsx
View file

@ -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>
);
};

View file

@ -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 {};

View file

@ -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;

View file

@ -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.");
}
};

View file

@ -28,6 +28,7 @@
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="root"></div>

View file

@ -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": []
}

File diff suppressed because one or more lines are too long

981
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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 {