Files
property-match/src/components/team/AgentChatStickyBar.tsx
T
Benjamin SutterandClaude Opus 5 9ed21e33a7 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>
2026-08-06 12:44:22 +02:00

123 lines
3.8 KiB
TypeScript

import { Box, IconButton, TextField, Typography } from '@mui/material'
import { SendHorizonal } from 'lucide-react'
import { AgentAvatar } from './AgentAvatar'
import { AGENT_CHAT_MARKER, useAgentChat } from './useAgentChat'
import { AGENT_CHAT_PROMPT_SHORT } from '../../lib/constants'
import { DS_BORDER, DS_TEXT } from '../../lib/ds'
import { useAgentChatStore } from '../../stores/agentChatStore'
/**
* Kompakter Chatzustand im obersten globalen Balken (Runde 5, §5).
*
* Er erscheint, sobald der ausführliche Chatkopf der Agentenseite nach oben
* weggescrollt ist, und zeigt nebeneinander Porträt, Name, Frage und
* Eingabefeld. Er liegt im Balken zwischen Seitenmenü und Mandant/Konto und
* nicht als Schwebeleiste über der Seite — so kann er Tabellen, Filter und
* Seitennavigation gar nicht erst verdecken (§10).
*
* Ohne eigenen Eingabezustand: Text, Fokus und Agent kommen aus
* `agentChatStore`, damit der Wechsel zwischen beiden Zuständen mitten im
* Tippen nichts verliert.
*/
export function AgentChatStickyBar() {
const compact = useAgentChatStore(s => s.compact)
const agent = useAgentChatStore(s => s.agent)
// Der Inhalt ist eine eigene Komponente, damit `useAgentChat` erst dann
// montiert wird, wenn der kompakte Zustand tatsächlich sichtbar ist. Läge der
// Aufruf hier, liefe sein Fokuseffekt schon beim Start der Anwendung ins Leere
// und der Fokus bliebe beim Scrollen im weggescrollten Feld hängen.
if (!agent || !compact) return null
return <StickyChat />
}
function StickyChat() {
const { agent, draft, inputRef, setDraft, send, handleKeyDown, onFocus, onBlur } = useAgentChat()
if (!agent) return null
return (
<Box
{...{ [AGENT_CHAT_MARKER]: '' }}
sx={{
flex: 1,
minWidth: 0,
display: 'flex',
alignItems: 'center',
gap: 1.25,
// Abstand zu Mandant, Benachrichtigungen und Konto auf der rechten Seite.
mr: { xs: 1, sm: 2 },
maxWidth: 720,
}}
>
<AgentAvatar agent={agent} size={34} loading="eager" />
<Typography
sx={{
fontWeight: 700,
fontSize: '0.875rem',
color: DS_TEXT.primary,
whiteSpace: 'nowrap',
flexShrink: 0,
}}
>
{agent.name}
</Typography>
{/*
Bei engen Breiten verschwindet zuerst der Hilfstext, danach schrumpft das
Eingabefeld (§10). Porträt und Name bleiben in jeder Breite stehen —
ohne sie wüsste niemand mehr, mit wem er schreibt.
*/}
<Typography
sx={{
fontSize: '0.8125rem',
color: DS_TEXT.secondary,
whiteSpace: 'nowrap',
flexShrink: 0,
display: { xs: 'none', lg: 'block' },
}}
>
{AGENT_CHAT_PROMPT_SHORT}
</Typography>
<TextField
size="small"
value={draft}
onChange={e => setDraft(e.target.value)}
onKeyDown={handleKeyDown}
onFocus={onFocus}
onBlur={onBlur}
inputRef={inputRef}
placeholder={`Nachricht an ${agent.name}`}
aria-label={`Nachricht an ${agent.name}`}
sx={{
flex: 1,
minWidth: 120,
'& .MuiOutlinedInput-root': {
borderRadius: 5,
bgcolor: 'white',
fontSize: '0.8125rem',
pr: 0.5,
'& fieldset': { borderColor: DS_BORDER.default },
},
}}
slotProps={{
input: {
endAdornment: (
<IconButton
size="small"
onClick={send}
disabled={!draft.trim()}
aria-label={`Nachricht an ${agent.name} senden`}
sx={{ color: DS_TEXT.primary }}
>
<SendHorizonal size={15} />
</IconButton>
),
},
}}
/>
</Box>
)
}