feat(competitor-analysis): Fix 404, SDK compatibility, and update docs

Resolved multiple issues preventing the 'competitor-analysis' app from running and serving its frontend:

1.  **Fixed Python SyntaxError in Prompts:** Corrected unterminated string literals and ensure proper multi-line string formatting (using .format() instead of f-strings for complex prompts) in .
2.  **Addressed Python SDK Compatibility (google-generativeai==0.3.0):**
    *   Removed  for  and  by adapting the orchestrator to pass JSON schemas as direct Python dictionaries, as required by the older SDK version.
    *   Updated  with detailed guidance on handling / imports and dictionary-based schema definitions for older SDKs.
3.  **Corrected Frontend Build Dependencies:** Moved critical build dependencies (like , , ) from  to  in .
    *   Updated  to include this  pitfall, ensuring frontend build tools are installed in Docker.
4.  **Updated Documentation:**
    *   : Added comprehensive lessons learned regarding  dependencies, Python SDK versioning (specifically  and  imports for ), and robust multi-line prompt handling.
    *   : Integrated specific details of the encountered errors and their solutions, making the migration report a more complete historical record and guide.

These changes collectively fix the 404 error by ensuring the Python backend starts correctly and serves the frontend assets after a successful build.
This commit is contained in:
2026-01-10 09:10:00 +00:00
parent ff5184a26e
commit 8e5ab8949b
31 changed files with 3383 additions and 13 deletions

View File

