From 5283f8a84bd1cbdee31bf87c76859ad2b0bc3d3a Mon Sep 17 00:00:00 2001 From: Floke Date: Mon, 12 Jan 2026 15:44:26 +0000 Subject: [PATCH] feat(ca): Add analysis loading feature and update documentation to v5-Stable --- MIGRATION_REPORT_COMPETITOR_ANALYSIS.md | 14 +++++ competitor-analysis-app/App.tsx | 11 +++- .../components/InputForm.tsx | 50 ++++++++++++++++-- competitor-analysis-app/translations.ts | 4 ++ import_competitive_radar.py | 52 +++++++++++++++---- 5 files changed, 117 insertions(+), 14 deletions(-) diff --git a/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md b/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md index 7f6f13e4..ba516182 100644 --- a/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md +++ b/MIGRATION_REPORT_COMPETITOR_ANALYSIS.md @@ -134,6 +134,20 @@ Der Validierungslauf mit `analysis_robo-planet.de-4.json` bestätigt den Erfolg **Status:** ✅ **MIGRATION COMPLETE & VERIFIED.** +### 📡 Zukunftsfähigkeit & Ausblick: Das Competitor Radar + +Neben der reinen Analyse wurde das Fundament für ein dauerhaftes Monitoring-System ("Competitor Radar") gelegt: + +1. **Persistence (State Loading):** + * **Feature:** Auf der Startseite wurde eine **Lade-Funktion** implementiert. Nutzer können nun eine zuvor exportierte `.json`-Datei hochladen, um den exakten Stand einer Analyse wiederherzustellen. Dies ermöglicht das Fortsetzen oder Aktualisieren von Berichten ohne Datenverlust. +2. **Vision: Aktives Monitoring:** + * In der nächsten Ausbaustufe wird das System von einer statischen On-Demand-Analyse zu einem dynamischen Radar transformiert. Ziel ist die automatisierte Überwachung der Wettbewerber auf: + * **Neue Produkt-Releases:** Automatischer Abgleich mit der "Grounded Truth". + * **Neuigkeiten & PR:** Scanning von News-Sektionen auf strategische Schwenks. + * **Messen & Events:** Identifikation von Marktpräsenz und Networking-Aktivitäten. +3. **Relationaler Import (v6):** + * Der Notion-Import wurde auf **v6** aktualisiert. Er unterstützt nun lückenlos die neue v5-Struktur, importiert Chain-of-Thought Beschreibungen in Rich-Text-Felder und verknüpft erstmals auch die extrahierten **Referenzkunden** relational in Notion. + --- *Dokumentation finalisiert am 12.01.2026.* diff --git a/competitor-analysis-app/App.tsx b/competitor-analysis-app/App.tsx index 7f750f5a..02c71f83 100644 --- a/competitor-analysis-app/App.tsx +++ b/competitor-analysis-app/App.tsx @@ -91,6 +91,15 @@ const App: React.FC = () => { } }, []); + const handleLoadAnalysis = useCallback((loadedState: AppState) => { + if (loadedState && loadedState.step && loadedState.company) { + setAppState(loadedState); + setHighestStep(loadedState.step); + } else { + alert("Ungültige Analyse-Datei."); + } + }, []); + const handleUpdateAnalysis = useCallback((index: number, updatedAnalysis: any) => { setAppState(prevState => { if (!prevState) return null; @@ -410,7 +419,7 @@ const App: React.FC = () => {
- {!appState && !isLoading && } + {!appState && !isLoading && } {isLoading && !appState && } diff --git a/competitor-analysis-app/components/InputForm.tsx b/competitor-analysis-app/components/InputForm.tsx index 1279513d..73b69f23 100644 --- a/competitor-analysis-app/components/InputForm.tsx +++ b/competitor-analysis-app/components/InputForm.tsx @@ -1,15 +1,17 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { translations } from '../translations'; interface InputFormProps { onStart: (startUrl: string, maxCompetitors: number, marketScope: string, language: 'de' | 'en') => void; + onLoadAnalysis: (state: any) => void; } -const InputForm: React.FC = ({ onStart }) => { +const InputForm: React.FC = ({ onStart, onLoadAnalysis }) => { const [startUrl, setStartUrl] = useState('https://www.mobilexag.de'); const [maxCompetitors, setMaxCompetitors] = useState(12); const [marketScope, setMarketScope] = useState('DACH'); const [language, setLanguage] = useState<'de' | 'en'>('de'); + const fileInputRef = useRef(null); const t = translations[language]; @@ -18,6 +20,27 @@ const InputForm: React.FC = ({ onStart }) => { onStart(startUrl, maxCompetitors, marketScope, language); }; + const handleLoadClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const json = JSON.parse(e.target?.result as string); + onLoadAnalysis(json); + } catch (error) { + console.error("Error parsing JSON:", error); + alert("Fehler beim Lesen der Datei. Ist es eine valide JSON-Datei?"); + } + }; + reader.readAsText(file); + } + }; + const inputClasses = "w-full bg-light-primary dark:bg-brand-primary text-light-text dark:text-brand-text border border-light-accent dark:border-brand-accent rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-brand-highlight placeholder-light-subtle dark:placeholder-brand-light"; return ( @@ -81,13 +104,34 @@ const InputForm: React.FC = ({ onStart }) => { -
+
+ +
+
+ {t.inputForm.loadAnalysisLabel} +
+
+ + +
diff --git a/competitor-analysis-app/translations.ts b/competitor-analysis-app/translations.ts index 26f7d388..05c7bf57 100644 --- a/competitor-analysis-app/translations.ts +++ b/competitor-analysis-app/translations.ts @@ -15,6 +15,8 @@ export const translations = { marketScopePlaceholder: "z.B. DACH, EU, global", languageLabel: "Sprache", submitButton: "Analyse starten", + loadAnalysisLabel: "Oder bestehende Analyse laden", + loadButton: "Analyse-Datei (.json) hochladen" }, loadingSpinner: { message: "AI analysiert, bitte warten...", @@ -158,6 +160,8 @@ export const translations = { marketScopePlaceholder: "e.g., DACH, EU, global", languageLabel: "Language", submitButton: "Start Analysis", + loadAnalysisLabel: "Or load existing analysis", + loadButton: "Upload Analysis File (.json)" }, loadingSpinner: { message: "AI is analyzing, please wait...", diff --git a/import_competitive_radar.py b/import_competitive_radar.py index c7d2fc92..d709d579 100644 --- a/import_competitive_radar.py +++ b/import_competitive_radar.py @@ -4,15 +4,15 @@ import requests import sys # Configuration -JSON_FILE = 'analysis_robo-planet.de-2.json' +JSON_FILE = 'analysis_robo-planet.de-4.json' TOKEN_FILE = 'notion_token.txt' PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818" # Database Titles -DB_TITLE_HUB = "📦 Competitive Radar (Companies) v5" -DB_TITLE_LANDMINES = "💣 Competitive Radar (Landmines) v5" -DB_TITLE_REFS = "🏆 Competitive Radar (References) v5" -DB_TITLE_PRODUCTS = "🤖 Competitive Radar (Products) v5" +DB_TITLE_HUB = "📦 Competitive Radar (Companies) v6" +DB_TITLE_LANDMINES = "💣 Competitive Radar (Landmines) v6" +DB_TITLE_REFS = "🏆 Competitive Radar (References) v6" +DB_TITLE_PRODUCTS = "🤖 Competitive Radar (Products) v6" def load_json_data(filepath): with open(filepath, 'r') as f: @@ -28,7 +28,7 @@ def create_database(token, parent_id, title, properties): payload = {"parent": {"type": "page_id", "page_id": parent_id}, "title": [{"type": "text", "text": {"content": title}}], "properties": properties} r = requests.post(url, headers=headers, json=payload) if r.status_code != 200: - print(f"Error {title}: {r.text}") + print(f"Error creating DB '{title}': {r.text}") sys.exit(1) return r.json()['id'] @@ -37,14 +37,17 @@ def create_page(token, db_id, properties): headers = {"Authorization": f"Bearer {token}", "Notion-Version": "2022-06-28", "Content-Type": "application/json"} payload = {"parent": {"database_id": db_id}, "properties": properties} r = requests.post(url, headers=headers, json=payload) + if r.status_code != 200: + print(f"Error creating page: {r.text}") return r.json().get('id') def main(): token = load_notion_token(TOKEN_FILE) data = load_json_data(JSON_FILE) - print("🚀 Level 4 Import starting...") + print("🚀 Level 5 Import starting (v6 Databases)...") + # 1. Create Databases hub_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_HUB, { "Name": {"title": {}}, "Website": {"url": {}}, @@ -60,43 +63,72 @@ def main(): prod_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_PRODUCTS, { "Product": {"title": {}}, "Category": {"select": {}}, + "Purpose": {"rich_text": {}}, "Related Competitor": {"relation": {"database_id": hub_id, "dual_property": {"synced_property_name": "Products"}}} }) + ref_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_REFS, { + "Customer": {"title": {}}, + "Industry": {"select": {}}, + "Quote": {"rich_text": {}}, + "Related Competitor": {"relation": {"database_id": hub_id, "dual_property": {"synced_property_name": "References"}}} + }) + + # 2. Import Companies & Products comp_map = {} for analysis in data.get('analyses', []): c = analysis['competitor'] name = c['name'] + + # v5: 'target_industries' is at root level of analysis object + industries = analysis.get('target_industries', []) + props = { "Name": {"title": [{"text": {"content": name}}]}, "Website": {"url": c['url'] or "https://google.com"}, - "Target Industries": {"multi_select": [{"name": i[:100]} for i in analysis.get('target_industries', [])]} + "Target Industries": {"multi_select": [{"name": i[:100].replace(',', '')} for i in industries if i]} } pid = create_page(token, hub_id, props) if pid: comp_map[name] = pid - print(f" - Created: {name}") + print(f" - Created Company: {name}") for prod in analysis.get('portfolio', []): p_props = { "Product": {"title": [{"text": {"content": prod['product'][:100]}}]}, - "Category": {"select": {"name": prod.get('purpose', 'Other')[:100]}}, + "Category": {"select": {"name": prod.get('category', 'Other')[:100]}}, + "Purpose": {"rich_text": [{"text": {"content": prod.get('purpose', '')[:2000]}}]}, "Related Competitor": {"relation": [{"id": pid}]} } create_page(token, prod_id, p_props) + # 3. Import Battlecards (Landmines) for card in data.get('battlecards', []): cid = comp_map.get(card['competitor_name']) if not cid: continue for q in card.get('landmine_questions', []): + # Handle both string and object formats from LLM text = q['text'] if isinstance(q, dict) else q cat = q.get('category', 'General') if isinstance(q, dict) else 'General' + create_page(token, lm_id, { "Question": {"title": [{"text": {"content": text[:100]}}]}, "Topic": {"select": {"name": cat}}, "Related Competitor": {"relation": [{"id": cid}]} }) + # 4. Import References + for ref_analysis in data.get('reference_analysis', []): + cid = comp_map.get(ref_analysis['competitor_name']) + if not cid: continue + for ref in ref_analysis.get('references', []): + create_page(token, ref_id, { + "Customer": {"title": [{"text": {"content": ref['name'][:100]}}]}, + "Industry": {"select": {"name": ref.get('industry', 'Unknown')[:100].replace(',', '')}}, + "Quote": {"rich_text": [{"text": {"content": ref.get('testimonial_snippet', '')[:2000]}}]}, + "Related Competitor": {"relation": [{"id": cid}]} + }) + print("✅ DONE") if __name__ == "__main__":