import {type Dispatch, type SetStateAction,useState} from 'react'; import { generateCharacterPanelChatSuggestions, generateCharacterPanelChatSummary, streamCharacterPanelChatReply, } from '../../services/aiService'; import type {StoryGenerationContext} from '../../services/aiTypes'; import type { Character, CharacterChatRecord, CharacterChatTurn, GameState, } from '../../types'; const MAX_CHARACTER_CHAT_TURNS = 24; export type CharacterChatTarget = { character: Character; npcId: string | null; roleLabel: string; hp: number; maxHp: number; mana: number; maxMana: number; affinity?: number | null; }; export type CharacterChatModalState = { target: CharacterChatTarget; draft: string; messages: CharacterChatTurn[]; suggestions: string[]; summary: string; isSending: boolean; isLoadingSuggestions: boolean; error: string | null; }; export interface CharacterChatUi { modal: CharacterChatModalState | null; openChat: (target: CharacterChatTarget) => void; closeChat: () => void; setDraft: (value: string) => void; useSuggestion: (value: string) => void; refreshSuggestions: () => void; sendDraft: () => void; } export function getCharacterChatRecord(state: GameState, characterId: string): CharacterChatRecord { return state.characterChats[characterId] ?? { history: [], summary: '', updatedAt: null, }; } export function trimCharacterChatHistory(history: CharacterChatTurn[]) { return history.slice(-MAX_CHARACTER_CHAT_TURNS); } export function buildLocalCharacterChatSummary( character: Character, history: CharacterChatTurn[], previousSummary: string, ) { const latestTurns = history .slice(-4) .map(turn => `${turn.speaker === 'player' ? '玩家' : character.name}: ${turn.text}`) .join(' '); const currentSummary = latestTurns ? `${character.name} 在私下对话中变得更加开放。最近的交流:${latestTurns}` : `${character.name} 愿意继续私下对话,并逐渐更加信任玩家。`; if (!previousSummary) { return currentSummary.slice(0, 118); } return `${previousSummary} ${currentSummary}`.slice(0, 118); } export function buildLocalCharacterChatSuggestions(character: Character) { return [ '请更清楚地告诉我你的意思。', `${character.name},你真正担心的是什么?`, '暂时放下大局。我想更好地了解你。', ]; } function buildCharacterChatRecordUpdate( state: GameState, characterId: string, record: CharacterChatRecord, ) { return { ...state, characterChats: { ...state.characterChats, [characterId]: record, }, }; } type CharacterChatTargetStatus = { roleLabel: string; hp: number; maxHp: number; mana: number; maxMana: number; affinity?: number | null; }; function buildTargetStatus(target: CharacterChatTarget): CharacterChatTargetStatus { return { roleLabel: target.roleLabel, hp: target.hp, maxHp: target.maxHp, mana: target.mana, maxMana: target.maxMana, affinity: target.affinity ?? null, }; } export function useCharacterChatFlow({ gameState, setGameState, buildStoryContextFromState, }: { gameState: GameState; setGameState: Dispatch>; buildStoryContextFromState: (state: GameState) => StoryGenerationContext; }) { const [characterChatModal, setCharacterChatModal] = useState(null); const loadCharacterChatSuggestions = async ( target: CharacterChatTarget, messages: CharacterChatTurn[], summary: string, ) => { if (!gameState.worldType || !gameState.playerCharacter) { setCharacterChatModal(current => current && current.target.character.id === target.character.id ? { ...current, suggestions: buildLocalCharacterChatSuggestions(target.character), isLoadingSuggestions: false, } : current, ); return; } setCharacterChatModal(current => current && current.target.character.id === target.character.id ? { ...current, isLoadingSuggestions: true, } : current, ); try { const suggestions = await generateCharacterPanelChatSuggestions( gameState.worldType, gameState.playerCharacter, target.character, gameState.storyHistory, buildStoryContextFromState(gameState), messages, summary, buildTargetStatus(target), ); setCharacterChatModal(current => current && current.target.character.id === target.character.id ? { ...current, suggestions, isLoadingSuggestions: false, } : current, ); } catch (error) { console.error('Failed to generate character chat suggestions:', error); setCharacterChatModal(current => current && current.target.character.id === target.character.id ? { ...current, suggestions: buildLocalCharacterChatSuggestions(target.character), isLoadingSuggestions: false, } : current, ); } }; const openCharacterChat = (target: CharacterChatTarget) => { const record = getCharacterChatRecord(gameState, target.character.id); setCharacterChatModal({ target, draft: '', messages: record.history, suggestions: buildLocalCharacterChatSuggestions(target.character), summary: record.summary, isSending: false, isLoadingSuggestions: true, error: null, }); void loadCharacterChatSuggestions(target, record.history, record.summary); }; const sendCharacterChatDraft = async () => { if (!characterChatModal || !gameState.worldType || !gameState.playerCharacter) { return; } const draft = characterChatModal.draft.trim(); if (!draft) { return; } const target = characterChatModal.target; const existingRecord = getCharacterChatRecord(gameState, target.character.id); const baseMessages = trimCharacterChatHistory(characterChatModal.messages); const nextMessages = trimCharacterChatHistory([ ...baseMessages, { speaker: 'player' as const, text: draft, }, ]); setCharacterChatModal(current => current && current.target.character.id === target.character.id ? { ...current, draft: '', messages: [...nextMessages, {speaker: 'character', text: ''}], suggestions: [], isSending: true, isLoadingSuggestions: true, error: null, } : current, ); let replyText = ''; try { replyText = await streamCharacterPanelChatReply( gameState.worldType, gameState.playerCharacter, target.character, gameState.storyHistory, buildStoryContextFromState(gameState), nextMessages, existingRecord.summary, draft, buildTargetStatus(target), { onUpdate: text => { setCharacterChatModal(current => current && current.target.character.id === target.character.id ? { ...current, messages: [...nextMessages, {speaker: 'character', text}], } : current, ); }, }, ); } catch (error) { console.error('Failed to stream character panel chat reply:', error); setCharacterChatModal(current => current && current.target.character.id === target.character.id ? { ...current, draft, messages: baseMessages, isSending: false, isLoadingSuggestions: false, error: error instanceof Error ? error.message : '未知智能生成错误', suggestions: current.suggestions.length > 0 ? current.suggestions : buildLocalCharacterChatSuggestions(target.character), } : current, ); return; } const finalMessages = trimCharacterChatHistory([ ...nextMessages, { speaker: 'character' as const, text: replyText, }, ]); let nextSummary = existingRecord.summary; try { nextSummary = await generateCharacterPanelChatSummary( gameState.worldType, gameState.playerCharacter, target.character, gameState.storyHistory, buildStoryContextFromState(gameState), finalMessages, existingRecord.summary, buildTargetStatus(target), ); } catch (error) { console.error('Failed to summarize character chat:', error); nextSummary = buildLocalCharacterChatSummary(target.character, finalMessages, existingRecord.summary); } const nextRecord: CharacterChatRecord = { history: finalMessages, summary: nextSummary, updatedAt: new Date().toISOString(), }; setGameState(current => buildCharacterChatRecordUpdate(current, target.character.id, nextRecord), ); setCharacterChatModal(current => current && current.target.character.id === target.character.id ? { ...current, messages: finalMessages, summary: nextSummary, isSending: false, error: null, } : current, ); await loadCharacterChatSuggestions(target, finalMessages, nextSummary); }; const characterChatUi: CharacterChatUi = { modal: characterChatModal, openChat: openCharacterChat, closeChat: () => setCharacterChatModal(null), setDraft: (value: string) => setCharacterChatModal(current => (current ? {...current, draft: value} : current)), useSuggestion: (value: string) => setCharacterChatModal(current => (current ? {...current, draft: value} : current)), refreshSuggestions: () => { if (!characterChatModal) { return; } void loadCharacterChatSuggestions( characterChatModal.target, characterChatModal.messages, characterChatModal.summary, ); }, sendDraft: () => { void sendCharacterChatDraft(); }, }; return { characterChatModal, setCharacterChatModal, characterChatUi, openCharacterChat, sendCharacterChatDraft, loadCharacterChatSuggestions, clearCharacterChatModal: () => setCharacterChatModal(null), }; }