feat(runde-5): Startseite mit Agenten-Meetingraum, sticky Chat und Noras Kanäle
Hauptnavigation und Startseite (§2) - Hauptseite «Übersicht» samt Route, Menüeintrag, Hook, Service und den nur dort verwendeten Widgets entfernt; /supply/dashboard leitet auf die Startseite um, damit Lesezeichen nicht ins Leere laufen. - «Meine Objekte» heisst im Menü «Startseite»; die frühere Überschrift «Objektverwaltung» heisst jetzt «Meine Objekte». - Die Startseite scrollt als Ganzes. Die Höhe des Meetingraums rechnet gegen die Viewport-Höhe, damit Titel und Balken «Filter & Übersicht» beim Einstieg auf jeder Monitorgrösse sichtbar bleiben (geprüft 1280–1920 px). Agenten-Meetingraum (§3) - Zentralperspektive aus clip-path-Flächen, ohne 3D-Engine und ohne Animationsbibliothek: rechts geschlossene Wand, links und gegenüber boden- bis deckenhohe Glasfronten mit Blick auf Rhein, Basler Münster, Altstadt und Wettsteinbrücke. - Sitzordnung, Zielrouten, Grössen und Plakettenversatz liegen in einer zentralen Konfiguration (meetingRoomSeats.ts). - Neuer Agent Giorgio, Personalverwalter, sitzt vis-à-vis und führt in «Meine Agenten». Er steht bewusst nicht in AGENT_WORKSPACES: er hat keine Arbeitsseite, sondern ist die Adminfläche selbst. - Auf schmalen Schirmen tritt eine vereinfachte Kachelanordnung an die Stelle der Perspektive; ausgeblendet wird kein Agent. Sticky Chat bei allen Agenten (§5, §9–§11) - Scrollt der Chatkopf unter den globalen Balken, übernimmt dort eine kompakte Fassung mit Porträt, Name, Frage und Eingabefeld. - Ein gemeinsamer Zustand (agentChatStore) statt zweier Chat-Instanzen: Eingabetext und Fokus überstehen den Wechsel in beide Richtungen. - Noras Seite scrollte bisher gar nicht — Chatkopf ausserhalb des Scrollbereichs, zwei getrennt scrollende Spalten darunter. Behoben über den Seitenaufbau, nicht über eine Nora-Sonderlösung. Nora: CRM-Kanal und gemeinsame Lead-Liste (§12, §13) - Dritter Kanal «CRM» mit eigenem Domänentyp, Mockdaten, Provider, Service und Hook nach dem bestehenden Schichtenmuster. - Die Reiter «KI-Signale»/«Netzwerk» sind einer gemeinsamen Liste gewichen. Jede Zeile trägt ihre Kanalherkunft als Badge; die Herkunft steht im Datenmodell (LeadChannel) und wird nicht aus der Darstellung geraten. - Der frühere Reiterbalken ist der Filter «Kanaltyp» mit Alle/KI Signal/ Netzwerk/CRM, Standard «Alle», mit Empty State und Rücksetzung. Produkttour und Profilmenü (§8) - Tour auf die neue Struktur umgeschrieben: Startseite, Meetingraum, Agenten, sticky Chat, Leadkanäle. «Übersicht» kommt nicht mehr vor, «Meine Objekte» bezeichnet nur noch den Objektbereich. - Bereich «Demo-Modus» mit «Verwaltung» und «Bürosuche» entfernt. Geprüft: 413 Tests, Typecheck, Build, ESLint, Token-Schwelle (889/925). Im Browser: Navigation, Agentenklicks aller sechs Plätze, Kanalfilter und Detailschubladen, Sticky Chat bei vier Agenten, 1280–1920 px sowie Tablet- und Mobilbreite, keine 404 für Bilder oder Routen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import type { FocusEvent, KeyboardEvent } from 'react'
|
||||
import { useAgentChatStore } from '../../stores/agentChatStore'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
|
||||
/**
|
||||
* Markiert beide Eingabefelder des Agentenchats. Wandert der Fokus vom einen
|
||||
* zum anderen, ist das ein Zustandswechsel und kein Verlassen des Chats — ohne
|
||||
* diese Markierung löschte das `blur` des alten Feldes die Fokusnotiz, die das
|
||||
* neue Feld gerade braucht.
|
||||
*/
|
||||
export const AGENT_CHAT_MARKER = 'data-agent-chat'
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* Ob diese Darstellung gerade die sichtbare ist. Beide Zustände hängen
|
||||
* gleichzeitig im Baum; nur der sichtbare darf den Fokus an sich ziehen.
|
||||
*/
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemeinsame Chatlogik für den ausführlichen und den kompakten Zustand
|
||||
* (Runde 5, §5).
|
||||
*
|
||||
* Beide Darstellungen senden dieselbe Nachricht, reagieren gleich auf Enter und
|
||||
* reichen den Fokus über den Zustandswechsel hinweg weiter. Läge das in beiden
|
||||
* Komponenten, wäre jede Korrektur zweimal zu machen — und beim zweiten Mal
|
||||
* vergessen.
|
||||
*
|
||||
* Das Absenden bleibt Frontend-Simulation: es existiert kein Chat-Backend, und
|
||||
* eine erfundene Antwort wäre schlimmer als eine ehrliche Rückmeldung.
|
||||
*/
|
||||
export function useAgentChat({ active = true }: Options = {}) {
|
||||
const agent = useAgentChatStore(s => s.agent)
|
||||
const draft = useAgentChatStore(s => s.draft)
|
||||
const focused = useAgentChatStore(s => s.focused)
|
||||
const setDraft = useAgentChatStore(s => s.setDraft)
|
||||
const setFocused = useAgentChatStore(s => s.setFocused)
|
||||
const showToast = useToastStore(s => s.showToast)
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
|
||||
|
||||
/**
|
||||
* Wer beim Scrollen mitten im Satz ist, tippt nach dem Zustandswechsel
|
||||
* weiter — im jeweils sichtbaren Feld, mit dem Cursor am Ende des Textes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!active || !focused) return
|
||||
const el = inputRef.current
|
||||
if (!el || document.activeElement === el) return
|
||||
el.focus()
|
||||
const end = el.value.length
|
||||
el.setSelectionRange(end, end)
|
||||
}, [active, focused])
|
||||
|
||||
const send = useCallback(() => {
|
||||
const text = draft.trim()
|
||||
if (!text || !agent) return
|
||||
setDraft('')
|
||||
showToast(
|
||||
`Nachricht an ${agent.name} vorgemerkt — der Chat ist in dieser Demo noch nicht angebunden.`,
|
||||
'info',
|
||||
)
|
||||
}, [draft, agent, setDraft, showToast])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
send()
|
||||
}
|
||||
},
|
||||
[send],
|
||||
)
|
||||
|
||||
const onBlur = useCallback(
|
||||
(e: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
// Fokus wandert zum anderen Chatfeld → der Nutzer ist weiterhin im Chat.
|
||||
const next = e.relatedTarget
|
||||
if (next instanceof HTMLElement && next.closest(`[${AGENT_CHAT_MARKER}]`)) return
|
||||
// Das Feld verschwindet gerade aus dem Baum → Folge des Zustandswechsels,
|
||||
// keine Entscheidung des Nutzers.
|
||||
if (!e.currentTarget.isConnected) return
|
||||
setFocused(false)
|
||||
},
|
||||
[setFocused],
|
||||
)
|
||||
|
||||
return {
|
||||
agent,
|
||||
draft,
|
||||
inputRef,
|
||||
setDraft,
|
||||
send,
|
||||
handleKeyDown,
|
||||
onFocus: useCallback(() => setFocused(true), [setFocused]),
|
||||
onBlur,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user