240 lines
10 KiB
Markdown
240 lines
10 KiB
Markdown
---
|
|
phase: 08-mobile-pwa
|
|
plan: 02
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- packages/portal/app/(dashboard)/chat/page.tsx
|
|
- packages/portal/components/chat-window.tsx
|
|
- packages/portal/components/chat-sidebar.tsx
|
|
- packages/portal/components/mobile-chat-header.tsx
|
|
- packages/portal/lib/use-visual-viewport.ts
|
|
- packages/portal/lib/use-chat-socket.ts
|
|
- packages/portal/messages/en.json
|
|
- packages/portal/messages/es.json
|
|
- packages/portal/messages/pt.json
|
|
autonomous: true
|
|
requirements:
|
|
- MOB-03
|
|
- MOB-06
|
|
|
|
must_haves:
|
|
truths:
|
|
- "On mobile, tapping a conversation shows full-screen chat with back arrow header"
|
|
- "Back arrow returns to conversation list on mobile"
|
|
- "Desktop two-column chat layout is unchanged"
|
|
- "Chat input stays visible when iOS virtual keyboard opens"
|
|
- "Message input is fixed at bottom, does not scroll away"
|
|
- "Streaming responses (word-by-word tokens) work on mobile"
|
|
- "No hover-dependent interactions break on touch devices"
|
|
artifacts:
|
|
- path: "packages/portal/components/mobile-chat-header.tsx"
|
|
provides: "Back arrow + agent name header for mobile full-screen chat"
|
|
exports: ["MobileChatHeader"]
|
|
- path: "packages/portal/lib/use-visual-viewport.ts"
|
|
provides: "Visual Viewport API hook for iOS keyboard offset"
|
|
exports: ["useVisualViewport"]
|
|
key_links:
|
|
- from: "packages/portal/app/(dashboard)/chat/page.tsx"
|
|
to: "packages/portal/components/mobile-chat-header.tsx"
|
|
via: "rendered when mobileShowChat is true on < md screens"
|
|
pattern: "MobileChatHeader"
|
|
- from: "packages/portal/components/chat-window.tsx"
|
|
to: "packages/portal/lib/use-visual-viewport.ts"
|
|
via: "keyboard offset applied to input container"
|
|
pattern: "useVisualViewport"
|
|
- from: "packages/portal/app/(dashboard)/chat/page.tsx"
|
|
to: "mobileShowChat state"
|
|
via: "toggles between conversation list and full-screen chat on mobile"
|
|
pattern: "mobileShowChat"
|
|
---
|
|
|
|
<objective>
|
|
Mobile-optimized chat experience with WhatsApp-style full-screen conversation flow, iOS keyboard handling, and touch-safe interactions.
|
|
|
|
Purpose: Chat is the primary user interaction on mobile. The two-column desktop layout doesn't work on small screens. This plan implements the conversation list -> full-screen chat pattern (like WhatsApp/iMessage) and handles the iOS virtual keyboard problem that breaks fixed inputs.
|
|
|
|
Output: Full-screen mobile chat with back navigation, Visual Viewport keyboard handling, touch-safe interaction patterns.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/08-mobile-pwa/08-CONTEXT.md
|
|
@.planning/phases/08-mobile-pwa/08-RESEARCH.md
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs. -->
|
|
|
|
From packages/portal/app/(dashboard)/chat/page.tsx:
|
|
```typescript
|
|
// ChatPageInner renders:
|
|
// - <div className="w-72 shrink-0"> with <ChatSidebar ... />
|
|
// - <div className="flex-1"> with <ChatWindow ... />
|
|
// State: activeConversationId, showAgentPicker
|
|
// handleSelectConversation sets activeConversationId + updates URL
|
|
// Container: <div className="flex h-[calc(100vh-4rem)] overflow-hidden">
|
|
```
|
|
|
|
From packages/portal/components/chat-window.tsx:
|
|
```typescript
|
|
export interface ChatWindowProps {
|
|
conversationId: string | null;
|
|
authHeaders: ChatSocketAuthHeaders;
|
|
}
|
|
// ActiveConversation renders:
|
|
// - Connection status banner
|
|
// - Message list: <div className="flex-1 overflow-y-auto px-4 py-4">
|
|
// - Input area: <div className="shrink-0 border-t px-4 py-3">
|
|
// Container: <div className="flex flex-col h-full">
|
|
```
|
|
|
|
From packages/portal/components/chat-sidebar.tsx:
|
|
```typescript
|
|
export interface ChatSidebarProps {
|
|
conversations: Conversation[];
|
|
activeId: string | null;
|
|
onSelect: (id: string) => void;
|
|
onNewChat: () => void;
|
|
}
|
|
```
|
|
|
|
From packages/portal/lib/use-chat-socket.ts:
|
|
```typescript
|
|
export type ChatSocketAuthHeaders = {
|
|
userId: string;
|
|
role: string;
|
|
tenantId: string | null;
|
|
}
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Mobile full-screen chat toggle and Visual Viewport keyboard hook</name>
|
|
<files>
|
|
packages/portal/app/(dashboard)/chat/page.tsx,
|
|
packages/portal/components/chat-window.tsx,
|
|
packages/portal/components/chat-sidebar.tsx,
|
|
packages/portal/components/mobile-chat-header.tsx,
|
|
packages/portal/lib/use-visual-viewport.ts
|
|
</files>
|
|
<action>
|
|
1. Create `lib/use-visual-viewport.ts` — hook to handle iOS keyboard offset:
|
|
```typescript
|
|
import { useState, useEffect } from 'react'
|
|
export function useVisualViewport() {
|
|
const [offset, setOffset] = useState(0)
|
|
useEffect(() => {
|
|
const vv = window.visualViewport
|
|
if (!vv) return
|
|
const handler = () => {
|
|
const diff = window.innerHeight - vv.height - vv.offsetTop
|
|
setOffset(Math.max(0, diff))
|
|
}
|
|
vv.addEventListener('resize', handler)
|
|
vv.addEventListener('scroll', handler)
|
|
return () => { vv.removeEventListener('resize', handler); vv.removeEventListener('scroll', handler) }
|
|
}, [])
|
|
return offset
|
|
}
|
|
```
|
|
|
|
2. Create `components/mobile-chat-header.tsx`:
|
|
- "use client" component
|
|
- Props: `agentName: string, onBack: () => void`
|
|
- Renders: `md:hidden` — only visible on mobile
|
|
- Layout: flex row with ArrowLeft icon button (onBack), agent avatar circle (first letter of agentName), agent name text
|
|
- Style: sticky top-0 z-10 bg-background border-b, h-14, items centered
|
|
- Back arrow: large tap target (min 44x44px), uses lucide ArrowLeft
|
|
|
|
3. Update `app/(dashboard)/chat/page.tsx` — add mobile full-screen toggle:
|
|
- Add state: `const [mobileShowChat, setMobileShowChat] = useState(false)`
|
|
- Modify `handleSelectConversation` to also call `setMobileShowChat(true)` (always, not just on mobile — CSS handles visibility)
|
|
- Update container: change `h-[calc(100vh-4rem)]` to `h-[calc(100dvh-4rem)] md:h-[calc(100vh-4rem)]` (dvh for mobile to handle iOS browser chrome)
|
|
- Chat sidebar panel: wrap with conditional classes:
|
|
```tsx
|
|
<div className={cn(
|
|
"md:w-72 md:shrink-0 md:block",
|
|
mobileShowChat ? "hidden" : "flex flex-col w-full"
|
|
)}>
|
|
```
|
|
- Chat window panel: wrap with conditional classes:
|
|
```tsx
|
|
<div className={cn(
|
|
"flex-1 md:block",
|
|
!mobileShowChat ? "hidden" : "flex flex-col w-full"
|
|
)}>
|
|
{mobileShowChat && (
|
|
<MobileChatHeader
|
|
agentName={activeConversationAgentName}
|
|
onBack={() => setMobileShowChat(false)}
|
|
/>
|
|
)}
|
|
<ChatWindow ... />
|
|
</div>
|
|
```
|
|
- Extract agent name from conversations array for the active conversation: `const activeConversationAgentName = conversations.find(c => c.id === activeConversationId)?.agent_name ?? 'AI Employee'`
|
|
- When URL has `?id=xxx` on mount and on mobile, set mobileShowChat to true:
|
|
```tsx
|
|
useEffect(() => {
|
|
if (urlConversationId) setMobileShowChat(true)
|
|
}, [urlConversationId])
|
|
```
|
|
- On mobile, the "New Chat" agent picker should also set mobileShowChat true after conversation creation (already handled by handleSelectConversation calling setMobileShowChat(true))
|
|
|
|
4. Update `components/chat-window.tsx` — keyboard-safe input on mobile:
|
|
- Import and use `useVisualViewport` in ActiveConversation
|
|
- Apply keyboard offset to the input container:
|
|
```tsx
|
|
const keyboardOffset = useVisualViewport()
|
|
// On the input area div:
|
|
<div className="shrink-0 border-t px-4 py-3"
|
|
style={{ paddingBottom: `calc(${keyboardOffset}px + env(safe-area-inset-bottom, 0px))` }}>
|
|
```
|
|
- When keyboardOffset > 0 (keyboard is open), auto-scroll to bottom of message list
|
|
- Change the EmptyState container from `h-full` to responsive: works both when full-screen and when sharing space
|
|
|
|
5. Update `components/chat-sidebar.tsx` — touch-optimized tap targets:
|
|
- Ensure conversation buttons have minimum 44px height (current py-3 likely sufficient, verify)
|
|
- The "New Conversation" button should have at least 44x44 tap target on mobile
|
|
- Replace any `hover:bg-accent` with `hover:bg-accent active:bg-accent` so touch devices get immediate feedback via the active pseudo-class (Tailwind v4 wraps hover in @media(hover:hover) already, but active provides touch feedback)
|
|
|
|
6. Add i18n key: `chat.backToConversations` in en/es/pt.json for the back button aria-label
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npm run build</automated>
|
|
</verify>
|
|
<done>
|
|
On mobile (< 768px): chat page shows conversation list full-width. Tapping a conversation shows full-screen chat with back arrow header. Back arrow returns to list. iOS keyboard pushes the input up instead of hiding it. Desktop two-column layout unchanged. Build passes. All chat functionality (send, streaming, typing indicator) works in both layouts.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `npm run build` passes in packages/portal
|
|
- Chat page renders conversation list on mobile by default
|
|
- Selecting a conversation shows full-screen chat with MobileChatHeader on mobile
|
|
- Back button returns to conversation list
|
|
- Desktop layout unchanged (two columns)
|
|
- Chat input stays visible when keyboard opens (Visual Viewport hook active)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
Mobile chat follows the WhatsApp-style pattern: conversation list full-screen, then full-screen chat with back arrow. Input is keyboard-safe on iOS. Touch interactions have immediate feedback. Desktop layout is unmodified.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/08-mobile-pwa/08-02-SUMMARY.md`
|
|
</output>
|