feat: Market Intel Database & UI Polish
- market_db_manager.py: Created SQLite manager for saving/loading projects. - server.cjs: Added API routes for project management. - geminiService.ts: Added client-side DB functions. - StepInput.tsx: Added 'Past Runs' sidebar to load previous audits. - App.tsx: Added auto-save functionality and full state hydration logic. - StepOutreach.tsx: Improved UI layout by merging generated campaigns and suggestions into one list.
This commit is contained in:
@@ -194,61 +194,64 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden flex flex-col md:flex-row min-h-[600px]">
|
||||
{/* Sidebar Tabs */}
|
||||
<div className="w-full md:w-80 bg-slate-50 border-r border-slate-200 flex flex-col">
|
||||
<div className="p-4 border-b border-slate-200">
|
||||
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider">Generated Campaigns</h3>
|
||||
</div>
|
||||
|
||||
{/* Generated List */}
|
||||
<div className="flex-1 overflow-y-auto p-2 flex flex-col gap-2">
|
||||
{emails.map((email, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setActiveTab(idx)}
|
||||
className={`text-left p-3 rounded-lg text-sm font-medium transition-all group ${activeTab === idx
|
||||
? 'bg-white shadow-sm text-indigo-700 border border-indigo-100 ring-1 ring-indigo-500/20'
|
||||
: 'text-slate-600 hover:bg-slate-200/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="truncate font-semibold">{email.persona}</span>
|
||||
<span className="text-[10px] bg-slate-200 text-slate-500 px-1.5 py-0.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">#{idx+1}</span>
|
||||
{/* Sidebar Tabs */}
|
||||
<div className="w-full md:w-80 bg-slate-50 border-r border-slate-200 flex flex-col h-full max-h-[600px]">
|
||||
<div className="p-4 border-b border-slate-200 flex-none">
|
||||
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider">Campaign Roles</h3>
|
||||
</div>
|
||||
|
||||
{/* Unified Scroll List */}
|
||||
<div className="flex-1 overflow-y-auto p-2 flex flex-col gap-2">
|
||||
{/* Generated Campaigns */}
|
||||
{emails.map((email, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setActiveTab(idx)}
|
||||
className={`text-left p-3 rounded-lg text-sm font-medium transition-all group ${
|
||||
activeTab === idx
|
||||
? 'bg-white shadow-sm text-indigo-700 border border-indigo-100 ring-1 ring-indigo-500/20'
|
||||
: 'text-slate-600 hover:bg-slate-200/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="truncate font-semibold">{email.persona}</span>
|
||||
<span className="text-[10px] bg-slate-200 text-slate-500 px-1.5 py-0.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">#{idx+1}</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-1 truncate font-normal opacity-80">{email.subject}</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Separator for Suggestions */}
|
||||
{availableRoles.length > 0 && (
|
||||
<div className="mt-4 mb-2 px-2 flex items-center gap-2">
|
||||
<div className="h-px bg-slate-200 flex-1"></div>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider flex items-center gap-1">
|
||||
<Sparkles size={10} /> Suggestions
|
||||
</span>
|
||||
<div className="h-px bg-slate-200 flex-1"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Suggestions List */}
|
||||
{availableRoles.map((role, i) => (
|
||||
<div key={`sugg-${i}`} className="flex items-center justify-between p-2 pl-3 bg-slate-100/50 border border-slate-200/50 rounded-lg group hover:border-indigo-200 transition-colors">
|
||||
<span className="text-xs font-medium text-slate-600 truncate max-w-[180px]" title={role}>{role}</span>
|
||||
<button
|
||||
onClick={() => handleGenerateSpecific(role)}
|
||||
disabled={!!isGeneratingSpecific}
|
||||
className="bg-white hover:bg-indigo-50 text-indigo-600 border border-slate-200 hover:border-indigo-200 p-1.5 rounded-md transition-all disabled:opacity-50 shadow-sm"
|
||||
title="Generate Campaign"
|
||||
>
|
||||
{isGeneratingSpecific === role ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<Plus size={14} strokeWidth={3} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-1 truncate font-normal opacity-80">{email.subject}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Suggestions List */}
|
||||
{availableRoles.length > 0 && (
|
||||
<div className="border-t border-slate-200 bg-slate-100/50 flex flex-col max-h-[40%]">
|
||||
<div className="p-3 border-b border-slate-200 flex items-center gap-2">
|
||||
<Sparkles size={14} className="text-indigo-500" />
|
||||
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider">Other Relevant Roles</h3>
|
||||
</div>
|
||||
<div className="overflow-y-auto p-2 gap-2 flex flex-col">
|
||||
{availableRoles.map((role, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-2 bg-white border border-slate-200 rounded-lg shadow-sm">
|
||||
<span className="text-xs font-medium text-slate-700 truncate max-w-[140px]" title={role}>{role}</span>
|
||||
<button
|
||||
onClick={() => handleGenerateSpecific(role)}
|
||||
disabled={!!isGeneratingSpecific}
|
||||
className="bg-indigo-50 hover:bg-indigo-100 text-indigo-700 p-1.5 rounded-md transition-colors disabled:opacity-50"
|
||||
title="Generate Campaign"
|
||||
>
|
||||
{isGeneratingSpecific === role ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<Plus size={14} strokeWidth={3} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 p-8 flex flex-col overflow-y-auto bg-slate-50/30">
|
||||
|
||||
Reference in New Issue
Block a user