({ activeBookId, mode = activeBookId ? "book" : "book-create", nav, theme, t, sse: _sse }: ChatPageProps)
| 362 | // -- Component -- |
| 363 | |
| 364 | export function ChatPage({ activeBookId, mode = activeBookId ? "book" : "book-create", nav, theme, t, sse: _sse }: ChatPageProps) { |
| 365 | // -- Store selectors -- |
| 366 | const messages = useChatStore(chatSelectors.activeMessages); |
| 367 | const activeSession = useChatStore(chatSelectors.activeSession); |
| 368 | const activeSessionId = useChatStore((s) => s.activeSessionId); |
| 369 | const input = useChatStore((s) => s.input); |
| 370 | const loading = useChatStore(chatSelectors.isActiveSessionStreaming); |
| 371 | const selectedModel = useChatStore((s) => s.selectedModel); |
| 372 | const selectedService = useChatStore((s) => s.selectedService); |
| 373 | // -- Store actions -- |
| 374 | const setInput = useChatStore((s) => s.setInput); |
| 375 | const sendMessage = useChatStore((s) => s.sendMessage); |
| 376 | const setSelectedModel = useChatStore((s) => s.setSelectedModel); |
| 377 | const loadSessionList = useChatStore((s) => s.loadSessionList); |
| 378 | const createSession = useChatStore((s) => s.createSession); |
| 379 | const markProposalResolved = useChatStore((s) => s.markProposalResolved); |
| 380 | const loadSessionDetail = useChatStore((s) => s.loadSessionDetail); |
| 381 | const activateSession = useChatStore((s) => s.activateSession); |
| 382 | const setSessionPlayMode = useChatStore((s) => s.setSessionPlayMode); |
| 383 | |
| 384 | const scrollRef = useRef<HTMLDivElement>(null); |
| 385 | const scrollFrameRef = useRef<ScrollFrameId | null>(null); |
| 386 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| 387 | const autoScrollPinnedRef = useRef(true); |
| 388 | |
| 389 | const isZh = t("nav.connected") === "\u5DF2\u8FDE\u63A5"; |
| 390 | const hasBook = Boolean(activeBookId); |
| 391 | const currentSessionKind: ChatSessionKind = activeSession?.sessionKind |
| 392 | ?? (mode === "interactive-film-authoring" ? "interactive-film-authoring" |
| 393 | : mode === "book-create" ? "book-create" |
| 394 | : activeBookId ? "book" : "chat"); |
| 395 | const playMode = activeSession?.playMode; |
| 396 | // A play session must pick its playstyle (点着玩 / 自由玩) before chatting. |
| 397 | const needsPlayModeChoice = currentSessionKind === "play" && !playMode; |
| 398 | // Even in 点着玩 the world is shaped by free typing first; the choice panel |
| 399 | // only replaces the input once play has actually started (a play tool |
| 400 | // produced choices). |
| 401 | const playChoiceSet = useMemo( |
| 402 | () => (currentSessionKind === "play" && playMode === "guided" ? latestPlayChoiceSet(messages) : null), |
| 403 | [currentSessionKind, playMode, messages], |
| 404 | ); |
| 405 | const [consumedPlayChoiceKey, setConsumedPlayChoiceKey] = useState<string | null>(null); |
| 406 | const playChoices = playChoiceSet?.choices ?? []; |
| 407 | const showChoicePanel = shouldShowPlayChoicePanel({ |
| 408 | playMode, |
| 409 | choiceSetKey: playChoiceSet?.key ?? null, |
| 410 | consumedChoiceKey: consumedPlayChoiceKey, |
| 411 | choiceCount: playChoices.length, |
| 412 | }); |
| 413 | // World panel (holdings / state / relations) defaults collapsed; the scene |
| 414 | // image and choices live in the chat center now, opened on demand. |
| 415 | const [worldPanelOpen, setWorldPanelOpen] = useState(false); |
| 416 | const [playImageError, setPlayImageError] = useState<string | null>(null); |
| 417 | const [playImageMenuOpen, setPlayImageMenuOpen] = useState(false); |
| 418 | const [playImageSettings, setPlayImageSettings] = useState<PlayImageSettings>({ actors: false, moments: false, inventory: false }); |
| 419 | const [playImageCoverReady, setPlayImageCoverReady] = useState(false); |
| 420 | const [skillPanelOpen, setSkillPanelOpen] = useState(false); |
| 421 | const [selectedSkillIds, setSelectedSkillIds] = useState<string[]>([]); |
nothing calls this directly
no test coverage detected