377 lines
10 KiB
TypeScript
377 lines
10 KiB
TypeScript
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<SetStateAction<GameState>>;
|
|
buildStoryContextFromState: (state: GameState) => StoryGenerationContext;
|
|
}) {
|
|
const [characterChatModal, setCharacterChatModal] = useState<CharacterChatModalState | null>(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),
|
|
};
|
|
}
|