import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { AssistantRuntimeProvider } from '@assistant-ui/react' import type { ApiClient } from '@/api/client' import type { DecryptedMessage, ModelMode, PermissionMode, Session } from '@/types/api' import type { NormalizedMessage } from '@/chat/types' import { normalizeDecryptedMessage } from '@/chat/normalize' import { reduceChatBlocks } from '@/chat/reducer' import { Button } from '@/components/ui/button' import { HappyComposer } from '@/components/AssistantChat/HappyComposer' import { HappyThread } from '@/components/AssistantChat/HappyThread' import { useHappyRuntime } from '@/lib/assistant-runtime' import { SessionHeader } from '@/components/SessionHeader' import { MessageBubble } from '@/components/MessageBubble' import { getTelegramWebApp } from '@/hooks/useTelegram' export function SessionChat(props: { api: ApiClient session: Session messages: DecryptedMessage[] messagesWarning: string | null hasMoreMessages: boolean isLoadingMessages: boolean isLoadingMoreMessages: boolean isSending: boolean onBack: () => void onRefresh: () => void onLoadMore: () => void onSend: (text: string) => void onRetryMessage?: (localId: string) => void }) { const controlsDisabled = !props.session.active const normalizedCacheRef = useRef>(new Map()) useEffect(() => { normalizedCacheRef.current.clear() }, [props.session.id]) const normalizedMessages: NormalizedMessage[] = useMemo(() => { const cache = normalizedCacheRef.current const normalized: NormalizedMessage[] = [] const seen = new Set() for (const message of props.messages) { seen.add(message.id) const cached = cache.get(message.id) if (cached && cached.source === message) { if (cached.normalized) normalized.push(cached.normalized) continue } const next = normalizeDecryptedMessage(message) cache.set(message.id, { source: message, normalized: next }) if (next) normalized.push(next) } for (const id of cache.keys()) { if (!seen.has(id)) { cache.delete(id) } } return normalized }, [props.messages]) const reduced = useMemo(() => reduceChatBlocks(normalizedMessages, props.session.agentState), [normalizedMessages, props.session.agentState]) const [debugViewMode, setDebugViewMode] = useState<'reduced' | 'raw'>('reduced') const viewMode = import.meta.env.DEV ? debugViewMode : 'reduced' // Permission mode change handler const handlePermissionModeChange = useCallback(async (mode: PermissionMode) => { try { await props.api.setPermissionMode(props.session.id, mode as 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan') getTelegramWebApp()?.HapticFeedback?.notificationOccurred('success') props.onRefresh() } catch (e) { getTelegramWebApp()?.HapticFeedback?.notificationOccurred('error') console.error('Failed to set permission mode:', e) } }, [props.api, props.session.id, props.onRefresh]) // Model mode change handler const handleModelModeChange = useCallback(async (mode: ModelMode) => { try { await props.api.setModelMode(props.session.id, mode as 'default' | 'sonnet' | 'opus') getTelegramWebApp()?.HapticFeedback?.notificationOccurred('success') props.onRefresh() } catch (e) { getTelegramWebApp()?.HapticFeedback?.notificationOccurred('error') console.error('Failed to set model mode:', e) } }, [props.api, props.session.id, props.onRefresh]) // Abort handler const handleAbort = useCallback(async () => { await props.api.abortSession(props.session.id) props.onRefresh() }, [props.api, props.session.id, props.onRefresh]) const runtime = useHappyRuntime({ session: props.session, blocks: viewMode === 'raw' ? [] : reduced.blocks, isSending: props.isSending, onSendMessage: props.onSend, onAbort: handleAbort }) const threadHeader = useMemo(() => { if (props.isLoadingMessages) { return (
Loading…
) } return ( <> {props.messagesWarning ? (
{props.messagesWarning}
) : null} {import.meta.env.DEV ? (
) : null} {props.hasMoreMessages ? (
) : null} {import.meta.env.DEV && viewMode === 'reduced' && normalizedMessages.length === 0 && props.messages.length > 0 ? (
Message normalization returned 0 items for {props.messages.length} messages (see `hapi/web/src/chat/normalize.ts`).
) : null} {viewMode === 'raw' ? (
{props.messages.map((m) => ( props.onRetryMessage!(m.localId!) : undefined } /> ))}
) : null} ) }, [ props.isLoadingMessages, props.messagesWarning, props.hasMoreMessages, props.isLoadingMoreMessages, props.onLoadMore, props.messages, props.messages.length, props.onRetryMessage, viewMode, normalizedMessages.length ]) return (
{controlsDisabled ? (
Session is inactive. Controls are disabled.
) : null}
) }