add continue action to narrator messages

This commit is contained in:
vegu-ai-tools
2026-05-15 18:45:09 +03:00
parent 118d6ea73d
commit b677379a94
4 changed files with 75 additions and 22 deletions

View File

@@ -2,7 +2,7 @@
features:
- "Message Revision History: Regenerated AI messages now show a paginator above the message body. Click the arrows to browse previous regenerations; the version you're viewing becomes the canonical one the AI continues from. Lives in the browser session only."
improvements:
- "Message Revision History: Continuing a character message now creates a navigable revision entry tagged 'Continued', alongside the existing regenerate entries. The pre-continuation text is reachable via the paginator arrows."
- "Message Revision History: Continuing a character or narrator message now creates a navigable revision entry tagged 'Continued', alongside the existing regenerate entries. The pre-continuation text is reachable via the paginator arrows. Narrator messages also gain the Continue action on the hover toolbar to match the character-message flow."
- "Pydantic Migration: Internal data models across the codebase converted to pydantic for stricter validation. No user-visible behavior changes."
- "Character Sheet: Removed the read-only Character Sheet dialog and its button from the World State panel. The Manage character button (World State Manager) already covers viewing and editing character details."
- "Message Toolbar: Consolidated the hover toolbar shared by character, narrator, and context-investigation messages into a single component. Action chips now use a filled (tonal) style for better visibility, and the revision chip is labeled with the editor agent's configured revision method — 'Dedupe', 'Unslop', or 'Targeted Rewrite' — instead of the generic 'Editor Revision' label."

View File

@@ -70,7 +70,6 @@
:scene-rev="sceneRev"
>
<template #extra-actions>
<!-- generate continuation -->
<v-chip size="x-small" class="ml-2" label color="primary" v-if="!continuing && isLastMessage" variant="tonal" @click="continueConversation" :disabled="uxLocked || appBusy">
<v-icon class="mr-1">mdi-fast-forward</v-icon>
Continue
@@ -92,6 +91,7 @@
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
import { insertNewlineAtCursor } from '@/utils/textAreaUtils';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
import { spliceContinuation } from '@/utils/messageContinuation';
import MessageAssetImage from './MessageAssetImage.vue';
import MessageAssetMixin from './MessageAssetMixin.js';
import RevisionNav from './RevisionNav.vue';
@@ -305,24 +305,8 @@ export default {
return;
}
// if text ends with a quote and completion starts with a quote, remove the quotes
// and insert a period at the end of the current text
if (this.text.endsWith('"') && completion.startsWith('"')) {
completion = completion.slice(1);
let text = this.text.slice(0, -1);
this.editing_text = spliceContinuation(this.text, completion);
// if text does not end with a period, add one
if (!text.endsWith('.')) {
text += '.';
}
this.editing_text = text + " " + completion;
} else {
this.editing_text = this.text + completion;
}
// Tag the commit so the echo lands as a new entry on the
// slot's revision stack instead of replacing the active one.
this.submitEdit({
reason: 'continue',
mutation_source: 'continue',

View File

@@ -61,7 +61,18 @@
:tts-busy="ttsBusy"
:rev="rev"
:scene-rev="sceneRev"
/>
>
<template #extra-actions>
<v-chip size="x-small" class="ml-2" label color="narrator" v-if="!continuing && isLastMessage" variant="tonal" @click="continueNarration" :disabled="uxLocked || appBusy">
<v-icon class="mr-1">mdi-fast-forward</v-icon>
Continue
</v-chip>
<v-chip size="x-small" class="ml-2" label color="narrator" v-if="continuing && isLastMessage" variant="tonal" disabled>
<v-progress-circular class="mr-1" size="14" indeterminate="disable-shrink" color="narrator"></v-progress-circular>
Continuing...
</v-chip>
</template>
</MessageToolbar>
</div>
<div v-else>
<span class="text-muted text-caption">To edit the intro message open the <v-btn size="x-small" variant="text" color="primary" @click="openWorldStateManager('scene')"><v-icon>mdi-script</v-icon>Scene Editor</v-btn></span>
@@ -84,6 +95,7 @@
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
import { insertNewlineAtCursor } from '@/utils/textAreaUtils';
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
import { spliceContinuation } from '@/utils/messageContinuation';
import MessageAssetImage from './MessageAssetImage.vue';
import MessageAssetMixin from './MessageAssetMixin.js';
import RevisionNav from './RevisionNav.vue';
@@ -211,6 +223,7 @@ export default {
return {
editing: false,
autocompleting: false,
continuing: false,
editing_text: "",
hovered: false,
}
@@ -245,6 +258,31 @@ export default {
)
},
continueNarration() {
this.continuing = true;
this.autocompleteRequest(
{
partial: this.text,
context: "narrative:continue",
},
(completion) => {
this.continuing = false;
if (completion.trim() === "") {
return;
}
this.editing_text = spliceContinuation(this.text, completion);
this.submitEdit({
reason: 'continue',
mutation_source: 'continue',
});
},
this.$refs.textarea
)
},
cancelEdit() {
this.editing = false;
},
@@ -261,8 +299,15 @@ export default {
this.$refs.textarea.focus();
});
},
submitEdit() {
this.getWebsocket().send(JSON.stringify({ type: 'scene_message', action: 'edit', id: this.message_id, text: this.editing_text }));
submitEdit(meta = null) {
const payload = {
...(meta || {}),
type: 'scene_message',
action: 'edit',
id: this.message_id,
text: this.editing_text,
};
this.getWebsocket().send(JSON.stringify(payload));
this.editing = false;
},
deleteMessage() {

View File

@@ -0,0 +1,24 @@
/**
* Splice an autocompleted continuation onto a partial message body.
*
* When the partial closes a dialogue quote (ends in `"`) and the
* completion opens with a fresh quote, the two adjacent quote
* characters would collide on naive concatenation. Drop the dangling
* closing quote on the partial and the leading quote on the
* completion, ensure the partial ends with a period, and join with a
* space — yielding a clean break between the closed dialogue and the
* next sentence. Otherwise concatenate as-is.
*
* Shared between CharacterMessage and NarratorMessage continue flows.
*/
export function spliceContinuation(text, completion) {
if (text.endsWith('"') && completion.startsWith('"')) {
const tailCompletion = completion.slice(1);
let body = text.slice(0, -1);
if (!body.endsWith('.')) {
body += '.';
}
return body + ' ' + tailCompletion;
}
return text + completion;
}