Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="./docs/static/img/yozakura_logo_horizontal.svg" width="420" />
</p>

Yozakura is an AI-powered social simulation in which characters (including the user) move around a map, interact with each other via the user's LLM of choice, and form memories and intentions towards each other, creating a dynamically evolving narrative with up to dozens or even hundreds of characters.
Yozakura is an AI-powered social simulation in which characters (including the user) move around a map, interact with each other via the user's LLM of choice, and form memories and intentions towards each other, creating a dynamically evolving narrative with up to dozens or even hundreds of characters. Read more about core features [here](https://mistval.github.io/yozakura/docs/intro).

## Installation

Expand Down
86 changes: 46 additions & 40 deletions client/src/components/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export default function ChatPane() {
const npcChatEndMode = useSettingsStore((s) => s.npcChatEndMode);
const intelligentChatEndMaxLength = useSettingsStore((s) => s.intelligentChatEndMaxLength);
const intelligentChatEndGroupMaxLength = useSettingsStore((s) => s.intelligentChatEndGroupMaxLength);
const editImagePromptsBeforeDispatch = useSettingsStore((s) => s.editImagePromptsBeforeDispatch);
const transcript = useTurnMachineStore((state) => state.transcript);
const chatMode = useActiveChatMedium();
const participants = useActiveChatParticipants();
Expand All @@ -46,13 +45,10 @@ export default function ChatPane() {
const userLocation = useChatUserLocation();

const [inputIsNonEmpty, setInputIsNonEmpty] = useState(false);
const [imagePrompt, setImagePrompt] = useState('');
const [showImagePrompt, setShowImagePrompt] = useState(false);
const [showChatSettings, setShowChatSettings] = useState(false);
const [settingsCharacterId, setSettingsCharacterId] = useState<string | undefined>(undefined);
const [editingMessageId, setEditingMessageId] = useState<string | undefined>(undefined);
const [editingMessageDraft, setEditingMessageDraft] = useState('');
const [imageAfterMessageId, setImageAfterMessageId] = useState<string | undefined>(undefined);

const transcriptContainerRef = useRef<HTMLDivElement | null>(null);
const autoScrollUnlockedRef = useRef(true);
Expand All @@ -65,18 +61,31 @@ export default function ChatPane() {
const submitChatDeleteMessage = useScenarioLoopStateStore((state) => state.submitChatDeleteMessage);
const submitChatRedoMessage = useScenarioLoopStateStore((state) => state.submitChatRedoMessage);
const submitChatEditMessage = useScenarioLoopStateStore((state) => state.submitChatEditMessage);
const submitChatRequestImage = useScenarioLoopStateStore((state) => state.submitChatRequestImage);
const submitChatGenerateImage = useScenarioLoopStateStore((state) => state.submitChatGenerateImage);
const userRequestedPhaseTransition = useScenarioLoopStateStore(
(state) => state.userRequestedPhaseTransition
);
const setUserRequestedPhaseTransition = useScenarioLoopStateStore(
(state) => state.setUserRequestedPhaseTransition
);
const setUserRequestedGenerationAbort = useScenarioLoopStateStore(
(state) => state.setUserRequestedGenerationAbort
);
const pendingImagePromptEdit = useScenarioLoopStateStore((state) => state.pendingImagePromptEdit);
const setPendingImagePromptEdit = useScenarioLoopStateStore((state) => state.setPendingImagePromptEdit);

const generatingAutoImage = chatState === 'generating_image';
const processingMemories = chatState === 'processing_memories';
const chatMemoryUpdateStatus = processingMemories ? (processingMemoryStatusInfo ?? '') : '';

const isGenerating =
chatState === 'character_speaking' ||
chatState === 'generating_image' ||
chatState === 'selecting_speaker' ||
chatState === 'judging_conversation_end' ||
chatState === 'processing_memories';

const participantById = useMemo(
() => Object.fromEntries(participants.map((participant) => [participant.id, participant])),
[participants]
Expand Down Expand Up @@ -171,31 +180,14 @@ export default function ChatPane() {
assertNonNullish(participant, `Participant with ID ${participantId} not found`);
submitChatSpeakAs(participant.id);
};

const openImagePrompt = async (afterMessageId?: string) => {
const imageCharacterId = afterMessageId
? transcript.getMessageById(afterMessageId)!.asCharacterChatMessage().senderId
: (transcript.getMostRecentSpeakerId() ?? primaryNpc.id);

const fullPrompt = await ChatCoordinator.buildSceneImageFullPrompt(imageCharacterId, {
upToMessageId: afterMessageId,
});

if (editImagePromptsBeforeDispatch) {
setImagePrompt(fullPrompt);
setShowImagePrompt(true);
setImageAfterMessageId(afterMessageId);
const confirmImagePrompt = () => {
if (!pendingImagePromptEdit) {
return;
}

submitChatGenerateImage(fullPrompt, afterMessageId);
setImageAfterMessageId(undefined);
};

const generateImageNow = () => {
setShowImagePrompt(false);
submitChatGenerateImage(imagePrompt, imageAfterMessageId);
setImageAfterMessageId(undefined);
const { prompt, afterMessageId } = pendingImagePromptEdit;
setPendingImagePromptEdit(undefined);
submitChatGenerateImage(prompt, afterMessageId);
};

const deleteMessage = (id: string) => {
Expand Down Expand Up @@ -485,7 +477,7 @@ export default function ChatPane() {
type="button"
onClick={() => {
const id = entry.getId();
openImagePrompt(id);
submitChatRequestImage(id);
}}
disabled={!isAwaitingCharacterInput}
title="Retry message"
Expand Down Expand Up @@ -551,13 +543,19 @@ export default function ChatPane() {
rows={2}
className="flex-1"
/>
<button
type="button"
onClick={send}
disabled={!isAwaitingCharacterInput || !inputIsNonEmpty || !isAwaitingCharacterInput}
>
Send
</button>
{isGenerating ? (
<button
type="button"
onClick={() => setUserRequestedGenerationAbort(true)}
aria-label="Stop generation"
>
</button>
) : (
<button type="button" onClick={send} disabled={!isAwaitingCharacterInput || !inputIsNonEmpty}>
Send
</button>
)}
{showSkipButton && (
<button
type="button"
Expand All @@ -570,17 +568,25 @@ export default function ChatPane() {
Skip
</button>
)}
<button type="button" onClick={() => openImagePrompt(undefined)} disabled={!isAwaitingCharacterInput}>
<button
type="button"
onClick={() => submitChatRequestImage(undefined)}
disabled={!isAwaitingCharacterInput}
>
Image
</button>
</div>

<ImagePromptModal
open={showImagePrompt}
prompt={imagePrompt}
onChange={setImagePrompt}
onCancel={() => setShowImagePrompt(false)}
onConfirm={generateImageNow}
open={Boolean(pendingImagePromptEdit)}
prompt={pendingImagePromptEdit?.prompt ?? ''}
onChange={(prompt) =>
setPendingImagePromptEdit(
pendingImagePromptEdit ? { ...pendingImagePromptEdit, prompt } : undefined
)
}
onCancel={() => setPendingImagePromptEdit(undefined)}
onConfirm={confirmImagePrompt}
/>

<ChatSettings open={showChatSettings} onClose={() => setShowChatSettings(false)} />
Expand Down
Loading
Loading