diff --git a/docs/12-suche.md b/docs/12-suche.md new file mode 100644 index 0000000..91e7c2e --- /dev/null +++ b/docs/12-suche.md @@ -0,0 +1,125 @@ +# 12 Globale Suche + +Ein Feld oben in der Kopfleiste, zwei Modi. Übernommen aus dem ICON CRM – +gleiche Gesten, gleicher Aufbau, damit niemand zweimal lernen muss, wo er +hinfassen soll. + +`Strg+K` (bzw. `Cmd+K`) springt von überall dorthin, `Esc` leert das Feld. + +| Modus | Symbol | Verhalten | +| --- | --- | --- | +| Volltext | Lupe | läuft beim Tippen mit (220 ms Verzögerung), Treffer nach Bereichen gruppiert | +| Erweitert | Funke | eine Frage in Worten, beantwortet vom Modell – erst auf Enter | + +## Was durchsucht wird + +```mermaid +flowchart LR + Q["Suchfeld"] --> API["GET /api/search?q="] + API --> L[("crm_leads")] + API --> O[("crm_opportunities")] + API --> A[("crm_acquisitions")] + API --> E[("crm_events")] + API --> IC["ICON CRM
fetchIconContacts()"] +``` + +| Gruppe | Quelle | Felder | +| --- | --- | --- | +| Kontakte | `crm_leads` | Name, Firma, E-Mail, Telefon, Mobil, Position, Tags, Notizen, Ort | +| Kontakte (ICON CRM) | Schnittstelle | Name, Position, Firma, E-Mail, Telefon, Tags | +| Opportunities | `crm_opportunities` | Titel, Firma, Beschreibung, Notizen, Tags, nächster Schritt | +| Akquise | `crm_acquisitions` | Titel, Firma, Beschreibung, Notizen, Tags, Absender, Anbieter, Berater | +| Termine | `crm_events` | Titel, Beschreibung, Ort | + +Gelöschtes (`deleted_at`) bleibt außen vor. Kontakte aus dem Vorbereich „Aus +Mails" (`mail_pool = 1`) erscheinen mit, aber hinter den gepflegten und mit dem +Hinweis „aus dem Postfach" – sie sind oft unvollständig. + +Bewusst `LIKE` statt Volltextindex: die Datenmengen sind klein, und ein +`FULLTEXT`-Index hülfe bei Teilwortsuche („informat" soll „Finanz Informatik" +finden) nicht. + +Die Kontakte des ICON CRM liegen nicht in dieser Datenbank; sie kommen über die +Schnittstelle (`fetchIconContacts()`, eine Minute Zwischenspeicher) und werden +im Speicher gefiltert. Fällt das andere Haus aus, fehlt nur diese Gruppe – die +übrige Suche bleibt benutzbar. Ihre Treffer sind **gold umrandet** und öffnen +nichts: gepflegt werden sie drüben. + +## Was ein Treffer öffnet + +| Gruppe | Klick führt zu | +| --- | --- | +| Kontakte | die Kontaktkarte (`goTo('Kontakte', { leadId })`) | +| Opportunities | die Karte der Opportunity (`goTo('Opportunities', { opportunityId })`) | +| Akquise | die Akquise-Ansicht | +| Termine | die Interviews-Ansicht | +| Kontakte (ICON CRM) | nichts – nur Anzeige | + +Für Kontakt und Opportunity gibt es einen Direktaufruf: `navigation.goTo()` +nimmt dafür `leadId` bzw. `opportunityId` entgegen. Akquise und Termine kennen +keinen – dort wechselt der Treffer in die Ansicht. + +## Erweiterte Suche + +```mermaid +sequenceDiagram + actor B as Benutzer + participant E as Express + participant M as MariaDB + participant O as OpenAI + + B->>E: POST /api/search/ai { question } + E->>M: Bestand lesen (Kontakte, Opportunities, Akquise, Termine) + E->>E: zusammenfassen, ab 400 Zeilen je Liste kuerzen + E->>O: Systemprompt + Bestand + Frage + O-->>E: { answer, refs } + E-->>B: Antwort + anklickbare Chips +``` + +Sie läuft **nicht** beim Tippen mit, sondern erst auf Enter – jede Anfrage +kostet. Und sie erzeugt **kein SQL**: der Bestand wird mitgegeben. Ein Modell, +das Abfragen auf die Datenbank schreiben darf, wäre eine Angriffsfläche; der +Bestand ist klein genug, um ihn vollständig mitzuliefern. Wird gekürzt, sagt die +Antwort das dazu. + +Die genannten Datensätze kommen als `refs` zurück und stehen unter der Antwort +als anklickbare Schaltflächen. + +Drei Regeln stehen ausdrücklich im Systemprompt, weil die Antworten sonst falsch +werden: + +* `daily_rate` ist ein **Tagessatz**; `null` heißt *nicht hinterlegt*, nicht + *null Euro*. Verglichen wird nur, wo ein Wert steht. +* Kontakte mit `mail_pool` sind noch nicht gepflegt – sie zählen mit, werden + aber als aus dem Postfach stammend benannt. +* `icon_contacts` sind Kontakte des ICON CRM. Ihre Kennung ist eine Zeichenkette + (`icon-31`) und gehört **nicht** in `refs`. + +## Schlüssel + +Die erweiterte Suche benutzt **denselben OpenAI-Schlüssel wie der Scanner** – +`openai_api_key` in der Scanner-Tabelle `settings`, gepflegt unter +*Einstellungen → Scanner*, ersatzweise `OPENAI_API_KEY` aus der Umgebung. Ein +zweiter Schlüssel an zweiter Stelle wäre eine Quelle für den Fall „die eine +Funktion geht, die andere nicht, und keiner weiß warum". + +Modell: `gpt-4o-mini`, wie im Scanner. Fehlt der Schlüssel, antwortet der +Endpunkt mit `503` und dem Hinweis auf die Einstellungen; die Volltextsuche +bleibt davon unberührt. + +Der Aufruf liegt in `server/src/ai.js` – bewusst ohne das `openai`-Paket: +gebraucht wird ein einziger Aufruf, und `fetch` ist in Node eingebaut. Der +Scanner behält sein SDK, dort hängt mehr daran. + +## Dateien + +| Datei | Aufgabe | +| --- | --- | +| `server/src/routes/search.js` | beide Endpunkte | +| `server/src/ai.js` | OpenAI-Aufruf, Schlüssel des Scanners | +| `server/src/routes/iconCrm.js` | `fetchIconContacts()` samt Zwischenspeicher | +| `src/components/GlobalSearch.tsx` | das Feld, beide Modi, die Trefferliste | +| `src/components/Header.tsx` | bindet es mittig in die Kopfleiste ein | + +Auf schmalen Fenstern (< 768 px) wird das Feld ausgeblendet – dort ist der Titel +der Ansicht wichtiger.