feat(ca): Finalize v5 pipeline - Hybrid Matrix, CoT Enrichment & User Repair Mode
This commit is contained in:
@@ -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)}%
|
||||
|
||||
@@ -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 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>
|
||||
|
||||
Reference in New Issue
Block a user