feat(ca): Finalize v5 pipeline - Hybrid Matrix, CoT Enrichment & User Repair Mode

This commit is contained in:
2026-01-12 15:29:43 +00:00
parent b5e6c415c7
commit 70e689384e
15 changed files with 547 additions and 1224 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import type { AppState, CompetitorCandidate, Product, TargetIndustry, Keyword, SilverBullet, Battlecard, ReferenceAnalysis } from './types';
import { fetchStep1Data, fetchStep2Data, fetchStep3Data, fetchStep4Data, fetchStep5Data_SilverBullets, fetchStep6Data_Conclusion, fetchStep7Data_Battlecards, fetchStep8Data_ReferenceAnalysis } from './services/geminiService';
import { fetchStep1Data, fetchStep2Data, fetchStep3Data, fetchStep4Data, fetchStep5Data_SilverBullets, fetchStep6Data_Conclusion, fetchStep7Data_Battlecards, fetchStep8Data_ReferenceAnalysis, reanalyzeCompetitor } from './services/geminiService';
import { generatePdfReport } from './services/pdfService';
import InputForm from './components/InputForm';
import StepIndicator from './components/StepIndicator';
@@ -91,6 +91,15 @@ const App: React.FC = () => {
}
}, []);
const handleUpdateAnalysis = useCallback((index: number, updatedAnalysis: any) => {
setAppState(prevState => {
if (!prevState) return null;
const newAnalyses = [...prevState.analyses];
newAnalyses[index] = updatedAnalysis;
return { ...prevState, analyses: newAnalyses };
});
}, []);
const handleConfirmStep = useCallback(async () => {
if (!appState) return;
@@ -112,7 +121,11 @@ const App: React.FC = () => {
case 3:
const shortlist = [...appState.competitor_candidates]
.sort((a, b) => b.confidence - a.confidence)
.slice(0, appState.initial_params.max_competitors);
.slice(0, appState.initial_params.max_competitors)
.map(c => ({
...c,
manual_urls: c.manual_urls ? c.manual_urls.split('\n').map(u => u.trim()).filter(u => u) : []
}));
const { analyses } = await fetchStep4Data(appState.company, shortlist, lang);
newState = { competitors_shortlist: shortlist, analyses, step: 4 };
break;
@@ -158,7 +171,7 @@ const App: React.FC = () => {
case 1: return <Step1Extraction products={appState.products} industries={appState.target_industries} onProductsChange={(p) => handleUpdateState('products', p)} onIndustriesChange={(i) => handleUpdateState('target_industries', i)} t={t.step1} lang={appState.initial_params.language} />;
case 2: return <Step2Keywords keywords={appState.keywords} onKeywordsChange={(k) => handleUpdateState('keywords', k)} t={t.step2} />;
case 3: return <Step3Competitors candidates={appState.competitor_candidates} onCandidatesChange={(c) => handleUpdateState('competitor_candidates', c)} maxCompetitors={appState.initial_params.max_competitors} t={t.step3} />;
case 4: return <Step4Analysis analyses={appState.analyses} t={t.step4} />;
case 4: return <Step4Analysis analyses={appState.analyses} company={appState.company} onAnalysisUpdate={handleUpdateAnalysis} t={t.step4} />;
case 5: return <Step5SilverBullets silver_bullets={appState.silver_bullets} t={t.step5} />;
case 6: return <Step6Conclusion appState={appState} t={t.step6} />;
case 7: return <Step7_Battlecards appState={appState} t={t.step7} />;

File diff suppressed because it is too large Load Diff

View File

@@ -27,9 +27,10 @@ const Step3Competitors: React.FC<Step3CompetitorsProps> = ({ candidates, onCandi
fieldConfigs={[
{ key: 'name', label: t.nameLabel, type: 'text' },
{ key: 'url', label: 'URL', type: 'text' },
{ key: 'manual_urls', label: t.manualUrlsLabel, type: 'textarea' },
{ key: 'why', label: t.whyLabel, type: 'textarea' },
]}
newItemTemplate={{ name: '', url: '', confidence: 0.8, why: '', evidence: [] }}
newItemTemplate={{ name: '', url: '', confidence: 0.8, why: '', evidence: [], manual_urls: '' }}
renderDisplay={(item, index) => (
<div>
<div className="flex items-center justify-between">
@@ -39,6 +40,11 @@ const Step3Competitors: React.FC<Step3CompetitorsProps> = ({ candidates, onCandi
{t.visitButton}
</a>
<EvidencePopover evidence={item.evidence} />
{item.manual_urls && item.manual_urls.trim() && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{item.manual_urls.split('\n').filter(u => u.trim()).length} Manual URLs
</span>
)}
</div>
<span className="text-xs font-mono bg-light-accent dark:bg-brand-accent text-light-text dark:text-white py-1 px-2 rounded-full">
{(item.confidence * 100).toFixed(0)}%

View File

@@ -1,13 +1,17 @@
import React from 'react';
import type { Analysis } from '../types';
import React, { useState } from 'react';
import type { Analysis, AppState } from '../types';
import EvidencePopover from './EvidencePopover';
import { reanalyzeCompetitor } from '../services/geminiService';
interface Step4AnalysisProps {
analyses: Analysis[];
company: AppState['company'];
onAnalysisUpdate: (index: number, analysis: Analysis) => void;
t: any;
}
const DownloadIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>);
const RefreshIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>);
const downloadJSON = (data: any, filename: string) => {
const jsonStr = JSON.stringify(data, null, 2);
@@ -28,8 +32,46 @@ const OverlapBar: React.FC<{ score: number }> = ({ score }) => (
</div>
);
const Step4Analysis: React.FC<Step4AnalysisProps> = ({ analyses, t }) => {
const Step4Analysis: React.FC<Step4AnalysisProps> = ({ analyses, company, onAnalysisUpdate, t }) => {
const sortedAnalyses = [...analyses].sort((a, b) => b.overlap_score - a.overlap_score);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [manualUrls, setManualUrls] = useState<string>("");
const [isReanalyzing, setIsReanalyzing] = useState<boolean>(false);
const handleEditStart = (index: number) => {
setEditingIndex(index);
setManualUrls(""); // Reset
};
const handleReanalyze = async (index: number, competitor: Analysis['competitor']) => {
setIsReanalyzing(true);
try {
const urls = manualUrls.split('\n').map(u => u.trim()).filter(u => u);
// Construct a partial CompetitorCandidate object as expected by the service
const candidate = {
name: competitor.name,
url: competitor.url,
confidence: 0, // Not needed for re-analysis
why: "",
evidence: []
};
const updatedAnalysis = await reanalyzeCompetitor(company, candidate, urls);
// Find the original index in the unsorted 'analyses' array to update correctly
const originalIndex = analyses.findIndex(a => a.competitor.name === competitor.name);
if (originalIndex !== -1) {
onAnalysisUpdate(originalIndex, updatedAnalysis);
}
setEditingIndex(null);
} catch (error) {
console.error("Re-analysis failed:", error);
alert("Fehler bei der Re-Analyse. Bitte Logs prüfen.");
} finally {
setIsReanalyzing(false);
}
};
return (
<div>
@@ -49,6 +91,13 @@ const Step4Analysis: React.FC<Step4AnalysisProps> = ({ analyses, t }) => {
</a>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handleEditStart(index)}
title="Daten anreichern / Korrigieren"
className="text-light-subtle dark:text-brand-light hover:text-blue-500 p-1 rounded-full"
>
<RefreshIcon />
</button>
<button
onClick={() => downloadJSON(analysis, `analysis_${analysis.competitor.name.replace(/ /g, '_')}.json`)}
title={t.downloadJson_title}
@@ -60,6 +109,39 @@ const Step4Analysis: React.FC<Step4AnalysisProps> = ({ analyses, t }) => {
</div>
</div>
{/* Re-Analysis UI */}
{editingIndex === index && (
<div className="mb-6 bg-light-primary dark:bg-brand-primary p-4 rounded-md border border-brand-highlight">
<h4 className="font-bold text-sm mb-2">Manuelle Produkt-URLs ergänzen (optional)</h4>
<p className="text-xs text-light-subtle dark:text-brand-light mb-2">
Falls Produkte fehlen, fügen Sie hier direkte Links zu den Produktseiten ein (eine pro Zeile).
</p>
<textarea
className="w-full bg-light-secondary dark:bg-brand-secondary text-light-text dark:text-brand-text border border-light-accent dark:border-brand-accent rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-highlight mb-2"
rows={3}
placeholder="https://example.com/product-a&#10;https://example.com/product-b"
value={manualUrls}
onChange={(e) => setManualUrls(e.target.value)}
/>
<div className="flex justify-end space-x-2">
<button
onClick={() => setEditingIndex(null)}
className="px-3 py-1 text-sm bg-gray-500 hover:bg-gray-600 text-white rounded"
disabled={isReanalyzing}
>
Abbrechen
</button>
<button
onClick={() => handleReanalyze(index, analysis.competitor)}
className="px-3 py-1 text-sm bg-brand-highlight hover:bg-blue-600 text-white rounded flex items-center"
disabled={isReanalyzing}
>
{isReanalyzing ? "Analysiere..." : "Neu scrapen & Analysieren"}
</button>
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-semibold mb-2">{t.portfolio}</h4>

View File

@@ -72,6 +72,20 @@ export const fetchStep4Data = async (company: AppState['company'], competitors:
return response.json();
};
export const reanalyzeCompetitor = async (company: AppState['company'], competitor: CompetitorCandidate, manualUrls: string[]): Promise<Analysis> => {
const response = await fetch(`${API_BASE_URL}api/reanalyzeCompetitor`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ company, competitor, manual_urls: manualUrls }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export const fetchStep5Data_SilverBullets = async (company: AppState['company'], analyses: Analysis[], language: 'de' | 'en'): Promise<{ silver_bullets: SilverBullet[] }> => {
const response = await fetch(`${API_BASE_URL}api/fetchStep5Data_SilverBullets`, {
method: 'POST',

View File

@@ -58,6 +58,7 @@ export const translations = {
cardTitle: "Wettbewerber-Kandidaten",
nameLabel: "Name",
whyLabel: "Begründung",
manualUrlsLabel: "Zusätzliche Produkt-URLs (eine pro Zeile)",
visitButton: "Besuchen",
shortlistBoundary: "SHORTLIST-GRENZE",
editableCard: { add: "Hinzufügen", cancel: "Abbrechen", save: "Speichern" }
@@ -200,6 +201,7 @@ export const translations = {
cardTitle: "Competitor Candidates",
nameLabel: "Name",
whyLabel: "Justification",
manualUrlsLabel: "Additional Product URLs (one per line)",
visitButton: "Visit",
shortlistBoundary: "SHORTLIST BOUNDARY",
editableCard: { add: "Add", cancel: "Cancel", save: "Save" }

View File

@@ -27,6 +27,7 @@ export interface CompetitorCandidate {
confidence: number;
why: string;
evidence: Evidence[];
manual_urls?: string;
}
export interface ShortlistedCompetitor {