Globale Suche (Volltext + erweiterte Suche)
This commit is contained in:
@@ -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<br/>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.
|
||||
Reference in New Issue
Block a user