Refactor advance time scene tool menu

This commit is contained in:
vegu-ai-tools
2026-04-03 16:27:44 +03:00
parent 673382199d
commit c3454bbd5f
4 changed files with 183 additions and 41 deletions

View File

@@ -10,6 +10,7 @@
- "Asset Exists Node: Added an allow_partial option that enables startswith matching instead of exact asset ID matching."
- "Prompt From Template Node: Added a dedupe property (default on) to control prompt deduplication. Useful for templates with structured repeated content like beat listings that should not be deduplicated."
- "Gemma 4 Prompt Template: Added a prompt template for Google's Gemma 4 model family, with support for toggling thinking mode via the reasoning tokens setting."
- "Advance Time: Moved time tools into a dedicated sub-component with grouped presets (Minutes, Hours, Days, Weeks, Months, Years) and a custom time dialog for arbitrary durations."
fixes:
- "Message Regeneration: Fixed a bug where regenerating a message that fails (e.g., missing agent or function) would permanently remove the original message from the scene. The original message is now restored to both history and UI on failure, and unhandled exceptions during regeneration are caught."
- "Anthropic Client: Fixed generation error caused by missing required max_tokens parameter when response length capping is disabled. Now defaults to the model's API output token limit."
@@ -24,5 +25,6 @@
- "Encryption: Fixed API keys in agent action configs (e.g., OpenAI-compatible TTS, visual backends) being stored in plaintext. The encryption walker now handles the nested AgentActionConfig structure where the key is wrapped in a {value: ...} dict."
- "Agent Config: Stopped persisting unified_api_key fields to config.yaml. These are static reference pointers (e.g., 'openai.api_key') defined in code and never change, so saving them was unnecessary."
- "Contextual Generate: Fixed list-type generation prefilling an empty line after '1.', which caused some LLMs to produce empty lists."
- "Advance Time: Fixed toolbar time advancement being completely non-functional due to a missing websocket handler. Also fixed invalid ISO 8601 duration strings for the 1 week and 2 weeks options."
improvements:
- "Prompts Tab Alert: The top-level Prompts tab now shows a warning icon when any active template overrides are outdated, so you can spot them without opening the tab."

View File

@@ -3,6 +3,7 @@ import pydantic
from talemate.instance import get_agent
from talemate.server.websocket_plugin import Plugin
from talemate.status import set_loading
from talemate.util.time import amount_unit_to_iso8601_duration
__all__ = [
"WorldStateWebsocketHandler",
@@ -18,6 +19,24 @@ class SummarizeAndPinPayload(pydantic.BaseModel):
num_messages: int = 5
class AdvanceTimePayload(pydantic.BaseModel):
duration: str | None = None
amount: int | None = None
unit: str | None = None
@pydantic.model_validator(mode="after")
def check_duration_or_amount_unit(self):
if not self.duration and (self.amount is None or not self.unit):
raise ValueError("Either 'duration' or 'amount' + 'unit' must be provided.")
return self
@property
def iso_duration(self) -> str:
if self.duration:
return self.duration
return amount_unit_to_iso8601_duration(self.amount, self.unit)
class WorldStateWebsocketHandler(Plugin):
router = "world_state_agent"
@@ -47,3 +66,9 @@ class WorldStateWebsocketHandler(Plugin):
message_id, num_messages=payload.num_messages
)
await self.signal_operation_done(allow_auto_save=False)
@set_loading("Advancing time", cancellable=True, as_async=True)
async def handle_advance_time(self, data: dict):
payload = AdvanceTimePayload(**data)
await self.agent.advance_time(payload.iso_duration)
await self.signal_operation_done()

View File

