feat(ca): Add analysis loading feature and update documentation to v5-Stable
This commit is contained in:
@@ -134,6 +134,20 @@ Der Validierungslauf mit `analysis_robo-planet.de-4.json` bestätigt den Erfolg
|
|||||||
|
|
||||||
**Status:** ✅ **MIGRATION COMPLETE & VERIFIED.**
|
**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.*
|
*Dokumentation finalisiert am 12.01.2026.*
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
const handleUpdateAnalysis = useCallback((index: number, updatedAnalysis: any) => {
|
||||||
setAppState(prevState => {
|
setAppState(prevState => {
|
||||||
if (!prevState) return null;
|
if (!prevState) return null;
|
||||||
@@ -410,7 +419,7 @@ const App: React.FC = () => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="mx-auto">
|
<main className="mx-auto">
|
||||||
{!appState && !isLoading && <InputForm onStart={handleStartAnalysis} />}
|
{!appState && !isLoading && <InputForm onStart={handleStartAnalysis} onLoadAnalysis={handleLoadAnalysis} />}
|
||||||
|
|
||||||
{isLoading && !appState && <LoadingSpinner t={t.loadingSpinner} />}
|
{isLoading && !appState && <LoadingSpinner t={t.loadingSpinner} />}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { translations } from '../translations';
|
import { translations } from '../translations';
|
||||||
|
|
||||||
interface InputFormProps {
|
interface InputFormProps {
|
||||||
onStart: (startUrl: string, maxCompetitors: number, marketScope: string, language: 'de' | 'en') => void;
|
onStart: (startUrl: string, maxCompetitors: number, marketScope: string, language: 'de' | 'en') => void;
|
||||||
|
onLoadAnalysis: (state: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputForm: React.FC<InputFormProps> = ({ onStart }) => {
|
const InputForm: React.FC<InputFormProps> = ({ onStart, onLoadAnalysis }) => {
|
||||||
const [startUrl, setStartUrl] = useState('https://www.mobilexag.de');
|
const [startUrl, setStartUrl] = useState('https://www.mobilexag.de');
|
||||||
const [maxCompetitors, setMaxCompetitors] = useState(12);
|
const [maxCompetitors, setMaxCompetitors] = useState(12);
|
||||||
const [marketScope, setMarketScope] = useState('DACH');
|
const [marketScope, setMarketScope] = useState('DACH');
|
||||||
const [language, setLanguage] = useState<'de' | 'en'>('de');
|
const [language, setLanguage] = useState<'de' | 'en'>('de');
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const t = translations[language];
|
const t = translations[language];
|
||||||
|
|
||||||
@@ -18,6 +20,27 @@ const InputForm: React.FC<InputFormProps> = ({ onStart }) => {
|
|||||||
onStart(startUrl, maxCompetitors, marketScope, language);
|
onStart(startUrl, maxCompetitors, marketScope, language);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLoadClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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";
|
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 (
|
return (
|
||||||
@@ -81,13 +104,34 @@ const InputForm: React.FC<InputFormProps> = ({ onStart }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4">
|
<div className="pt-4 space-y-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full bg-brand-highlight hover:bg-blue-600 text-white font-bold py-3 px-4 rounded-lg shadow-lg transition duration-300 transform hover:scale-105"
|
className="w-full bg-brand-highlight hover:bg-blue-600 text-white font-bold py-3 px-4 rounded-lg shadow-lg transition duration-300 transform hover:scale-105"
|
||||||
>
|
>
|
||||||
{t.inputForm.submitButton}
|
{t.inputForm.submitButton}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div className="relative flex py-2 items-center">
|
||||||
|
<div className="flex-grow border-t border-light-accent dark:border-brand-accent"></div>
|
||||||
|
<span className="flex-shrink-0 mx-4 text-light-subtle dark:text-brand-light text-sm">{t.inputForm.loadAnalysisLabel}</span>
|
||||||
|
<div className="flex-grow border-t border-light-accent dark:border-brand-accent"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept=".json"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLoadClick}
|
||||||
|
className="w-full bg-light-accent hover:bg-gray-300 dark:bg-brand-accent dark:hover:bg-gray-600 text-light-text dark:text-white font-bold py-2 px-4 rounded-lg shadow transition duration-300"
|
||||||
|
>
|
||||||
|
{t.inputForm.loadButton}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const translations = {
|
|||||||
marketScopePlaceholder: "z.B. DACH, EU, global",
|
marketScopePlaceholder: "z.B. DACH, EU, global",
|
||||||
languageLabel: "Sprache",
|
languageLabel: "Sprache",
|
||||||
submitButton: "Analyse starten",
|
submitButton: "Analyse starten",
|
||||||
|
loadAnalysisLabel: "Oder bestehende Analyse laden",
|
||||||
|
loadButton: "Analyse-Datei (.json) hochladen"
|
||||||
},
|
},
|
||||||
loadingSpinner: {
|
loadingSpinner: {
|
||||||
message: "AI analysiert, bitte warten...",
|
message: "AI analysiert, bitte warten...",
|
||||||
@@ -158,6 +160,8 @@ export const translations = {
|
|||||||
marketScopePlaceholder: "e.g., DACH, EU, global",
|
marketScopePlaceholder: "e.g., DACH, EU, global",
|
||||||
languageLabel: "Language",
|
languageLabel: "Language",
|
||||||
submitButton: "Start Analysis",
|
submitButton: "Start Analysis",
|
||||||
|
loadAnalysisLabel: "Or load existing analysis",
|
||||||
|
loadButton: "Upload Analysis File (.json)"
|
||||||
},
|
},
|
||||||
loadingSpinner: {
|
loadingSpinner: {
|
||||||
message: "AI is analyzing, please wait...",
|
message: "AI is analyzing, please wait...",
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import requests
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
JSON_FILE = 'analysis_robo-planet.de-2.json'
|
JSON_FILE = 'analysis_robo-planet.de-4.json'
|
||||||
TOKEN_FILE = 'notion_token.txt'
|
TOKEN_FILE = 'notion_token.txt'
|
||||||
PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818"
|
PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818"
|
||||||
|
|
||||||
# Database Titles
|
# Database Titles
|
||||||
DB_TITLE_HUB = "📦 Competitive Radar (Companies) v5"
|
DB_TITLE_HUB = "📦 Competitive Radar (Companies) v6"
|
||||||
DB_TITLE_LANDMINES = "💣 Competitive Radar (Landmines) v5"
|
DB_TITLE_LANDMINES = "💣 Competitive Radar (Landmines) v6"
|
||||||
DB_TITLE_REFS = "🏆 Competitive Radar (References) v5"
|
DB_TITLE_REFS = "🏆 Competitive Radar (References) v6"
|
||||||
DB_TITLE_PRODUCTS = "🤖 Competitive Radar (Products) v5"
|
DB_TITLE_PRODUCTS = "🤖 Competitive Radar (Products) v6"
|
||||||
|
|
||||||
def load_json_data(filepath):
|
def load_json_data(filepath):
|
||||||
with open(filepath, 'r') as f:
|
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}
|
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)
|
r = requests.post(url, headers=headers, json=payload)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
print(f"Error {title}: {r.text}")
|
print(f"Error creating DB '{title}': {r.text}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return r.json()['id']
|
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"}
|
headers = {"Authorization": f"Bearer {token}", "Notion-Version": "2022-06-28", "Content-Type": "application/json"}
|
||||||
payload = {"parent": {"database_id": db_id}, "properties": properties}
|
payload = {"parent": {"database_id": db_id}, "properties": properties}
|
||||||
r = requests.post(url, headers=headers, json=payload)
|
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')
|
return r.json().get('id')
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
token = load_notion_token(TOKEN_FILE)
|
token = load_notion_token(TOKEN_FILE)
|
||||||
data = load_json_data(JSON_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, {
|
hub_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_HUB, {
|
||||||
"Name": {"title": {}},
|
"Name": {"title": {}},
|
||||||
"Website": {"url": {}},
|
"Website": {"url": {}},
|
||||||
@@ -60,43 +63,72 @@ def main():
|
|||||||
prod_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_PRODUCTS, {
|
prod_id = create_database(token, PARENT_PAGE_ID, DB_TITLE_PRODUCTS, {
|
||||||
"Product": {"title": {}},
|
"Product": {"title": {}},
|
||||||
"Category": {"select": {}},
|
"Category": {"select": {}},
|
||||||
|
"Purpose": {"rich_text": {}},
|
||||||
"Related Competitor": {"relation": {"database_id": hub_id, "dual_property": {"synced_property_name": "Products"}}}
|
"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 = {}
|
comp_map = {}
|
||||||
for analysis in data.get('analyses', []):
|
for analysis in data.get('analyses', []):
|
||||||
c = analysis['competitor']
|
c = analysis['competitor']
|
||||||
name = c['name']
|
name = c['name']
|
||||||
|
|
||||||
|
# v5: 'target_industries' is at root level of analysis object
|
||||||
|
industries = analysis.get('target_industries', [])
|
||||||
|
|
||||||
props = {
|
props = {
|
||||||
"Name": {"title": [{"text": {"content": name}}]},
|
"Name": {"title": [{"text": {"content": name}}]},
|
||||||
"Website": {"url": c['url'] or "https://google.com"},
|
"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)
|
pid = create_page(token, hub_id, props)
|
||||||
if pid:
|
if pid:
|
||||||
comp_map[name] = pid
|
comp_map[name] = pid
|
||||||
print(f" - Created: {name}")
|
print(f" - Created Company: {name}")
|
||||||
|
|
||||||
for prod in analysis.get('portfolio', []):
|
for prod in analysis.get('portfolio', []):
|
||||||
p_props = {
|
p_props = {
|
||||||
"Product": {"title": [{"text": {"content": prod['product'][:100]}}]},
|
"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}]}
|
"Related Competitor": {"relation": [{"id": pid}]}
|
||||||
}
|
}
|
||||||
create_page(token, prod_id, p_props)
|
create_page(token, prod_id, p_props)
|
||||||
|
|
||||||
|
# 3. Import Battlecards (Landmines)
|
||||||
for card in data.get('battlecards', []):
|
for card in data.get('battlecards', []):
|
||||||
cid = comp_map.get(card['competitor_name'])
|
cid = comp_map.get(card['competitor_name'])
|
||||||
if not cid: continue
|
if not cid: continue
|
||||||
for q in card.get('landmine_questions', []):
|
for q in card.get('landmine_questions', []):
|
||||||
|
# Handle both string and object formats from LLM
|
||||||
text = q['text'] if isinstance(q, dict) else q
|
text = q['text'] if isinstance(q, dict) else q
|
||||||
cat = q.get('category', 'General') if isinstance(q, dict) else 'General'
|
cat = q.get('category', 'General') if isinstance(q, dict) else 'General'
|
||||||
|
|
||||||
create_page(token, lm_id, {
|
create_page(token, lm_id, {
|
||||||
"Question": {"title": [{"text": {"content": text[:100]}}]},
|
"Question": {"title": [{"text": {"content": text[:100]}}]},
|
||||||
"Topic": {"select": {"name": cat}},
|
"Topic": {"select": {"name": cat}},
|
||||||
"Related Competitor": {"relation": [{"id": cid}]}
|
"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")
|
print("✅ DONE")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user