Dateien nach "competitor-analysis/components" hochladen
This commit is contained in:
91
competitor-analysis/components/Step7_Battlecards.tsx
Normal file
91
competitor-analysis/components/Step7_Battlecards.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import type { AppState, Battlecard } from '../types';
|
||||||
|
|
||||||
|
interface Step7BattlecardsProps {
|
||||||
|
appState: AppState | null;
|
||||||
|
t: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 inline-block" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" /></svg>);
|
||||||
|
const StrengthsIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 inline-block" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13a1 1 0 102 0V9.414l1.293 1.293a1 1 0 001.414-1.414z" clipRule="evenodd" /></svg>);
|
||||||
|
const LandmineIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 inline-block" viewBox="0 0 20 20" fill="currentColor"><path d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" /></svg>);
|
||||||
|
const BulletIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 inline-block" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM7 9a1 1 0 000 2h6a1 1 0 100-2H7z" clipRule="evenodd" /></svg>);
|
||||||
|
|
||||||
|
const BattlecardComponent: React.FC<{ battlecard: Battlecard, t: any }> = ({ battlecard, t }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-light-secondary dark:bg-brand-secondary p-6 rounded-lg shadow-lg relative">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold flex items-center mb-2"><ProfileIcon /> {t.profile}</h3>
|
||||||
|
<p className="text-light-subtle dark:text-brand-light pl-7">{battlecard.competitor_profile.focus}</p>
|
||||||
|
<p className="text-light-subtle dark:text-brand-light pl-7 mt-1">{battlecard.competitor_profile.positioning}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold flex items-center mb-2"><StrengthsIcon /> {t.strengths}</h3>
|
||||||
|
<ul className="list-disc list-inside space-y-1 text-light-subtle dark:text-brand-light pl-7">
|
||||||
|
{(battlecard.strengths_vs_weaknesses || []).map((item, i) => <li key={i}>{item}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold flex items-center mb-2"><LandmineIcon /> {t.landmines}</h3>
|
||||||
|
<ul className="list-disc list-inside space-y-1 text-light-subtle dark:text-brand-light pl-7">
|
||||||
|
{(battlecard.landmine_questions || []).map((item, i) => <li key={i}>{item}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold flex items-center mb-2"><BulletIcon /> {t.silverBullet}</h3>
|
||||||
|
<blockquote className="border-l-4 border-brand-highlight pl-4 ml-7">
|
||||||
|
<p className="text-lg italic text-light-subtle dark:text-brand-light">"{battlecard.silver_bullet}"</p>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Step7_Battlecards: React.FC<Step7BattlecardsProps> = ({ appState, t }) => {
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
|
if (!appState || !appState.battlecards || appState.battlecards.length === 0) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">{t.title}</h2>
|
||||||
|
<p className="text-light-subtle dark:text-brand-light">{t.generating}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { battlecards } = appState;
|
||||||
|
const activeBattlecard = battlecards[activeTab];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">{t.title}</h2>
|
||||||
|
<p className="text-light-subtle dark:text-brand-light mb-6">
|
||||||
|
{t.subtitle}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex border-b border-light-accent dark:border-brand-accent mb-4 overflow-x-auto">
|
||||||
|
{battlecards.map((card, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setActiveTab(index)}
|
||||||
|
className={`py-2 px-4 font-semibold text-sm focus:outline-none whitespace-nowrap ${
|
||||||
|
activeTab === index
|
||||||
|
? 'border-b-2 border-brand-highlight text-light-text dark:text-white'
|
||||||
|
: 'text-light-subtle dark:text-brand-light hover:bg-light-accent dark:hover:bg-brand-accent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{card.competitor_name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeBattlecard && <BattlecardComponent battlecard={activeBattlecard} t={t.card} />}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Step7_Battlecards;
|
||||||
81
competitor-analysis/components/Step8_References.tsx
Normal file
81
competitor-analysis/components/Step8_References.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { ReferenceAnalysis } from '../types';
|
||||||
|
|
||||||
|
interface Step8ReferencesProps {
|
||||||
|
analyses: ReferenceAnalysis[];
|
||||||
|
groundingMetadata: any[];
|
||||||
|
t: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LinkIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 inline-block ml-1" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clipRule="evenodd" /></svg>);
|
||||||
|
|
||||||
|
|
||||||
|
const Step8_References: React.FC<Step8ReferencesProps> = ({ analyses, groundingMetadata, t }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">{t.title}</h2>
|
||||||
|
<p className="text-light-subtle dark:text-brand-light mb-6">
|
||||||
|
{t.subtitle}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{(analyses || []).map((analysis, index) => {
|
||||||
|
// This robust check prevents crashes if `references` is null, undefined, or not an array.
|
||||||
|
const hasReferences = Array.isArray(analysis.references) && analysis.references.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className="bg-light-secondary dark:bg-brand-secondary p-6 rounded-lg shadow-lg">
|
||||||
|
<h3 className="text-xl font-bold text-light-text dark:text-white mb-4">{analysis.competitor_name}</h3>
|
||||||
|
|
||||||
|
{!hasReferences ? (
|
||||||
|
<p className="text-light-subtle dark:text-brand-light">{t.noReferencesFound}</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{analysis.references.map((ref, refIndex) => (
|
||||||
|
<div key={refIndex} className="bg-light-primary dark:bg-brand-primary p-4 rounded-md border-l-4 border-brand-accent">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<strong className="text-light-text dark:text-white">{ref.name}</strong>
|
||||||
|
{ref.industry && <span className="text-xs ml-2 bg-light-accent dark:bg-brand-accent px-2 py-0.5 rounded-full">{ref.industry}</span>}
|
||||||
|
</div>
|
||||||
|
{ref.case_study_url && (
|
||||||
|
<a href={ref.case_study_url} target="_blank" rel="noopener noreferrer" className="text-sm text-blue-500 dark:text-blue-400 hover:underline flex-shrink-0">
|
||||||
|
{t.caseStudyLink} <LinkIcon/>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{ref.testimonial_snippet && (
|
||||||
|
<blockquote className="mt-2 text-sm italic text-light-subtle dark:text-brand-light border-l-2 border-gray-400 pl-3">
|
||||||
|
"{ref.testimonial_snippet}"
|
||||||
|
</blockquote>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{groundingMetadata && groundingMetadata.length > 0 && (
|
||||||
|
<div className="mt-8 bg-light-secondary dark:bg-brand-secondary p-6 rounded-lg shadow-lg">
|
||||||
|
<h3 className="text-xl font-bold text-light-text dark:text-white mb-4">{t.sourcesTitle}</h3>
|
||||||
|
<ul className="list-disc list-inside space-y-2">
|
||||||
|
{groundingMetadata
|
||||||
|
.filter(chunk => chunk.web && chunk.web.uri)
|
||||||
|
.map((chunk, index) => (
|
||||||
|
<li key={index} className="text-sm">
|
||||||
|
<a href={chunk.web.uri} target="_blank" rel="noopener noreferrer" className="text-blue-500 dark:text-blue-400 hover:underline">
|
||||||
|
{chunk.web.title || chunk.web.uri}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Step8_References;
|
||||||
50
competitor-analysis/components/StepIndicator.tsx
Normal file
50
competitor-analysis/components/StepIndicator.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface StepIndicatorProps {
|
||||||
|
currentStep: number;
|
||||||
|
highestStep: number;
|
||||||
|
onStepClick: (step: number) => void;
|
||||||
|
t: {
|
||||||
|
title: string;
|
||||||
|
steps: string[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepIndicator: React.FC<StepIndicatorProps> = ({ currentStep, highestStep, onStepClick, t }) => {
|
||||||
|
|
||||||
|
const steps = t.steps.map((name, index) => ({ id: index + 1, name }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-light-secondary dark:bg-brand-secondary p-4 rounded-lg shadow-md border border-light-accent dark:border-brand-accent">
|
||||||
|
<h3 className="font-bold text-lg mb-4">{t.title}</h3>
|
||||||
|
<ol className="space-y-3">
|
||||||
|
{steps.map((step) => {
|
||||||
|
const isCompleted = step.id < currentStep;
|
||||||
|
const isActive = step.id === currentStep;
|
||||||
|
const isClickable = step.id <= highestStep;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={step.id}
|
||||||
|
className={`flex items-center p-1 rounded-md transition-colors ${isClickable ? 'cursor-pointer hover:bg-light-accent dark:hover:bg-brand-accent' : 'cursor-default'}`}
|
||||||
|
onClick={() => isClickable && onStepClick(step.id)}
|
||||||
|
>
|
||||||
|
<span className={`flex-shrink-0 flex items-center justify-center w-6 h-6 rounded-full mr-3 text-sm font-bold ${
|
||||||
|
isCompleted ? 'bg-green-500 text-white' :
|
||||||
|
isActive ? 'bg-brand-highlight text-white ring-2 ring-offset-2 ring-offset-light-secondary dark:ring-offset-brand-secondary ring-brand-highlight' :
|
||||||
|
'bg-light-accent dark:bg-brand-accent text-light-text dark:text-brand-text'
|
||||||
|
}`}>
|
||||||
|
{isCompleted ? '✓' : step.id}
|
||||||
|
</span>
|
||||||
|
<span className={`font-medium ${isActive ? 'text-light-text dark:text-white' : 'text-light-subtle dark:text-brand-light'}`}>
|
||||||
|
{step.name}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StepIndicator;
|
||||||
Reference in New Issue
Block a user