@@ -165,20 +165,7 @@
<!-- advance time -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn class="hotkey mx-1" v-bind="props" :disabled="appBusy || !appReady" color="primary" icon variant="text">
<v-icon>mdi-clock</v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-subheader>Advance Time</v-list-subheader>
<v-list-item density="compact" v-for="(option, index) in advanceTimeOptions" :key="index"
@click="advanceTime(option.value)">
<v-list-item-title density="compact" class="text-capitalize">{{ option.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<SceneToolsTime :disabled="appBusy || !appReady" />
<!-- world tools -->
<SceneToolsWorld
@@ -234,6 +221,7 @@ import SceneToolsVisual from './SceneToolsVisual.vue';
import SceneToolsWorld from './SceneToolsWorld.vue';
import SceneToolsSettings from './SceneToolsSettings.vue';
import SceneToolsSave from './SceneToolsSave.vue';
import SceneToolsTime from './SceneToolsTime.vue';
import RequestInput from './RequestInput.vue';
export default {
@@ -247,6 +235,7 @@ export default {
SceneToolsSave,
SceneToolsWorld,
SceneToolsSettings,
SceneToolsTime,
RequestInput,
},
props: {
@@ -312,29 +301,6 @@ export default {
{"value": "toggleAutoSave", "title": "Auto Save", "icon": "mdi-content-save", "description": "Automatically save after each game-loop", "status": () => { return this.canAutoSave ? this.autoSave : "Manually save scene for auto-save to be available"; }},
{"value": "toggleAutoProgress", "title": "Auto Progress", "icon": "mdi-robot", "description": "AI automatically progresses after player turn.", "status": () => { return this.autoProgress }},
],
advanceTimeOptions: [
{"value" : "P10Y", "title": "10 years"},
{"value" : "P5Y", "title": "5 years"},
{"value" : "P3Y", "title": "3 years"},
{"value" : "P2Y", "title": "2 years"},
{"value" : "P1Y", "title": "1 year"},
{"value" : "P6M", "title": "6 months"},
{"value" : "P3M", "title": "3 months"},
{"value" : "P1M", "title": "1 month"},
{"value" : "P14D:2 Weeks later", "title": "2 weeks"},
{"value" : "P7D:1 Week later", "title": "1 week"},
{"value" : "P3D", "title": "3 days"},
{"value" : "P2D", "title": "2 days"},
{"value" : "P1D", "title": "1 day"},
{"value" : "PT12H", "title": "12 hours"},
{"value" : "PT8H", "title": "8 hours"},
{"value" : "PT4H", "title": "4 hours"},
{"value" : "PT2H", "title": "2 hours"},
{"value" : "PT1H", "title": "1 hour"},
{"value" : "PT30M", "title": "30 minutes"},
{"value" : "PT15M", "title": "15 minutes"},
{"value" : "PT5M", "title": "5 minutes"}
],
}
},
inject: [
@@ -460,10 +426,6 @@ export default {
this.$emit('cancel-audio-queue');
},
advanceTime(duration) {
this.getWebsocket().send(JSON.stringify({ type: 'world_state_agent', action: 'advance_time', duration: duration }));
},
// Handle incoming messages
handleMessage(data) {

View File

@@ -0,0 +1,153 @@
<template>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn class="hotkey mx-1" v-bind="props" :disabled="disabled" color="primary" icon variant="text">
<v-icon>mdi-clock</v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-subheader>Advance Time</v-list-subheader>
<v-menu v-for="group in timeGroups" :key="group.label" open-on-hover location="end">
<template v-slot:activator="{ props }">
<v-list-item v-bind="props" @click.stop :prepend-icon="group.icon" append-icon="mdi-chevron-right" density="compact">
<v-list-item-title>{{ group.label }}</v-list-item-title>
</v-list-item>
</template>
<v-list density="compact">
<v-list-item v-for="option in group.options" :key="option.value"
density="compact" @click="advanceTime(option.value)">
<v-list-item-title class="text-capitalize">{{ option.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-divider />
<v-list-item density="compact" @click="openCustomTimeDialog">
<v-list-item-title>Custom...</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- Custom Time Dialog -->
<v-dialog v-model="customTimeDialog" max-width="400">
<v-card>
<v-card-title class="text-body-1">Advance Time</v-card-title>
<v-card-text>
<div class="d-flex align-center">
<v-number-input v-model="customTimeAmount" :min="1" label="Amount"
style="max-width: 180px" hide-details="auto" />
<v-select v-model="customTimeUnit" :items="customTimeUnits" label="Unit"
style="max-width: 180px" hide-details="auto" class="ml-2" />
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="customTimeDialog = false">Cancel</v-btn>
<v-btn color="primary" @click="submitCustomTime">Advance</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: "SceneToolsTime",
props: {
disabled: Boolean,
},
inject: ['getWebsocket'],
data() {
return {
customTimeDialog: false,
customTimeAmount: 1,
customTimeUnit: 'hours',
customTimeUnits: ['minutes', 'hours', 'days', 'weeks', 'months', 'years'],
timeGroups: [
{
label: "Minutes",
icon: "mdi-timer-sand",
options: [
{"value": "PT5M", "title": "5 minutes"},
{"value": "PT15M", "title": "15 minutes"},
{"value": "PT30M", "title": "30 minutes"},
],
},
{
label: "Hours",
icon: "mdi-clock-outline",
options: [
{"value": "PT1H", "title": "1 hour"},
{"value": "PT2H", "title": "2 hours"},
{"value": "PT4H", "title": "4 hours"},
{"value": "PT8H", "title": "8 hours"},
{"value": "PT12H", "title": "12 hours"},
],
},
{
label: "Days",
icon: "mdi-weather-sunny",
options: [
{"value": "P1D", "title": "1 day"},
{"value": "P2D", "title": "2 days"},
{"value": "P3D", "title": "3 days"},
],
},
{
label: "Weeks",
icon: "mdi-calendar-week",
options: [
{"value": "P7D", "title": "1 week"},
{"value": "P14D", "title": "2 weeks"},
],
},
{
label: "Months",
icon: "mdi-calendar-month",
options: [
{"value": "P1M", "title": "1 month"},
{"value": "P3M", "title": "3 months"},
{"value": "P6M", "title": "6 months"},
],
},
{
label: "Years",
icon: "mdi-calendar-multiple",
options: [
{"value": "P1Y", "title": "1 year"},
{"value": "P2Y", "title": "2 years"},
{"value": "P3Y", "title": "3 years"},
{"value": "P5Y", "title": "5 years"},
{"value": "P10Y", "title": "10 years"},
],
},
],
}
},
methods: {
advanceTime(duration) {
this.getWebsocket().send(JSON.stringify({
type: 'world_state_agent',
action: 'advance_time',
duration: duration,
}));
},
openCustomTimeDialog() {
this.customTimeAmount = 1;
this.customTimeUnit = 'hours';
this.customTimeDialog = true;
},
submitCustomTime() {
this.getWebsocket().send(JSON.stringify({
type: 'world_state_agent',
action: 'advance_time',
amount: this.customTimeAmount,
unit: this.customTimeUnit,
}));
this.customTimeDialog = false;
},
},
}
</script>