@@ -0,0 +1,129 @@
import type { Product, TargetIndustry, Keyword, CompetitorCandidate, Analysis, AppState, Conclusion, SilverBullet, Battlecard, ShortlistedCompetitor, ReferenceAnalysis } from '../types';
const API_BASE_URL = './'; // Relative to the application sub-path (/ca/)
export const fetchProductDetails = async (name: string, url: string, language: 'de' | 'en'): Promise<Product> => {
const response = await fetch(`${API_BASE_URL}api/fetchProductDetails`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, url, language }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export const fetchStep1Data = async (startUrl: string, language: 'de' | 'en'): Promise<{ products: Product[], target_industries: TargetIndustry[] }> => {
const response = await fetch(`${API_BASE_URL}api/fetchStep1Data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ start_url: startUrl, language }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export const fetchStep2Data = async (products: Product[], industries: TargetIndustry[], language: 'de' | 'en'): Promise<{ keywords: Keyword[] }> => {
const response = await fetch(`${API_BASE_URL}api/fetchStep2Data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ products, industries, language }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export const fetchStep3Data = async (keywords: Keyword[], marketScope: string, language: 'de' | 'en'): Promise<{ competitor_candidates: CompetitorCandidate[] }> => {
const response = await fetch(`${API_BASE_URL}api/fetchStep3Data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ keywords, market_scope: marketScope, language }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export const fetchStep4Data = async (company: AppState['company'], competitors: CompetitorCandidate[], language: 'de' | 'en'): Promise<{ analyses: Analysis[] }> => {
const response = await fetch(`${API_BASE_URL}api/fetchStep4Data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ company, competitors, language }),
});
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',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ company, analyses, language }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export const fetchStep6Data_Conclusion = async (company: AppState['company'], products: Product[], industries: TargetIndustry[], analyses: Analysis[], silver_bullets: SilverBullet[], language: 'de' | 'en'): Promise<{ conclusion: Conclusion }> => {
const response = await fetch(`${API_BASE_URL}api/fetchStep6Data_Conclusion`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ company, products, industries, analyses, silver_bullets, language }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export const fetchStep7Data_Battlecards = async (company: AppState['company'], analyses: Analysis[], silver_bullets: SilverBullet[], language: 'de' | 'en'): Promise<{ battlecards: Battlecard[] }> => {
const response = await fetch(`${API_BASE_URL}api/fetchStep7Data_Battlecards`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ company, analyses, silver_bullets, language }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export const fetchStep8Data_ReferenceAnalysis = async (competitors: ShortlistedCompetitor[], language: 'de' | 'en'): Promise<{ reference_analysis: ReferenceAnalysis[], groundingMetadata: any[] }> => {
const response = await fetch(`${API_BASE_URL}api/fetchStep8Data_ReferenceAnalysis`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ competitors, language }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};

View File

@@ -0,0 +1,179 @@
import type { AppState } from '../types';
// @ts-ignore
import { jsPDF } from "jspdf";
import "jspdf-autotable";
const addWrappedText = (doc: any, text: string, x: number, y: number, maxWidth: number): number => {
const lines = doc.splitTextToSize(text, maxWidth);
doc.text(lines, x, y);
// Approximate height of the text block based on font size and line height
const fontSize = doc.getFontSize();
const lineHeight = 1.15;
return lines.length * fontSize * lineHeight / 2.8;
};
export const generatePdfReport = async (appState: AppState, t: any) => {
const doc = new jsPDF();
const { company, analyses, silver_bullets, conclusion, battlecards, reference_analysis } = appState;
const pageMargin = 15;
const pageWidth = doc.internal.pageSize.getWidth();
const contentWidth = pageWidth - 2 * pageMargin;
let yPos = 20;
// Title Page
doc.setFontSize(22);
doc.setFont('helvetica', 'bold');
doc.text(t.title, pageWidth / 2, yPos, { align: 'center' });
yPos += 10;
doc.setFontSize(16);
doc.setFont('helvetica', 'normal');
doc.text(company.name, pageWidth / 2, yPos, { align: 'center' });
yPos += 15;
const addPageIfNeeded = (requiredHeight: number) => {
if (yPos + requiredHeight > 280) { // 297mm height, 10mm margin bottom
doc.addPage();
yPos = 20;
}
};
const addHeader = (title: string) => {
addPageIfNeeded(20);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text(title, pageMargin, yPos);
yPos += 8;
doc.setFont('helvetica', 'normal');
doc.setFontSize(10);
};
// Summary & Opportunities
if (conclusion) {
addHeader(t.summary);
yPos += addWrappedText(doc, conclusion.summary, pageMargin, yPos, contentWidth);
yPos += 5;
addHeader(t.opportunities);
yPos += addWrappedText(doc, conclusion.opportunities, pageMargin, yPos, contentWidth);
yPos += 10;
}
// Analysis Details
addHeader(t.step4_title);
analyses.forEach(a => {
addPageIfNeeded(40);
doc.setFontSize(12);
doc.setFont('helvetica', 'bold');
doc.text(a.competitor.name, pageMargin, yPos);
yPos += 6;
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
yPos += addWrappedText(doc, `Portfolio: ${a.portfolio.map(p => p.product).join(', ')}`, pageMargin + 5, yPos, contentWidth - 5);
yPos += addWrappedText(doc, `Industries: ${a.target_industries.join(', ')}`, pageMargin + 5, yPos, contentWidth - 5);
yPos += addWrappedText(doc, `Differentiators: ${a.differentiators.join('; ')}`, pageMargin + 5, yPos, contentWidth - 5);
yPos += 5;
});
// Silver Bullets
addHeader(t.step5_title);
silver_bullets.forEach(b => {
addPageIfNeeded(15);
yPos += addWrappedText(doc, `${t.against} ${b.competitor_name}: "${b.statement}"`, pageMargin, yPos, contentWidth);
yPos += 2;
});
yPos += 5;
// Matrices
if (conclusion) {
const transformMatrix = (matrix: any[] | undefined, rowKeyName: 'product' | 'industry') => {
if (!matrix || matrix.length === 0) return { head: [], body: [] };
const allCompetitors = new Set<string>();
matrix.forEach(row => (row.availability || []).forEach((item: any) => allCompetitors.add(item.competitor)));
const head = [['', ...Array.from(allCompetitors).sort()]];
const body = matrix.map(row => {
const rowData: (string|any)[] = [row[rowKeyName]];
head[0].slice(1).forEach(competitor => {
const item = (row.availability || []).find((a: any) => a.competitor === competitor);
rowData.push(item && item.has_offering ? '✓' : '');
});
return rowData;
});
return { head, body };
};
if (conclusion.product_matrix) {
addPageIfNeeded(50);
addHeader(t.productMatrix);
const { head, body } = transformMatrix(conclusion.product_matrix, 'product');
(doc as any).autoTable({
head: head, body: body, startY: yPos, theme: 'striped',
headStyles: { fillColor: [65, 90, 119] }, margin: { left: pageMargin }
});
yPos = (doc as any).lastAutoTable.finalY + 10;
}
if (conclusion.industry_matrix) {
addPageIfNeeded(50);
addHeader(t.industryMatrix);
const { head, body } = transformMatrix(conclusion.industry_matrix, 'industry');
(doc as any).autoTable({
head: head, body: body, startY: yPos, theme: 'striped',
headStyles: { fillColor: [65, 90, 119] }, margin: { left: pageMargin }
});
yPos = (doc as any).lastAutoTable.finalY + 10;
}
}
// Battlecards
if (battlecards && battlecards.length > 0) {
addHeader(t.step7_title);
battlecards.forEach(card => {
addPageIfNeeded(60);
doc.setFontSize(12);
doc.setFont('helvetica', 'bold');
doc.text(`${t.against} ${card.competitor_name}`, pageMargin, yPos);
yPos += 6;
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
yPos += addWrappedText(doc, `${t.focus}: ${card.competitor_profile.focus}`, pageMargin + 5, yPos, contentWidth - 5);
yPos += addWrappedText(doc, `${t.strengthsVsWeaknesses}:\n - ${(card.strengths_vs_weaknesses || []).join('\n - ')}`, pageMargin + 5, yPos, contentWidth - 5);
yPos += addWrappedText(doc, `${t.landmineQuestions}:\n - ${(card.landmine_questions || []).join('\n - ')}`, pageMargin + 5, yPos, contentWidth - 5);
yPos += addWrappedText(doc, `${t.silverBullet}: "${card.silver_bullet}"`, pageMargin + 5, yPos, contentWidth - 5);
yPos += 8;
});
}
// References
if (reference_analysis && reference_analysis.length > 0) {
addPageIfNeeded(50);
addHeader(t.step8_title);
reference_analysis.forEach(analysis => {
addPageIfNeeded(30);
doc.setFontSize(12);
doc.setFont('helvetica', 'bold');
doc.text(analysis.competitor_name, pageMargin, yPos);
yPos += 6;
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
if ((analysis.references || []).length > 0) {
(analysis.references || []).forEach(ref => {
yPos += addWrappedText(doc, `${ref.name} (${ref.industry || 'N/A'}): "${ref.testimonial_snippet || ''}"`, pageMargin + 5, yPos, contentWidth - 5);
});
} else {
yPos += addWrappedText(doc, t.noReferencesFound, pageMargin + 5, yPos, contentWidth - 5);
}
yPos += 5;
});
}
// Add footer with page numbers
const pageCount = (doc as any).internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(8);
doc.text(`Page ${i} of ${pageCount}`, pageWidth - pageMargin, doc.internal.pageSize.getHeight() - 10, { align: 'right' });
}
doc.save(`analysis_${company.name}.pdf`);
};