mirror of
https://github.com/vegu-ai/talemate.git
synced 2026-09-01 19:48:52 +02:00
Background illustration followups: indicator, Immersive scene-tools toggle, per-kind display config (#58)
* Background illustration followups: split scene background/illustration display config, scene-tools Background toggle chip, active-backdrop indicator (closes #57)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
0.39.0.dev:
|
||||
features:
|
||||
- "Scene Illustration Background Mode: Scene illustrations gained a new 'Background' display size that renders the most recent illustration as a backdrop filling the whole scene view instead of showing it inline with messages. Message text sits on translucent blurred panels with a drop shadow, and the message input, control chips and buttons turn opaque so everything stays legible against the image; panel opacity and the text shadow are configurable. An Illustration chip on the message hover toolbar keeps the image menu (view, regenerate, edit, select) reachable, and the menu offers a 'Display as background' shortcut to switch into the mode. Configure under Settings → Appearance → Message Visuals."
|
||||
- "Scene Illustration Background Mode: Scene illustrations gained a new 'Background' display size that renders the most recent illustration as a backdrop filling the whole scene view instead of showing it inline with messages. Message text sits on translucent blurred panels with a drop shadow, and the message input, control chips and buttons turn opaque so everything stays legible against the image; panel opacity and the text shadow are configurable. Scene Backgrounds ('Visualize Scene (Background)') and Scene Illustrations ('Visualize Moment') are individually configurable, so environmental backgrounds can fill the backdrop while moment illustrations keep rendering inline — when both use Background, the most recent image is the active backdrop. An 'Immersive' quick-toggle chip in the scene tools flips the mode whenever the scene has a suitable background image, a small marker icon shows which message set the current backdrop (click it for the image menu), and an Illustration chip on the message hover toolbar keeps the image menu (view, regenerate, edit, select) reachable. Configure under Settings → Appearance → Message Visuals."
|
||||
- "Visual Prompt Finalization: The Visualizer agent gained a Prompt Finalization settings tab defining post-processing actions (exact, fuzzy or regex match and replace, or an AI instruction) that rewrite image prompts right before they are sent to the image generation backend. Actions can target positive and/or negative prompts, be restricted to specific visual types, and be overridden per scene. Characters can define their own actions under World Editor → Characters → Visuals → Prompt Finalization, which run after the agent's. Reusable action sets are managed as a new 'Visual prompt finalizer' template type, including a shipped Ideogram JSON preset that converts the positive prompt into an Ideogram 4.0 structured JSON prompt. Prompt-only generation output is finalized as well, and a new FinalizePrompt node exposes the step to custom node graphs."
|
||||
improvements:
|
||||
- "Uniform Settings Framework: Agent settings and client settings now share one field-definition schema on the backend and one field renderer on the frontend. Client-specific settings gain the full widget set previously exclusive to agents (sliders, autocompletes, selects with rich choices, per-value notes) plus conditional visibility, and choice lists are delivered to the frontend in a single normalized shape."
|
||||
|
||||
@@ -24,6 +24,8 @@ async_signals.register(
|
||||
"config.changed.follow",
|
||||
)
|
||||
|
||||
MESSAGE_ASSET_KINDS = ("avatar", "card", "scene_illustration", "scene_background")
|
||||
|
||||
|
||||
class Client(pydantic.BaseModel):
|
||||
"""
|
||||
@@ -593,13 +595,14 @@ class MarkupMessageStyle(HistoryMessageStyle):
|
||||
|
||||
class MessageAssetCadenceConfig(pydantic.BaseModel):
|
||||
cadence: Literal["always", "never", "on_change"] = "always"
|
||||
# "background" is only meaningful for scene_illustration assets
|
||||
# "background" is only meaningful for scene_illustration and
|
||||
# scene_background assets
|
||||
size: Literal["small", "medium", "big", "background"] = "medium"
|
||||
# opacity of the text panels rendered over the backdrop (scene_illustration
|
||||
# "background" size mode only)
|
||||
# opacity of the text panels rendered over the backdrop ("background"
|
||||
# size mode only)
|
||||
background_panel_opacity: float = pydantic.Field(default=0.8, ge=0.0, le=1.0)
|
||||
# drop shadow on message text over the backdrop (scene_illustration
|
||||
# "background" size mode only)
|
||||
# drop shadow on message text over the backdrop ("background" size mode
|
||||
# only)
|
||||
background_text_shadow: bool = True
|
||||
|
||||
|
||||
@@ -617,16 +620,27 @@ class SceneAppearance(pydantic.BaseModel):
|
||||
brackets: MarkupMessageStyle = MarkupMessageStyle()
|
||||
emphasis: MarkupMessageStyle = MarkupMessageStyle()
|
||||
entities: MarkupMessageStyle = MarkupMessageStyle()
|
||||
# scene_background and scene_illustration assets share the message-level
|
||||
# asset_type "scene_illustration"; separate entries keep their display
|
||||
# individually configurable
|
||||
message_assets: Dict[str, MessageAssetCadenceConfig] = pydantic.Field(
|
||||
default_factory=lambda: {
|
||||
"avatar": MessageAssetCadenceConfig(),
|
||||
"card": MessageAssetCadenceConfig(),
|
||||
"scene_illustration": MessageAssetCadenceConfig(),
|
||||
kind: MessageAssetCadenceConfig() for kind in MESSAGE_ASSET_KINDS
|
||||
}
|
||||
)
|
||||
|
||||
auto_attach_assets: bool = True
|
||||
|
||||
@pydantic.field_validator("message_assets", mode="after")
|
||||
@classmethod
|
||||
def ensure_default_message_asset_entries(cls, value):
|
||||
# configs saved before an entry existed (e.g. scene_background) come
|
||||
# in without it — fill the gaps so consumers can rely on all keys
|
||||
for key in MESSAGE_ASSET_KINDS:
|
||||
if key not in value:
|
||||
value[key] = MessageAssetCadenceConfig()
|
||||
return value
|
||||
|
||||
|
||||
class Appearance(pydantic.BaseModel):
|
||||
scene: SceneAppearance = SceneAppearance()
|
||||
|
||||
@@ -115,14 +115,42 @@
|
||||
></v-select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 12px;">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="mr-2">mdi-image-filter-hdr</v-icon>
|
||||
<div class="text-caption font-weight-medium">Scene Background</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding: 4px 12px;">
|
||||
<v-select
|
||||
v-model="config.scene_background.cadence"
|
||||
:items="cadenceOptionsNoChange"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 200px;"
|
||||
></v-select>
|
||||
</td>
|
||||
<td style="padding: 4px 12px;">
|
||||
<v-select
|
||||
v-model="config.scene_background.size"
|
||||
:items="sceneIllustrationSizeOptions"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 200px;"
|
||||
></v-select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<v-row v-if="config.scene_illustration.size === 'background'" class="mt-3">
|
||||
<v-row v-for="kind in backgroundConfiguredKinds" :key="kind" class="mt-3">
|
||||
<v-col cols="12" md="6">
|
||||
<v-slider
|
||||
v-model="config.scene_illustration.background_panel_opacity"
|
||||
label="Message panel opacity"
|
||||
v-model="config[kind].background_panel_opacity"
|
||||
:label="`Message panel opacity (${kindLabels[kind]})`"
|
||||
color="primary"
|
||||
:min="0"
|
||||
:max="1"
|
||||
@@ -134,8 +162,8 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-checkbox
|
||||
v-model="config.scene_illustration.background_text_shadow"
|
||||
label="Message text shadow"
|
||||
v-model="config[kind].background_text_shadow"
|
||||
:label="`Message text shadow (${kindLabels[kind]})`"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
@@ -149,7 +177,8 @@
|
||||
<strong>Always:</strong> Show visual on every message<br>
|
||||
<strong>Never:</strong> Never show visual inline with messages<br>
|
||||
<strong>On change:</strong> Only show when visual changes (portraits: tracked per character)<br><br>
|
||||
<strong>Scene Illustration sizes:</strong> Big = full width above message, Small/Medium = inline with text, Background = fills behind the scene text
|
||||
<strong>Scene Illustration</strong> covers images of the current moment ("Visualize Moment"), <strong>Scene Background</strong> covers purely environmental images ("Visualize Scene (Background)").<br>
|
||||
<strong>Sizes:</strong> Big = full width above message, Small/Medium = inline with text, Background = fills behind the scene text. When both types use Background, the most recent image is the active backdrop.
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -157,6 +186,33 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BACKDROP_ASSET_KINDS } from '@/constants/visual';
|
||||
|
||||
function defaultAssetConfig() {
|
||||
return {
|
||||
avatar: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
},
|
||||
card: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
},
|
||||
scene_illustration: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
background_panel_opacity: 0.8,
|
||||
background_text_shadow: true,
|
||||
},
|
||||
scene_background: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
background_panel_opacity: 0.8,
|
||||
background_text_shadow: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AppConfigAppearanceAssets',
|
||||
props: {
|
||||
@@ -167,21 +223,10 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
autoAttachAssets: true,
|
||||
config: {
|
||||
avatar: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
},
|
||||
card: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
},
|
||||
scene_illustration: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
background_panel_opacity: 0.8,
|
||||
background_text_shadow: true,
|
||||
},
|
||||
config: defaultAssetConfig(),
|
||||
kindLabels: {
|
||||
scene_illustration: 'Scene Illustration',
|
||||
scene_background: 'Scene Background',
|
||||
},
|
||||
cadenceOptions: [
|
||||
{ title: 'Always', value: 'always' },
|
||||
@@ -201,10 +246,17 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// "background" only makes sense for scene illustrations
|
||||
// "background" only makes sense for scene illustrations/backgrounds
|
||||
sceneIllustrationSizeOptions() {
|
||||
return [...this.sizeOptions, { title: 'Background', value: 'background' }];
|
||||
},
|
||||
// kinds currently set to the Background display size — each gets its
|
||||
// own panel-opacity / text-shadow controls
|
||||
backgroundConfiguredKinds() {
|
||||
return BACKDROP_ASSET_KINDS.filter(
|
||||
kind => this.config[kind].size === 'background'
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
immutableConfig: {
|
||||
@@ -213,22 +265,7 @@ export default {
|
||||
this.isHydrating = true;
|
||||
|
||||
if (!newVal) {
|
||||
this.config = {
|
||||
avatar: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
},
|
||||
card: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
},
|
||||
scene_illustration: {
|
||||
cadence: 'always',
|
||||
size: 'medium',
|
||||
background_panel_opacity: 0.8,
|
||||
background_text_shadow: true,
|
||||
},
|
||||
};
|
||||
this.config = defaultAssetConfig();
|
||||
this.isHydrating = false;
|
||||
return;
|
||||
}
|
||||
@@ -239,24 +276,15 @@ export default {
|
||||
// Load auto_attach_assets setting
|
||||
this.autoAttachAssets = sceneConfig.auto_attach_assets !== undefined ? sceneConfig.auto_attach_assets : true;
|
||||
|
||||
// Build config for all asset types with defaults
|
||||
this.config = {
|
||||
avatar: {
|
||||
cadence: messageAssets.avatar?.cadence || 'always',
|
||||
size: messageAssets.avatar?.size || 'medium',
|
||||
},
|
||||
card: {
|
||||
cadence: messageAssets.card?.cadence || 'always',
|
||||
size: messageAssets.card?.size || 'medium',
|
||||
},
|
||||
scene_illustration: {
|
||||
cadence: messageAssets.scene_illustration?.cadence || 'always',
|
||||
size: messageAssets.scene_illustration?.size || 'medium',
|
||||
// ?? not || — 0 and false are valid values
|
||||
background_panel_opacity: messageAssets.scene_illustration?.background_panel_opacity ?? 0.8,
|
||||
background_text_shadow: messageAssets.scene_illustration?.background_text_shadow ?? true,
|
||||
},
|
||||
};
|
||||
// Overlay stored values onto the defaults (?? so 0 / false
|
||||
// survive)
|
||||
const config = defaultAssetConfig();
|
||||
for (const [kind, entry] of Object.entries(config)) {
|
||||
for (const field of Object.keys(entry)) {
|
||||
entry[field] = messageAssets[kind]?.[field] ?? entry[field];
|
||||
}
|
||||
}
|
||||
this.config = config;
|
||||
|
||||
// Re-enable changed events after hydration completes
|
||||
this.$nextTick(() => {
|
||||
|
||||
@@ -16,13 +16,19 @@ export default {
|
||||
showAssetMenu: { default: null },
|
||||
getAssetFromCache: { default: null },
|
||||
requestSceneAssets: { default: null },
|
||||
// Provided by SceneMessages — splits scene_illustration assets into the
|
||||
// scene_background / scene_illustration config entries by vis_type
|
||||
resolveMessageAssetConfigKey: { default: null },
|
||||
},
|
||||
computed: {
|
||||
messageAssetDisplaySize() {
|
||||
const assetType = this.assetType || 'avatar';
|
||||
const configKey = this.resolveMessageAssetConfigKey
|
||||
? this.resolveMessageAssetConfigKey(this.assetId, assetType)
|
||||
: assetType;
|
||||
const messageAssets = this.appearanceConfig?.scene?.message_assets;
|
||||
if (messageAssets?.[assetType]?.size) {
|
||||
return messageAssets[assetType].size;
|
||||
if (messageAssets?.[configKey]?.size) {
|
||||
return messageAssets[configKey].size;
|
||||
}
|
||||
return 'medium';
|
||||
},
|
||||
|
||||
@@ -115,16 +115,6 @@
|
||||
Choose from existing scene illustrations
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="assetMenu.context.asset_type === 'scene_illustration' && !sceneIllustrationBackgroundMode"
|
||||
prepend-icon="mdi-image-area"
|
||||
@click="handleSetBackgroundDisplayMode"
|
||||
>
|
||||
<v-list-item-title>Display as background</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-wrap">
|
||||
Switch scene illustrations to the Background display mode
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider v-if="assetMenu.context.asset_type === 'card'"></v-divider>
|
||||
<v-list-item
|
||||
v-if="assetMenu.context.asset_type === 'card'"
|
||||
@@ -292,6 +282,12 @@
|
||||
|
||||
<div class="message-container mb-8" ref="messageContainer" :class="{ 'no-text-shadow': !sceneBackdropTextShadow }" :style="{ '--scene-backdrop-panel-opacity': sceneBackdropPanelOpacity }" style="flex-grow: 1; overflow-y: auto;" @click="onMessageContainerClick">
|
||||
<div v-for="(message, index) in messages" :key="message.id != null ? `${message.type}-${message.id}` : `idx-${index}`" class="message-wrapper">
|
||||
<!-- marks the message whose illustration is the active backdrop -->
|
||||
<v-tooltip v-if="sceneBackdrop && message.id != null && message.id === sceneBackdrop.messageId" text="This image is the current scene background" location="left">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" class="backdrop-indicator" size="small" color="primary" @click.stop="openBackdropIndicatorMenu($event, message)">mdi-image-area</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<div v-if="message.type === 'character' || message.type === 'processing_input'"
|
||||
:class="`message ${message.type}`" :id="`message-${message.id}`" :style="{ borderColor: message.color }">
|
||||
<div class="character-message">
|
||||
@@ -536,7 +532,7 @@ export default {
|
||||
ConfirmActionPrompt,
|
||||
AssetView,
|
||||
},
|
||||
emits: ['cancel-audio-queue', 'configure-entity-highlights', 'scene-backdrop'],
|
||||
emits: ['cancel-audio-queue', 'configure-entity-highlights', 'scene-backdrop', 'scene-backdrop-candidate'],
|
||||
data() {
|
||||
return {
|
||||
primaryModifierLabel,
|
||||
@@ -614,34 +610,48 @@ export default {
|
||||
messageAssetsConfig() {
|
||||
return this.appearanceConfig?.scene?.message_assets || null;
|
||||
},
|
||||
sceneIllustrationBackgroundMode() {
|
||||
return this.messageAssetsConfig?.scene_illustration?.size === 'background';
|
||||
},
|
||||
sceneBackdropPanelOpacity() {
|
||||
return this.messageAssetsConfig?.scene_illustration?.background_panel_opacity ?? 0.8;
|
||||
const kind = this.sceneBackdrop?.kind || 'scene_illustration';
|
||||
return this.messageAssetsConfig?.[kind]?.background_panel_opacity ?? 0.8;
|
||||
},
|
||||
sceneBackdropTextShadow() {
|
||||
return this.messageAssetsConfig?.scene_illustration?.background_text_shadow ?? true;
|
||||
const kind = this.sceneBackdrop?.kind || 'scene_illustration';
|
||||
return this.messageAssetsConfig?.[kind]?.background_text_shadow ?? true;
|
||||
},
|
||||
assetViewSrc() {
|
||||
return this.assetDataUrl(this.assetViewAssetId);
|
||||
},
|
||||
sceneBackdropAssetId() {
|
||||
// Most recent scene illustration becomes the backdrop
|
||||
if (!this.sceneIllustrationBackgroundMode) {
|
||||
return null;
|
||||
}
|
||||
sceneBackdrop() {
|
||||
// Most recent illustration whose config entry (scene_background
|
||||
// or scene_illustration, resolved by vis_type) is in "background"
|
||||
// display mode becomes the backdrop
|
||||
for (let i = this.messages.length - 1; i >= 0; i--) {
|
||||
const msg = this.messages[i];
|
||||
if (msg.asset_type === 'scene_illustration' && msg.asset_id) {
|
||||
return msg.asset_id;
|
||||
if (msg.asset_type !== 'scene_illustration' || !msg.asset_id) {
|
||||
continue;
|
||||
}
|
||||
const kind = this.messageAssetConfigKey(msg.asset_id, msg.asset_type);
|
||||
if (this.messageAssetsConfig?.[kind]?.size === 'background') {
|
||||
return { assetId: msg.asset_id, messageId: msg.id, kind };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
sceneBackdropAssetId() {
|
||||
return this.sceneBackdrop?.assetId || null;
|
||||
},
|
||||
sceneBackdropSrc() {
|
||||
return this.assetDataUrl(this.sceneBackdropAssetId);
|
||||
},
|
||||
// Whether the scene has any message-attached asset the scene-tools
|
||||
// "Immersive" chip could promote to a backdrop (independent of the
|
||||
// current display mode)
|
||||
sceneBackdropCandidateAvailable() {
|
||||
return this.messages.some(
|
||||
(msg) => msg.asset_id && msg.asset_type === 'scene_illustration' &&
|
||||
this.messageAssetConfigKey(msg.asset_id, msg.asset_type) === 'scene_background'
|
||||
);
|
||||
},
|
||||
editorRevisionsEnabled() {
|
||||
return this.agentStatus && this.agentStatus.editor && this.agentStatus.editor.actions && this.agentStatus.editor.actions["revision"] && this.agentStatus.editor.actions["revision"].enabled;
|
||||
},
|
||||
@@ -689,7 +699,7 @@ export default {
|
||||
return instructions;
|
||||
},
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'beginUxInteraction', 'endUxInteraction', 'clearUxInteractions', 'requestSceneAssets', 'openVisualLibraryWithAsset', 'setSceneIllustrationDisplaySize'],
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'beginUxInteraction', 'endUxInteraction', 'clearUxInteractions', 'requestSceneAssets', 'openVisualLibraryWithAsset'],
|
||||
provide() {
|
||||
return {
|
||||
requestDeleteMessage: this.requestDeleteMessage,
|
||||
@@ -713,6 +723,7 @@ export default {
|
||||
// Generate a visual asset for a context-investigation message
|
||||
visualizeMessage: this.visualizeMessage,
|
||||
isMessageVisualizing: this.isMessageVisualizing,
|
||||
resolveMessageAssetConfigKey: this.messageAssetConfigKey,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -1348,12 +1359,33 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle "Display as background" menu option — flips the
|
||||
* scene-illustration display mode to "background"
|
||||
* Resolve which message_assets config entry governs an asset's
|
||||
* display. Message-attached scene illustrations collapse both
|
||||
* SCENE_BACKGROUND and SCENE_ILLUSTRATION vis_types into the single
|
||||
* asset_type "scene_illustration"; the finer vis_type survives on
|
||||
* the asset meta and splits them into individually configurable
|
||||
* entries here.
|
||||
*/
|
||||
handleSetBackgroundDisplayMode() {
|
||||
this.assetMenu.show = false;
|
||||
this.setSceneIllustrationDisplaySize('background');
|
||||
messageAssetConfigKey(assetId, assetType) {
|
||||
if (assetType !== 'scene_illustration') {
|
||||
return assetType;
|
||||
}
|
||||
const visType = this.assetsMap[assetId]?.meta?.vis_type;
|
||||
return visType === VIS_TYPE.SCENE_BACKGROUND ? 'scene_background' : 'scene_illustration';
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the shared asset menu from the backdrop indicator icon —
|
||||
* in background mode there is no inline image to click on
|
||||
*/
|
||||
openBackdropIndicatorMenu(event, message) {
|
||||
this.showAssetMenu(event, {
|
||||
asset_id: message.asset_id,
|
||||
asset_type: message.asset_type,
|
||||
character: message.character || null,
|
||||
message_content: message.text || null,
|
||||
message_id: message.id,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2111,6 +2143,13 @@ export default {
|
||||
this.$emit('scene-backdrop', src);
|
||||
},
|
||||
},
|
||||
// Drives visibility of the scene-tools "Immersive" toggle chip
|
||||
sceneBackdropCandidateAvailable: {
|
||||
immediate: true,
|
||||
handler(available) {
|
||||
this.$emit('scene-backdrop-candidate', available);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.registerMessageHandler(this.handleMessage);
|
||||
@@ -2140,6 +2179,9 @@ export default {
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
/* slim right gutter so the backdrop indicator sits beside the panel,
|
||||
not over the message content */
|
||||
margin-right: 28px;
|
||||
/* tight, dense shadow so message text of every type stays legible
|
||||
over the backdrop */
|
||||
text-shadow: 1px 1px 2px #000000, 0 0 6px rgba(0, 0, 0, 0.85);
|
||||
@@ -2168,6 +2210,23 @@ export default {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* small always-on marker for the message that set the active backdrop;
|
||||
sits in the right gutter beside the message panel (over the backdrop
|
||||
image, hence the shadow) */
|
||||
.backdrop-indicator {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.9));
|
||||
}
|
||||
|
||||
.backdrop-indicator:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.message {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -3,19 +3,23 @@
|
||||
<v-sheet color="transparent" class="mb-2">
|
||||
<v-spacer></v-spacer>
|
||||
<!-- quick settings as v-chips -->
|
||||
<v-chip size="x-small" v-for="(option, index) in quickSettings" :key="index" @click="toggleQuickSetting(option.value)"
|
||||
:color="option.status() === true ? 'success' : 'grey'"
|
||||
:disabled="appBusy || !appReady" class="ma-1">
|
||||
<v-icon class="mr-1">{{ option.icon }}</v-icon>
|
||||
{{ option.title }}
|
||||
<v-icon class="ml-1" v-if="option.status() === true">mdi-check-circle-outline</v-icon>
|
||||
<v-icon class="ml-1" v-else-if="option.status() === false">mdi-circle-outline</v-icon>
|
||||
<v-tooltip v-else :text="option.status()">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon class="ml-1" v-bind="props" color="orange">mdi-alert-outline</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-chip>
|
||||
<v-tooltip v-for="(option, index) in visibleQuickSettings" :key="index" :text="option.description" location="top">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-chip size="x-small" v-bind="tooltipProps" @click="toggleQuickSetting(option.value)"
|
||||
:color="option.status() === true ? 'success' : 'grey'"
|
||||
:disabled="appBusy || !appReady" class="ma-1">
|
||||
<v-icon class="mr-1">{{ option.icon }}</v-icon>
|
||||
{{ option.title }}
|
||||
<v-icon class="ml-1" v-if="option.status() === true">mdi-check-circle-outline</v-icon>
|
||||
<v-icon class="ml-1" v-else-if="option.status() === false">mdi-circle-outline</v-icon>
|
||||
<v-tooltip v-else :text="option.status()">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon class="ml-1" v-bind="props" color="orange">mdi-alert-outline</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<SceneToolsSettings :app-busy="appBusy" :app-ready="appReady" />
|
||||
|
||||
@@ -224,6 +228,8 @@ import SceneToolsSave from './SceneToolsSave.vue';
|
||||
import SceneToolsTime from './SceneToolsTime.vue';
|
||||
import RequestInput from './RequestInput.vue';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
import { BACKDROP_ASSET_KINDS } from '@/constants/visual';
|
||||
|
||||
export default {
|
||||
|
||||
name: 'SceneTools',
|
||||
@@ -254,6 +260,12 @@ export default {
|
||||
agentStatus: Object,
|
||||
scene: Object,
|
||||
visualAgentReady: Boolean,
|
||||
// scene has a message-attached background-type asset the
|
||||
// "Immersive" chip could promote to a backdrop
|
||||
sceneBackdropCandidate: Boolean,
|
||||
// a backdrop is currently rendering (any kind) — keeps the chip
|
||||
// reachable so the mode can be toggled off
|
||||
sceneBackdropActive: Boolean,
|
||||
audioPlayedForMessageId: [Number, String],
|
||||
},
|
||||
computed: {
|
||||
@@ -287,7 +299,16 @@ export default {
|
||||
ttsAgentEnabled() {
|
||||
const ttsAgent = this.agentStatus?.tts;
|
||||
return ttsAgent && ttsAgent.available;
|
||||
}
|
||||
},
|
||||
|
||||
immersiveActive() {
|
||||
const messageAssets = this.appConfig()?.appearance?.scene?.message_assets;
|
||||
return BACKDROP_ASSET_KINDS.some(kind => messageAssets?.[kind]?.size === 'background');
|
||||
},
|
||||
|
||||
visibleQuickSettings() {
|
||||
return this.quickSettings.filter(option => !option.condition || option.condition());
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -312,7 +333,12 @@ export default {
|
||||
quickSettings: [
|
||||
{"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 }},
|
||||
{"value": "toggleImmersive", "title": "Immersive", "icon": "mdi-image-area", "description": "Render the most recent scene background image as a backdrop behind the scene", "condition": () => { return this.sceneBackdropCandidate || this.sceneBackdropActive }, "status": () => { return this.immersiveActive }},
|
||||
],
|
||||
// per-kind inline sizes to restore when Immersive is toggled off
|
||||
immersiveInlineSizes: {},
|
||||
// kinds to flip back to background when toggled on again
|
||||
immersiveKinds: ['scene_background'],
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
@@ -322,6 +348,7 @@ export default {
|
||||
'isWaitingForInput',
|
||||
'creativeEditor',
|
||||
'appConfig',
|
||||
'setMessageAssetDisplaySizes',
|
||||
'getTrackedCharacterState',
|
||||
'getTrackedWorldState',
|
||||
'getPlayerCharacterName',
|
||||
@@ -353,9 +380,35 @@ export default {
|
||||
} else if (setting == "toggleAutoProgress") {
|
||||
this.autoProgress = !this.autoProgress;
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'quick_settings', action: 'set', setting: 'auto_progress', value: this.autoProgress }));
|
||||
} else if (setting == "toggleImmersive") {
|
||||
this.toggleImmersive();
|
||||
}
|
||||
},
|
||||
|
||||
toggleImmersive() {
|
||||
const messageAssets = this.appConfig()?.appearance?.scene?.message_assets || {};
|
||||
const sizes = {};
|
||||
if (this.immersiveActive) {
|
||||
// restore every kind currently in background mode to its
|
||||
// remembered inline size
|
||||
for (const kind of BACKDROP_ASSET_KINDS) {
|
||||
if (messageAssets[kind]?.size === 'background') {
|
||||
sizes[kind] = this.immersiveInlineSizes[kind] || 'medium';
|
||||
}
|
||||
}
|
||||
this.immersiveKinds = Object.keys(sizes);
|
||||
} else {
|
||||
for (const kind of this.immersiveKinds) {
|
||||
const currentSize = messageAssets[kind]?.size;
|
||||
if (currentSize && currentSize !== 'background') {
|
||||
this.immersiveInlineSizes[kind] = currentSize;
|
||||
}
|
||||
sizes[kind] = 'background';
|
||||
}
|
||||
}
|
||||
this.setMessageAssetDisplaySizes(sizes);
|
||||
},
|
||||
|
||||
openWorldStateManager(tab, sub1, sub2, sub3) {
|
||||
this.$emit('open-world-state-manager', tab, sub1, sub2, sub3);
|
||||
},
|
||||
|
||||
@@ -265,6 +265,7 @@
|
||||
@cancel-audio-queue="onCancelAudioQueue"
|
||||
@configure-entity-highlights="onConfigureEntityHighlights"
|
||||
@scene-backdrop="sceneBackdropSrc = $event"
|
||||
@scene-backdrop-candidate="sceneBackdropCandidate = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -285,6 +286,8 @@
|
||||
:scene="scene"
|
||||
:activeCharacters="activeCharacters"
|
||||
:visual-agent-ready="visualAgentReady"
|
||||
:scene-backdrop-candidate="sceneBackdropCandidate"
|
||||
:scene-backdrop-active="!!sceneBackdropSrc"
|
||||
:audioPlayedForMessageId="audioPlayedForMessageId" />
|
||||
<SceneMessageInput
|
||||
ref="sceneMessageInput"
|
||||
@@ -458,6 +461,9 @@ export default {
|
||||
// data-url of the scene illustration acting as the scene backdrop
|
||||
// ("background" display mode), reported up by SceneMessages
|
||||
sceneBackdropSrc: null,
|
||||
// scene has a message-attached asset eligible for backdrop promotion
|
||||
// (drives the scene-tools "Immersive" toggle chip)
|
||||
sceneBackdropCandidate: false,
|
||||
tab: 'home',
|
||||
tabs: [
|
||||
{
|
||||
@@ -828,7 +834,7 @@ export default {
|
||||
appConfig: () => this.appConfig,
|
||||
openAppConfig: this.openAppConfig,
|
||||
openAgentActionOverrides: () => this.$refs.agentActionOverrides?.open(),
|
||||
setSceneIllustrationDisplaySize: this.setSceneIllustrationDisplaySize,
|
||||
setMessageAssetDisplaySizes: this.setMessageAssetDisplaySizes,
|
||||
configurationRequired: () => this.configurationRequired(),
|
||||
getTrackedCharacterState: (name, question) => this.$refs.worldState.trackedCharacterState(name, question),
|
||||
getTrackedCharacterStates: (name) => this.$refs.worldState.trackedCharacterStates(name),
|
||||
@@ -1349,9 +1355,10 @@ export default {
|
||||
}
|
||||
this.websocket.send(JSON.stringify({ type: 'configure_clients', clients: saveData }));
|
||||
},
|
||||
setSceneIllustrationDisplaySize(size) {
|
||||
// Shortcut used by the scene-illustration asset menu to flip the
|
||||
// display mode without opening the appearance settings.
|
||||
setMessageAssetDisplaySizes(sizes) {
|
||||
// Shortcut used by the scene-tools "Immersive" chip to flip
|
||||
// message-asset display modes ({kind: size}, applied in one save)
|
||||
// without opening the appearance settings.
|
||||
// IMPORTANT: the save handler validates the payload as the FULL config
|
||||
// model — a partial payload gets its missing sections replaced with
|
||||
// defaults. Always send the complete config (same as AppConfig.vue).
|
||||
@@ -1361,7 +1368,9 @@ export default {
|
||||
// appConfig is a full backend model dump — the nested structure
|
||||
// always exists
|
||||
const config = JSON.parse(JSON.stringify(this.appConfig));
|
||||
config.appearance.scene.message_assets.scene_illustration.size = size;
|
||||
for (const [assetKind, size] of Object.entries(sizes)) {
|
||||
config.appearance.scene.message_assets[assetKind].size = size;
|
||||
}
|
||||
this.websocket.send(JSON.stringify({ type: 'config', action: 'save', config }));
|
||||
},
|
||||
saveAgents(agents) {
|
||||
|
||||
@@ -22,6 +22,11 @@ export const VIS_TYPE_OPTIONS = [
|
||||
VIS_TYPE.UNSPECIFIED,
|
||||
];
|
||||
|
||||
// message_assets appearance-config kinds whose "background" display size
|
||||
// renders the asset as the scene backdrop (keys in
|
||||
// config.appearance.scene.message_assets, see config/schema.py)
|
||||
export const BACKDROP_ASSET_KINDS = ['scene_illustration', 'scene_background'];
|
||||
|
||||
// Must match FORMAT_TYPE in src/talemate/agents/visual/schema.py
|
||||
export const FORMAT_TYPE = Object.freeze({
|
||||
LANDSCAPE: 'LANDSCAPE',
|
||||
|
||||
@@ -32,6 +32,7 @@ from talemate.config.schema import (
|
||||
InferencePresetGroup,
|
||||
InferencePresets,
|
||||
RecentScene,
|
||||
SceneAppearance,
|
||||
)
|
||||
|
||||
|
||||
@@ -474,3 +475,32 @@ class TestCommitConfig:
|
||||
await config_state.commit_config()
|
||||
assert (tmp_path / "config.yaml").exists()
|
||||
assert isolated_config.dirty is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SceneAppearance.message_assets defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSceneAppearanceMessageAssets:
|
||||
DEFAULT_KEYS = {"avatar", "card", "scene_illustration", "scene_background"}
|
||||
|
||||
def test_defaults_include_all_entries(self):
|
||||
appearance = SceneAppearance()
|
||||
assert set(appearance.message_assets.keys()) == self.DEFAULT_KEYS
|
||||
|
||||
def test_missing_entries_are_filled(self):
|
||||
# configs saved before scene_background existed omit it — the
|
||||
# validator must fill the gap without touching stored values
|
||||
appearance = SceneAppearance(
|
||||
message_assets={
|
||||
"avatar": {"cadence": "never", "size": "small"},
|
||||
"card": {},
|
||||
"scene_illustration": {"size": "background"},
|
||||
}
|
||||
)
|
||||
assert set(appearance.message_assets.keys()) == self.DEFAULT_KEYS
|
||||
assert appearance.message_assets["avatar"].cadence == "never"
|
||||
assert appearance.message_assets["avatar"].size == "small"
|
||||
assert appearance.message_assets["scene_illustration"].size == "background"
|
||||
assert appearance.message_assets["scene_background"].size == "medium"
|
||||
|
||||
Reference in New Issue
Block a user