mirror of
https://github.com/vegu-ai/talemate.git
synced 2026-09-01 19:48:52 +02:00
feat: add information messages to SceneAppearance and enhance DirectorMessage
- Introduced `information_messages` to `SceneAppearance` for system notices. - Enhanced `DirectorMessage` with a new `subtype` property for categorization. - Updated UX elements to support new `BuildNoticeElement` for display-only messages. - Implemented color resolution for special message types, including information. - Refactored message color handling in frontend components to utilize new utility functions. - Added markdown rendering capabilities for plain text in UX elements. - Improved styling and behavior for various message types in the frontend.
This commit is contained in:
@@ -710,28 +710,28 @@
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "15ecf58a-8fed-4aac-be58-ae43827fcc79",
|
||||
"id": "d505505c-f22f-4d6c-97d5-cfc05d9c41e4",
|
||||
"name": "fn",
|
||||
"optional": false,
|
||||
"group": null,
|
||||
"socket_type": "function"
|
||||
},
|
||||
{
|
||||
"id": "30586a55-e9e1-4b8b-9554-826f69abc196",
|
||||
"id": "3b9e471a-c4ba-43e9-ab2a-829f21fe3434",
|
||||
"name": "name",
|
||||
"optional": false,
|
||||
"group": null,
|
||||
"socket_type": "str"
|
||||
},
|
||||
{
|
||||
"id": "87b6031d-5d53-4397-b38c-5d9ee70a18b3",
|
||||
"id": "5ad79ca7-370e-442b-a670-bfd9bcb38958",
|
||||
"name": "allow_multiple_calls",
|
||||
"optional": false,
|
||||
"group": null,
|
||||
"socket_type": "bool"
|
||||
},
|
||||
{
|
||||
"id": "da949bec-6c74-4503-87d2-d78ce63572f5",
|
||||
"id": "712b4cac-5fe5-42a8-aa1d-fa3caa50ba12",
|
||||
"name": "ai_callback",
|
||||
"optional": false,
|
||||
"group": null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -192,12 +192,13 @@
|
||||
"template": null,
|
||||
"max_calls": 8,
|
||||
"retries": 0,
|
||||
"response_length": 4096
|
||||
"response_length": 4096,
|
||||
"max_concurrent": 3
|
||||
},
|
||||
"x": 1822,
|
||||
"y": 1760,
|
||||
"width": 228,
|
||||
"height": 250,
|
||||
"height": 274,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "focal/Focal",
|
||||
@@ -663,7 +664,7 @@
|
||||
"x": 675,
|
||||
"y": 496,
|
||||
"width": 210,
|
||||
"height": 193,
|
||||
"height": 213,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "prompt/Jinja2Format",
|
||||
@@ -1007,12 +1008,13 @@
|
||||
"id": "cfaec2dd-2c88-4087-8678-43cadd6c3226",
|
||||
"properties": {
|
||||
"name": "my_function",
|
||||
"allow_multiple_calls": true
|
||||
"allow_multiple_calls": true,
|
||||
"allow_concurrent": false
|
||||
},
|
||||
"x": 420,
|
||||
"y": 1370,
|
||||
"width": 214,
|
||||
"height": 102,
|
||||
"height": 126,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "focal/Callback",
|
||||
@@ -1073,41 +1075,18 @@
|
||||
"registry": "agents/director/chat/DirectorChatSubAction",
|
||||
"base_type": "core/Node"
|
||||
},
|
||||
"d4071915-492a-41ac-b4f9-54e48a167c5c": {
|
||||
"title": "[Update Context] Story Configuration",
|
||||
"id": "d4071915-492a-41ac-b4f9-54e48a167c5c",
|
||||
"properties": {
|
||||
"group": "Update Context",
|
||||
"action_id": "update_context.story_configuration",
|
||||
"action_title": "Story Configuration",
|
||||
"description_chat": "Make changes to the story / experience's configuration: Title, Description, Introduction, Content Classification, Story Intention, Scene Intention and Scene Type.\n\nManage Contextual Pins (add, update, remove pins of world entries).",
|
||||
"description_scene_direction": "",
|
||||
"instruction_examples": [
|
||||
"Update the story intention to be more specific about the expected story's mature content."
|
||||
],
|
||||
"availability": "both",
|
||||
"force_enabled": false
|
||||
},
|
||||
"x": 860,
|
||||
"y": 1970,
|
||||
"width": 317,
|
||||
"height": 246,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "agents/director/chat/DirectorChatSubAction",
|
||||
"base_type": "core/Node"
|
||||
},
|
||||
"5e8fb08e-30c4-4c6a-97e0-8b4844598f4c": {
|
||||
"title": "AI Function Callback",
|
||||
"id": "5e8fb08e-30c4-4c6a-97e0-8b4844598f4c",
|
||||
"properties": {
|
||||
"name": "direct_context_change",
|
||||
"allow_multiple_calls": true
|
||||
"allow_multiple_calls": true,
|
||||
"allow_concurrent": false
|
||||
},
|
||||
"x": 950,
|
||||
"y": 2279,
|
||||
"width": 214,
|
||||
"height": 102,
|
||||
"height": 126,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "focal/Callback",
|
||||
@@ -1166,12 +1145,13 @@
|
||||
"id": "323899a1-ffba-4d63-af97-4a25f11bf00f",
|
||||
"properties": {
|
||||
"name": "my_function",
|
||||
"allow_multiple_calls": true
|
||||
"allow_multiple_calls": true,
|
||||
"allow_concurrent": false
|
||||
},
|
||||
"x": 380,
|
||||
"y": 2520,
|
||||
"width": 214,
|
||||
"height": 102,
|
||||
"height": 126,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "focal/Callback",
|
||||
@@ -1182,12 +1162,13 @@
|
||||
"id": "cdfcf3a5-ee6b-49f8-b1ad-078ba1100fd9",
|
||||
"properties": {
|
||||
"name": "my_function",
|
||||
"allow_multiple_calls": true
|
||||
"allow_multiple_calls": true,
|
||||
"allow_concurrent": false
|
||||
},
|
||||
"x": 400,
|
||||
"y": 2850,
|
||||
"width": 214,
|
||||
"height": 102,
|
||||
"height": 126,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "focal/Callback",
|
||||
@@ -1228,12 +1209,13 @@
|
||||
"id": "b90a792b-638e-45c3-9e33-c4951efbd76e",
|
||||
"properties": {
|
||||
"name": "my_function",
|
||||
"allow_multiple_calls": true
|
||||
"allow_multiple_calls": true,
|
||||
"allow_concurrent": false
|
||||
},
|
||||
"x": 400,
|
||||
"y": 1690,
|
||||
"width": 214,
|
||||
"height": 102,
|
||||
"height": 126,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "focal/Callback",
|
||||
@@ -1244,12 +1226,13 @@
|
||||
"id": "80265d56-f041-4b26-9468-2a0b38f886a2",
|
||||
"properties": {
|
||||
"name": "my_function",
|
||||
"allow_multiple_calls": true
|
||||
"allow_multiple_calls": true,
|
||||
"allow_concurrent": false
|
||||
},
|
||||
"x": 410,
|
||||
"y": 1980,
|
||||
"width": 214,
|
||||
"height": 102,
|
||||
"height": 126,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "focal/Callback",
|
||||
@@ -1310,6 +1293,32 @@
|
||||
"inherited": false,
|
||||
"registry": "focal/Argument",
|
||||
"base_type": "core/Node"
|
||||
},
|
||||
"d4071915-492a-41ac-b4f9-54e48a167c5c": {
|
||||
"title": "[Update Context] Story Configuration",
|
||||
"id": "d4071915-492a-41ac-b4f9-54e48a167c5c",
|
||||
"properties": {
|
||||
"group": "Update Context",
|
||||
"action_id": "update_context.story_configuration",
|
||||
"action_title": "Story Configuration",
|
||||
"description_chat": "Make changes to the story / experience's configuration: Title, Description, Introduction, Content Classification, Story Intention, Scene Intention and Scene Type.\n\nManage Contextual Pins (add, update, remove pins of world entries).",
|
||||
"description_scene_direction": "",
|
||||
"instruction_examples": [
|
||||
"Update the story intention to be more specific about the expected story's mature content.",
|
||||
"Change the scene type to `roleplay`. Julienne should explore the marketplace to see if she finds anything interesting.",
|
||||
"Keep the scene type `roleplay` but update the goal. Lara and Gustav are discussing how to traverse the puzzle room."
|
||||
],
|
||||
"availability": "both",
|
||||
"force_enabled": false
|
||||
},
|
||||
"x": 860,
|
||||
"y": 1970,
|
||||
"width": 317,
|
||||
"height": 246,
|
||||
"collapsed": false,
|
||||
"inherited": false,
|
||||
"registry": "agents/director/chat/DirectorChatSubAction",
|
||||
"base_type": "core/Node"
|
||||
}
|
||||
},
|
||||
"edges": {
|
||||
@@ -1497,9 +1506,6 @@
|
||||
"328832ac-fdf5-49b7-9f22-23514f5a4f46.state": [
|
||||
"e9460323-7f9c-484b-a993-e351e8fd2762.item1"
|
||||
],
|
||||
"d4071915-492a-41ac-b4f9-54e48a167c5c.state": [
|
||||
"e9460323-7f9c-484b-a993-e351e8fd2762.item2"
|
||||
],
|
||||
"5e8fb08e-30c4-4c6a-97e0-8b4844598f4c.callback": [
|
||||
"e9460323-7f9c-484b-a993-e351e8fd2762.item3"
|
||||
],
|
||||
@@ -1538,6 +1544,9 @@
|
||||
],
|
||||
"b6e0f644-176c-4503-8f5f-511700a77bac.value": [
|
||||
"f16b6be4-f755-4999-ba41-64e630e4bb61.is_player"
|
||||
],
|
||||
"d4071915-492a-41ac-b4f9-54e48a167c5c.state": [
|
||||
"e9460323-7f9c-484b-a993-e351e8fd2762.item2"
|
||||
]
|
||||
},
|
||||
"groups": [
|
||||
@@ -1628,28 +1637,28 @@
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "e29e1b3b-6730-477b-9ac1-142f040f1534",
|
||||
"id": "af480e2b-b458-43f8-b6d1-5b055bb465e3",
|
||||
"name": "fn",
|
||||
"optional": false,
|
||||
"group": null,
|
||||
"socket_type": "function"
|
||||
},
|
||||
{
|
||||
"id": "0ab8ae79-5983-4f28-88eb-1ac093b1d670",
|
||||
"id": "35f819dc-1a05-476f-bd58-dec8e6e6ef85",
|
||||
"name": "name",
|
||||
"optional": false,
|
||||
"group": null,
|
||||
"socket_type": "str"
|
||||
},
|
||||
{
|
||||
"id": "dfba608b-296c-4ad1-a542-dcbeda406666",
|
||||
"id": "4461c3b6-a329-4930-929b-0cecb0875a72",
|
||||
"name": "allow_multiple_calls",
|
||||
"optional": false,
|
||||
"group": null,
|
||||
"socket_type": "bool"
|
||||
},
|
||||
{
|
||||
"id": "dd9c4401-57ea-4023-a5bc-d5b1112be388",
|
||||
"id": "87ef9dea-d65f-4f58-9d44-fa54ee857d03",
|
||||
"name": "ai_callback",
|
||||
"optional": false,
|
||||
"group": null,
|
||||
@@ -1704,6 +1713,18 @@
|
||||
"min": null,
|
||||
"max": null,
|
||||
"ephemeral": false
|
||||
},
|
||||
"allow_concurrent": {
|
||||
"name": "allow_concurrent",
|
||||
"description": "Whether the action can be called concurrently",
|
||||
"type": "bool",
|
||||
"default": false,
|
||||
"choices": null,
|
||||
"readonly": false,
|
||||
"step": null,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"ephemeral": false
|
||||
}
|
||||
},
|
||||
"style": {
|
||||
|
||||
@@ -565,6 +565,7 @@ class SceneAppearance(pydantic.BaseModel):
|
||||
context_investigation_messages: HidableHistoryMessageStyle = (
|
||||
HidableHistoryMessageStyle()
|
||||
)
|
||||
information_messages: HistoryMessageStyle = HistoryMessageStyle()
|
||||
quotes: MarkupMessageStyle = MarkupMessageStyle()
|
||||
parentheses: MarkupMessageStyle = MarkupMessageStyle()
|
||||
brackets: MarkupMessageStyle = MarkupMessageStyle()
|
||||
|
||||
@@ -944,6 +944,21 @@ class DirectorMessage(Node):
|
||||
description="Describes the director action",
|
||||
type="str",
|
||||
default="actor_instruction",
|
||||
choices=[
|
||||
"actor_instruction",
|
||||
"user_direction",
|
||||
],
|
||||
)
|
||||
|
||||
subtype = PropertyField(
|
||||
name="subtype",
|
||||
description="The subtype of the director message, used for further categorization of the message",
|
||||
type="str",
|
||||
default=UNRESOLVED,
|
||||
choices=[
|
||||
"function_call",
|
||||
"user_direction",
|
||||
],
|
||||
)
|
||||
|
||||
def __init__(self, title="Director Message", **kwargs):
|
||||
@@ -955,18 +970,26 @@ class DirectorMessage(Node):
|
||||
self.add_input("meta", socket_type="dict", optional=True)
|
||||
self.add_input("character", socket_type="character", optional=True)
|
||||
self.add_input("action", socket_type="str", optional=True)
|
||||
self.add_input("subtype", socket_type="str", optional=True)
|
||||
|
||||
self.set_property("source", "ai")
|
||||
self.set_property("action", "actor_instruction")
|
||||
self.set_property("subtype", UNRESOLVED)
|
||||
|
||||
self.add_output("message", socket_type="message_object")
|
||||
self.add_output("source", socket_type="str")
|
||||
self.add_output("meta", socket_type="dict")
|
||||
self.add_output("character", socket_type="character")
|
||||
self.add_output("action", socket_type="str")
|
||||
self.add_output("subtype", socket_type="str")
|
||||
|
||||
async def run(self, state: GraphState):
|
||||
message = self.get_input_value("message")
|
||||
source = self.get_input_value("source")
|
||||
action = self.get_input_value("action")
|
||||
meta = self.get_input_value("meta")
|
||||
character: "Character" = self.get_input_value("character")
|
||||
message = self.normalized_input_value("message")
|
||||
source = self.normalized_input_value("source")
|
||||
action = self.normalized_input_value("action")
|
||||
meta = self.normalized_input_value("meta")
|
||||
subtype = self.normalized_input_value("subtype")
|
||||
character: "Character" = self.normalized_input_value("character")
|
||||
|
||||
extra = {}
|
||||
|
||||
@@ -974,13 +997,20 @@ class DirectorMessage(Node):
|
||||
extra["meta"] = meta
|
||||
|
||||
message = scene_message.DirectorMessage(
|
||||
message, source=source, action=action, **extra
|
||||
message, source=source, action=action, subtype=subtype, **extra
|
||||
)
|
||||
|
||||
if character and character is not UNRESOLVED:
|
||||
if character is not None:
|
||||
message.set_meta(character=character.name)
|
||||
|
||||
self.set_output_values({"message": message})
|
||||
self.set_output_values({
|
||||
"message": message,
|
||||
"source": source,
|
||||
"meta": meta,
|
||||
"character": character,
|
||||
"action": action,
|
||||
"subtype": subtype,
|
||||
})
|
||||
|
||||
|
||||
@register("scene/message/UnpackMeta")
|
||||
|
||||
@@ -21,6 +21,7 @@ from talemate.game.engine.nodes.registry import register
|
||||
from talemate.game.engine.ux.schema import (
|
||||
UXChoiceElement,
|
||||
UXElement,
|
||||
UXNoticeElement,
|
||||
UXSelection,
|
||||
UXTextInputElement,
|
||||
)
|
||||
@@ -30,6 +31,7 @@ from talemate.util.ux import normalize_choices
|
||||
__all__ = [
|
||||
"BuildChoiceElement",
|
||||
"BuildTextInputElement",
|
||||
"BuildNoticeElement",
|
||||
"StyleElement",
|
||||
"EmitElement",
|
||||
]
|
||||
@@ -133,7 +135,7 @@ class _BuildUxElementMixin:
|
||||
|
||||
|
||||
def _parse_ux_element(raw: Any) -> UXElement:
|
||||
if isinstance(raw, (UXChoiceElement, UXTextInputElement)):
|
||||
if isinstance(raw, (UXChoiceElement, UXTextInputElement, UXNoticeElement)):
|
||||
return raw
|
||||
return pydantic.TypeAdapter(UXElement).validate_python(raw)
|
||||
|
||||
@@ -416,6 +418,77 @@ class BuildTextInputElement(_BuildUxElementMixin, Node):
|
||||
)
|
||||
|
||||
|
||||
@register("ux/BuildNoticeElement")
|
||||
class BuildNoticeElement(_BuildUxElementMixin, Node):
|
||||
"""
|
||||
Builds a notice (display-only) UX element payload.
|
||||
|
||||
Fire-and-forget: EmitElement emits and returns immediately. The frontend
|
||||
renders the notice until the user dismisses it (when closable) or the
|
||||
client-side timeout expires.
|
||||
|
||||
Inputs:
|
||||
- state: any
|
||||
- id: str (optional)
|
||||
- closable: bool (optional)
|
||||
- timeout_seconds: int (optional)
|
||||
- title: str (optional)
|
||||
- body: str (optional)
|
||||
|
||||
Outputs:
|
||||
- state: any
|
||||
- ux_id: str
|
||||
- ux_element: dict
|
||||
"""
|
||||
|
||||
@pydantic.computed_field(description="Node style")
|
||||
@property
|
||||
def style(self) -> NodeStyle:
|
||||
return NodeStyle(
|
||||
node_color="#2d3a38",
|
||||
title_color="#3a4d4a",
|
||||
icon="F1B4F",
|
||||
)
|
||||
|
||||
def __init__(self, title="Build Notice Element", **kwargs):
|
||||
super().__init__(title=title, **kwargs)
|
||||
|
||||
class Fields:
|
||||
closable = _UxBuildCommonFields.closable
|
||||
timeout_seconds = _UxBuildCommonFields.timeout_seconds
|
||||
element_title = _UxBuildCommonFields.element_title
|
||||
element_body = _UxBuildCommonFields.element_body
|
||||
|
||||
def setup(self):
|
||||
self._ux_build_setup_common()
|
||||
self._ux_build_setup_tail_outputs()
|
||||
|
||||
async def run(self, state: GraphState):
|
||||
state_value = self.get_input_value("state")
|
||||
ux_id, closable, timeout_seconds, title, body = self._ux_build_resolve_common()
|
||||
|
||||
element = UXNoticeElement(
|
||||
id=ux_id,
|
||||
closable=closable,
|
||||
timeout_seconds=timeout_seconds,
|
||||
title=title,
|
||||
body=body,
|
||||
)
|
||||
|
||||
self.set_output_values(
|
||||
{
|
||||
"state": state_value,
|
||||
"id": element.id,
|
||||
"closable": element.closable,
|
||||
"timeout_seconds": element.timeout_seconds,
|
||||
"title": element.title or "",
|
||||
"body": element.body or "",
|
||||
"ux_id": element.id,
|
||||
"ux_element": element.model_dump(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register("ux/StyleElement")
|
||||
class StyleElement(Node):
|
||||
"""
|
||||
@@ -461,15 +534,23 @@ class StyleElement(Node):
|
||||
type="str",
|
||||
default="",
|
||||
)
|
||||
apply_scene_colors = PropertyField(
|
||||
name="apply_scene_colors",
|
||||
description="When true, the body text is rendered through the scene-message parser and gets its per-category colors (quotes/emphasis/parentheses/brackets). When false, body renders with markdown formatting but inherits the element's color.",
|
||||
type="bool",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def setup(self):
|
||||
self.add_input("state")
|
||||
self.add_input("ux_element", socket_type="ux_element")
|
||||
self.add_input("tint", socket_type="str", optional=True)
|
||||
self.add_input("icon", socket_type="str", optional=True)
|
||||
self.add_input("apply_scene_colors", socket_type="bool", optional=True)
|
||||
|
||||
self.set_property("tint", "muted")
|
||||
self.set_property("icon", "")
|
||||
self.set_property("apply_scene_colors", False)
|
||||
|
||||
self.add_output("state", socket_type="any")
|
||||
self.add_output("ux_id", socket_type="str")
|
||||
@@ -494,12 +575,18 @@ class StyleElement(Node):
|
||||
if self.get_input_socket("icon").source
|
||||
else self.get_property("icon")
|
||||
)
|
||||
apply_scene_colors = (
|
||||
self.normalized_input_value("apply_scene_colors")
|
||||
if self.get_input_socket("apply_scene_colors").source
|
||||
else self.get_property("apply_scene_colors")
|
||||
)
|
||||
|
||||
color = (str(tint).strip() if tint is not None else "") or None
|
||||
icon = (str(icon).strip() if icon is not None else "") or None
|
||||
|
||||
element.color = color
|
||||
element.icon = icon
|
||||
element.apply_scene_colors = bool(apply_scene_colors)
|
||||
|
||||
# Always sync to meta for backward compatibility
|
||||
if element.color is None:
|
||||
@@ -747,6 +834,9 @@ class EmitElement(Node):
|
||||
cancelled = False
|
||||
timed_out = False
|
||||
|
||||
# Kind dispatch — which await/selection machinery to run. This is not a
|
||||
# "should we block" check (use element.blocking for that); it's which
|
||||
# shape-specific protocol (choices list, text input) this kind speaks.
|
||||
if element.kind in ("choice", "text_input"):
|
||||
scene = active_scene.get()
|
||||
selection, timed_out, aborted = await self.wait_for_interaction(
|
||||
|
||||
@@ -6,10 +6,11 @@ import pydantic
|
||||
|
||||
__all__ = [
|
||||
"UXElementBase",
|
||||
"UXAwaitableElementBase",
|
||||
"UXTimedElementBase",
|
||||
"UXChoice",
|
||||
"UXChoiceElement",
|
||||
"UXTextInputElement",
|
||||
"UXNoticeElement",
|
||||
"UXElement",
|
||||
"UXSelection",
|
||||
"UXPresentPayload",
|
||||
@@ -30,9 +31,21 @@ class UXElementBase(pydantic.BaseModel):
|
||||
# waiting node should be resumed as cancelled.
|
||||
closable: bool = True
|
||||
|
||||
# Whether the element blocks other scene input while present. Interactive
|
||||
# elements (choice, text_input) block and are awaited by the backend;
|
||||
# display-only elements (notice) do not and are fire-and-forget.
|
||||
blocking: bool = True
|
||||
|
||||
title: str | None = None
|
||||
body: str | None = None
|
||||
|
||||
# When true, the frontend renders `body` through the scene-message text
|
||||
# parser, which paints its own per-category colors over the text
|
||||
# (quotes/emphasis/parentheses/brackets). When false, body still renders
|
||||
# markdown formatting but inherits the element's color. Authors flip this
|
||||
# via the `ux/StyleElement` node.
|
||||
apply_scene_colors: bool = False
|
||||
|
||||
# Presentation hints for the frontend (Vuetify-style).
|
||||
# Kept optional for backward compatibility; frontend may also read these from `meta`.
|
||||
icon: str | None = None
|
||||
@@ -41,13 +54,14 @@ class UXElementBase(pydantic.BaseModel):
|
||||
meta: dict[str, Any] = pydantic.Field(default_factory=dict)
|
||||
|
||||
|
||||
class UXAwaitableElementBase(UXElementBase):
|
||||
class UXTimedElementBase(UXElementBase):
|
||||
"""
|
||||
Base for UX elements that are awaitable and may timeout.
|
||||
Base for UX elements that carry a timeout. For blocking elements the backend
|
||||
enforces the timeout while awaiting the user; for non-blocking elements the
|
||||
frontend uses it to drive a client-side auto-dismiss.
|
||||
"""
|
||||
|
||||
# Timeout behavior for awaitable elements.
|
||||
# 0 means no timeout (wait forever until user selects/cancels).
|
||||
# 0 means no timeout.
|
||||
timeout_seconds: int = 0
|
||||
|
||||
# When timeout_seconds > 0, the backend can stamp the start time so the frontend
|
||||
@@ -66,14 +80,14 @@ class UXChoice(pydantic.BaseModel):
|
||||
disabled: bool = False
|
||||
|
||||
|
||||
class UXChoiceElement(UXAwaitableElementBase):
|
||||
class UXChoiceElement(UXTimedElementBase):
|
||||
kind: Literal["choice"] = "choice"
|
||||
choices: list[UXChoice] = pydantic.Field(default_factory=list)
|
||||
multi_select: bool = False
|
||||
default: str | list[str] | None = None
|
||||
|
||||
|
||||
class UXTextInputElement(UXAwaitableElementBase):
|
||||
class UXTextInputElement(UXTimedElementBase):
|
||||
kind: Literal["text_input"] = "text_input"
|
||||
|
||||
# Render as textarea when true, single-line input when false.
|
||||
@@ -89,8 +103,19 @@ class UXTextInputElement(UXAwaitableElementBase):
|
||||
trim: bool = True
|
||||
|
||||
|
||||
class UXNoticeElement(UXTimedElementBase):
|
||||
"""
|
||||
Display-only UX element. Fire-and-forget: the backend emits it and moves on;
|
||||
the frontend renders it until the user dismisses it (when closable) or the
|
||||
client-side timeout expires.
|
||||
"""
|
||||
|
||||
kind: Literal["notice"] = "notice"
|
||||
blocking: bool = False
|
||||
|
||||
|
||||
UXElement = Annotated[
|
||||
Union[UXChoiceElement, UXTextInputElement],
|
||||
Union[UXChoiceElement, UXTextInputElement, UXNoticeElement],
|
||||
pydantic.Field(discriminator="kind"),
|
||||
]
|
||||
|
||||
|
||||
@@ -302,10 +302,10 @@ class NarratorMessage(SceneMessage):
|
||||
|
||||
@dataclass
|
||||
class DirectorMessage(SceneMessage):
|
||||
action: str = "actor_instruction"
|
||||
action: Literal["actor_instruction", "user_direction"] = "actor_instruction"
|
||||
source: str = "ai"
|
||||
typ = "director"
|
||||
subtype: str | None = None
|
||||
subtype: Literal["function_call", "user_direction"] | None = None
|
||||
|
||||
@property
|
||||
def character_name(self) -> str:
|
||||
|
||||
@@ -78,7 +78,14 @@ COLOR_MAP = {
|
||||
COLOR_NAMES = sorted(list(COLOR_MAP.keys()))
|
||||
COLORS = sorted(list(COLOR_MAP.values()))
|
||||
|
||||
SPECIAL_COLOR_NAMES = ["narrator", "actor", "director", "time", "context_investigation"]
|
||||
SPECIAL_COLOR_NAMES = [
|
||||
"narrator",
|
||||
"actor",
|
||||
"director",
|
||||
"time",
|
||||
"context_investigation",
|
||||
"information",
|
||||
]
|
||||
|
||||
ALL_COLOR_NAMES = SPECIAL_COLOR_NAMES + COLOR_NAMES
|
||||
|
||||
|
||||
@@ -94,6 +94,12 @@
|
||||
<span v-html="renderedContextInvestigationPreview">
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3 d-flex align-center" :style="buildCssStyles('information_messages', config.information_messages)">
|
||||
<v-icon class="mr-2" :color="getColor('information_messages', config.information_messages?.color)">mdi-information-outline</v-icon>
|
||||
<span>
|
||||
A heads-up or system notice for the player.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -108,6 +114,7 @@
|
||||
|
||||
<script>
|
||||
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
|
||||
import { DEFAULT_APPEARANCE_COLORS } from '@/utils/messageColors.js';
|
||||
|
||||
export default {
|
||||
name: 'AppConfigAppearanceScene',
|
||||
@@ -140,12 +147,12 @@ export default {
|
||||
}
|
||||
// Ensure new styling fields exist with defaults if missing
|
||||
if (!sceneConfig.quotes) {
|
||||
sceneConfig.quotes = { color: this.defaultColors.quotes, italic: false, bold: false, override_color: true };
|
||||
sceneConfig.quotes = { color: DEFAULT_APPEARANCE_COLORS.quotes, italic: false, bold: false, override_color: true };
|
||||
} else if (sceneConfig.quotes.override_color === undefined) {
|
||||
sceneConfig.quotes.override_color = true;
|
||||
}
|
||||
if (!sceneConfig.parentheses) {
|
||||
sceneConfig.parentheses = { color: this.defaultColors.parentheses, italic: true, bold: false, override_color: true, show: true };
|
||||
sceneConfig.parentheses = { color: DEFAULT_APPEARANCE_COLORS.parentheses, italic: true, bold: false, override_color: true, show: true };
|
||||
} else {
|
||||
if (sceneConfig.parentheses.override_color === undefined) {
|
||||
sceneConfig.parentheses.override_color = true;
|
||||
@@ -155,7 +162,7 @@ export default {
|
||||
}
|
||||
}
|
||||
if (!sceneConfig.brackets) {
|
||||
sceneConfig.brackets = { color: this.defaultColors.brackets, italic: true, bold: false, override_color: true, show: true };
|
||||
sceneConfig.brackets = { color: DEFAULT_APPEARANCE_COLORS.brackets, italic: true, bold: false, override_color: true, show: true };
|
||||
} else {
|
||||
if (sceneConfig.brackets.override_color === undefined) {
|
||||
sceneConfig.brackets.override_color = true;
|
||||
@@ -165,7 +172,7 @@ export default {
|
||||
}
|
||||
}
|
||||
if (!sceneConfig.emphasis) {
|
||||
sceneConfig.emphasis = { color: this.defaultColors.emphasis, italic: true, bold: false, override_color: true };
|
||||
sceneConfig.emphasis = { color: DEFAULT_APPEARANCE_COLORS.emphasis, italic: true, bold: false, override_color: true };
|
||||
} else if (sceneConfig.emphasis.override_color === undefined) {
|
||||
sceneConfig.emphasis.override_color = true;
|
||||
}
|
||||
@@ -249,23 +256,13 @@ export default {
|
||||
colorPicker: null,
|
||||
color: "#000000",
|
||||
colorPickerTarget: null,
|
||||
defaultColors: {
|
||||
"narrator_messages": "#A180AE",
|
||||
"actor_messages": "#B39DDB",
|
||||
"director_messages": "#FF5722",
|
||||
"time_messages": "#FFECB3",
|
||||
"context_investigation_messages": "#D5C0A1",
|
||||
"quotes": "#FFFFFF",
|
||||
"parentheses": "#DB9DC2",
|
||||
"brackets": "#DC5D5D",
|
||||
"emphasis": "#B39DDB",
|
||||
},
|
||||
typLabelMap: {
|
||||
"narrator_messages": "Narrator Messages",
|
||||
"actor_messages": "Actor Messages",
|
||||
"director_messages": "Director Messages",
|
||||
"time_messages": "Time Messages",
|
||||
"context_investigation_messages": "Context Investigations",
|
||||
"information_messages": "Information Messages",
|
||||
"quotes": "Quotes",
|
||||
"parentheses": "Parentheses",
|
||||
"brackets": "Brackets",
|
||||
@@ -280,6 +277,7 @@ export default {
|
||||
"actor_messages": true,
|
||||
"director_messages": true,
|
||||
"context_investigation_messages": true,
|
||||
"information_messages": true,
|
||||
"quotes": true,
|
||||
"parentheses": true,
|
||||
"brackets": true,
|
||||
@@ -292,6 +290,7 @@ export default {
|
||||
"director_messages": true,
|
||||
"time_messages": true,
|
||||
"context_investigation_messages": true,
|
||||
"information_messages": true,
|
||||
"quotes": true,
|
||||
"parentheses": true,
|
||||
"brackets": true,
|
||||
@@ -338,7 +337,11 @@ export default {
|
||||
getColor(typ, color) {
|
||||
// if color is None load the default color
|
||||
if (color === null) {
|
||||
return this.defaultColors[typ];
|
||||
// Message-type keys use the `_messages` suffix in the config UI
|
||||
// (e.g. "narrator_messages") but DEFAULT_APPEARANCE_COLORS is
|
||||
// keyed by bare type name — strip the suffix on lookup.
|
||||
const key = typ.endsWith('_messages') ? typ.slice(0, -'_messages'.length) : typ;
|
||||
return DEFAULT_APPEARANCE_COLORS[key];
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-alert v-if="show" @mouseover="hovered=true" @mouseleave="hovered=false" @click="toggle()" class="clickable" variant="text" density="compact" :color="getMessageColor('context_investigation', null)">
|
||||
<v-alert v-if="show" @mouseover="hovered=true" @mouseleave="hovered=false" @click="toggle()" class="clickable" variant="text" density="compact" :color="getMessageColor('context_investigation')">
|
||||
<template v-slot:close>
|
||||
<v-btn size="small" icon variant="text" class="close-button" @click="deleteMessage" :disabled="uxLocked">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
<template>
|
||||
<div v-if="subtype === 'user_direction'">
|
||||
<!-- player-authored direction (~ / ~~ input) -->
|
||||
<v-alert class="clickable user-direction" variant="text" icon="mdi-account-voice" elevation="0" density="compact" color="deep-purple-lighten-2" @click:close="deleteMessage()" closable :disabled="uxLocked">
|
||||
<span class="text-caption text-medium-emphasis mr-1">Player directs:</span>
|
||||
<span class="director-text">{{ text }}</span>
|
||||
<v-alert class="clickable user-direction mb-8 ml-8" variant="text" elevation="0" density="compact" :style="getMessageStyle('information')" @click:close="deleteMessage()" closable :disabled="uxLocked">
|
||||
<div class="text-caption" :style="getMessageStyle('director')">Message to the director</div>
|
||||
<div v-html="renderedText"></div>
|
||||
</v-alert>
|
||||
</div>
|
||||
<div v-else-if="character">
|
||||
<!-- actor instructions (character direction)-->
|
||||
<div class="director-container" v-if="show && minimized" >
|
||||
<v-chip closable :color="getMessageColor('director', null)" class="clickable" @click:close="deleteMessage()" :disabled="uxLocked">
|
||||
<v-chip closable :color="getMessageColor('director')" class="clickable" @click:close="deleteMessage()" :disabled="uxLocked">
|
||||
<v-icon class="mr-2">{{ icon }}</v-icon>
|
||||
<span @click="toggle()">{{ character }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-alert v-else-if="show" class="clickable" variant="text" type="info" :icon="icon" :style="getMessageStyle('director')" elevation="0" density="compact" @click:close="deleteMessage()" :color="getMessageColor('director', null)">
|
||||
<v-alert v-else-if="show" class="clickable" variant="text" type="info" :icon="icon" :style="getMessageStyle('director')" elevation="0" density="compact" @click:close="deleteMessage()" :color="getMessageColor('director')">
|
||||
<span v-if="direction_mode==='internal_monologue'">
|
||||
<!-- internal monologue -->
|
||||
<span :style="getMessageStyle('director')" class="text-decoration-underline" @click="toggle()">{{ character }}</span>
|
||||
<span :style="getMessageStyle('director')" class="ml-1" @click="toggle()">thinks</span>
|
||||
<span :style="getMessageStyle('director')" class="director-text ml-1" @click="toggle()">{{ text }}</span>
|
||||
<span :style="getMessageStyle('director')" class="director-text ml-1" @click="toggle()" v-html="renderedTextInline"></span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- director instructs -->
|
||||
<span :style="getMessageStyle('director')" @click="toggle()">Director instructs</span>
|
||||
<span :style="getMessageStyle('director')" class="ml-1 text-decoration-underline" @click="toggle()">{{ character }}</span>
|
||||
<span :style="getMessageStyle('director')" class="director-text ml-1" @click="toggle()">{{ text }}</span>
|
||||
<span :style="getMessageStyle('director')" class="director-text ml-1" @click="toggle()" v-html="renderedTextInline"></span>
|
||||
</span>
|
||||
|
||||
</v-alert>
|
||||
</div>
|
||||
<div v-else-if="action">
|
||||
<v-alert :color="getMessageColor('director', null)" variant="text" type="info" :icon="icon"
|
||||
<v-alert :color="getMessageColor('director')" variant="text" type="info" :icon="icon"
|
||||
elevation="0" density="compact" >
|
||||
|
||||
<div>{{ text }}</div>
|
||||
<div v-html="renderedText"></div>
|
||||
<div class="text-grey text-caption">{{ action }}</div>
|
||||
</v-alert>
|
||||
</div>
|
||||
@@ -42,6 +42,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { parseBlock, parseInline } from '@/utils/markdownRenderer.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -58,6 +60,12 @@ export default {
|
||||
} else {
|
||||
return 'mdi-bullhorn-outline';
|
||||
}
|
||||
},
|
||||
renderedText() {
|
||||
return parseBlock(this.text);
|
||||
},
|
||||
renderedTextInline() {
|
||||
return parseInline(this.text);
|
||||
}
|
||||
},
|
||||
props: ['text', 'message_id', 'character', 'direction_mode', 'action', 'subtype', 'uxLocked', 'isLastMessage'],
|
||||
@@ -79,6 +87,10 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-direction {
|
||||
border-left: 3px solid rgb(var(--v-theme-mutedbg));
|
||||
}
|
||||
|
||||
.director-container {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@@ -376,6 +376,10 @@ import ConfirmActionPrompt from './ConfirmActionPrompt.vue';
|
||||
import VisualAssetsMixin from './VisualAssetsMixin.js';
|
||||
import { isVisualAgentReady } from '../constants/visual.js';
|
||||
import { primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
import {
|
||||
getMessageColor as resolveMessageColor,
|
||||
getMessageStyle as resolveMessageStyle,
|
||||
} from '@/utils/messageColors.js';
|
||||
|
||||
const MESSAGE_FLAGS = {
|
||||
NONE: 0,
|
||||
@@ -507,13 +511,6 @@ export default {
|
||||
primaryModifierLabel,
|
||||
messages: [],
|
||||
selectedForkMessageId: null,
|
||||
defaultColors: {
|
||||
"narrator": "#B39DDB",
|
||||
"character": "#FFFFFF",
|
||||
"director": "#FF5722",
|
||||
"time": "#B39DDB",
|
||||
"context_investigation": "#FFE0B2",
|
||||
},
|
||||
// Track last effective asset ID per scope for "on_change" cadence
|
||||
lastEffectiveAssetIdByScope: {},
|
||||
// Debounce timer for reapplyMessageAssetCadence
|
||||
@@ -642,12 +639,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
|
||||
getMessageColor(typ,color) {
|
||||
if(!this.appearanceConfig || !this.appearanceConfig.scene[`${typ}_messages`].color) {
|
||||
return this.defaultColors[typ];
|
||||
}
|
||||
|
||||
return color || this.appearanceConfig.scene[`${typ}_messages`].color;
|
||||
getMessageColor(typ) {
|
||||
return resolveMessageColor(this.appearanceConfig, typ);
|
||||
},
|
||||
|
||||
getMessageTypeHidden(typ) {
|
||||
@@ -664,16 +657,7 @@ export default {
|
||||
},
|
||||
|
||||
getMessageStyle(typ) {
|
||||
let styles = "";
|
||||
let config = this.appearanceConfig.scene[`${typ}_messages`];
|
||||
if (config.italic) {
|
||||
styles += "font-style: italic;";
|
||||
}
|
||||
if (config.bold) {
|
||||
styles += "font-weight: bold;";
|
||||
}
|
||||
styles += "color: " + this.getMessageColor(typ, config.color) + ";";
|
||||
return styles;
|
||||
return resolveMessageStyle(this.appearanceConfig, typ);
|
||||
},
|
||||
|
||||
clear() {
|
||||
@@ -1577,8 +1561,9 @@ export default {
|
||||
type: 'ux',
|
||||
element: el,
|
||||
});
|
||||
// Track UX interaction start
|
||||
if (this.beginUxInteraction) {
|
||||
// Track UX interaction start — only for blocking elements.
|
||||
// Non-blocking (fire-and-forget) elements must not lock scene input.
|
||||
if (this.beginUxInteraction && el.blocking !== false) {
|
||||
this.beginUxInteraction(id);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -12,21 +12,32 @@
|
||||
|
||||
<v-card variant="text" class="ux-element-card">
|
||||
<v-card-title v-if="element?.title || alertIcon" class="d-flex align-center pa-0 mb-2">
|
||||
<v-icon v-if="alertIcon" :color="alertColor" class="mr-2">{{ alertIcon }}</v-icon>
|
||||
<span :class="`text-subtitle-1 font-weight-bold text-${alertColor}`">
|
||||
<v-icon v-if="alertIcon" :color="resolvedColor || alertColor" class="mr-2">{{ alertIcon }}</v-icon>
|
||||
<span :class="['text-subtitle-1', 'font-weight-bold', colorClass]" :style="colorStyle">
|
||||
{{ element?.title || defaultTitle() }}
|
||||
</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<div v-if="element?.body" class="text-body-2 mb-2 ux-element-body" v-html="renderedBody"></div>
|
||||
<div
|
||||
v-if="element?.body && applySceneColors"
|
||||
class="text-body-2 mb-2 ux-element-body"
|
||||
:style="colorStyle"
|
||||
v-html="renderedBody"
|
||||
></div>
|
||||
<div
|
||||
v-else-if="element?.body"
|
||||
:class="['text-body-2', 'mb-2', 'ux-element-body-plain', colorClass]"
|
||||
:style="colorStyle"
|
||||
v-html="renderedPlainBody"
|
||||
></div>
|
||||
|
||||
<div v-if="hasTimeoutTimer" class="mb-2">
|
||||
<v-progress-linear
|
||||
:model-value="timeoutProgressPct"
|
||||
height="6"
|
||||
rounded
|
||||
:color="alertColor"
|
||||
:color="resolvedColor || alertColor"
|
||||
class="mb-1 ux-timeout-progress"
|
||||
/>
|
||||
</div>
|
||||
@@ -36,7 +47,7 @@
|
||||
:element="element"
|
||||
:ux-locked="uxLocked"
|
||||
:is-closable="isClosable"
|
||||
:tint-color="alertColor"
|
||||
:tint-color="resolvedColor || alertColor"
|
||||
@close="$emit('close', $event)"
|
||||
/>
|
||||
|
||||
@@ -45,11 +56,13 @@
|
||||
:element="element"
|
||||
:ux-locked="uxLocked"
|
||||
:is-closable="isClosable"
|
||||
:tint-color="alertColor"
|
||||
:tint-color="resolvedColor || alertColor"
|
||||
@close="$emit('close', $event)"
|
||||
/>
|
||||
|
||||
<div v-else class="text-body-2">
|
||||
<!-- 'notice' has no child component: the container chrome above (title, body, icon, timeout bar) is the full render. -->
|
||||
|
||||
<div v-else-if="!isKnownKind" class="text-body-2">
|
||||
Unsupported UX element kind: {{ element?.kind }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
@@ -61,6 +74,8 @@
|
||||
import UxElementChoice from "./UxElementChoice.vue";
|
||||
import UxElementTextInput from "./UxElementTextInput.vue";
|
||||
import { SceneTextParser } from "@/utils/sceneMessageRenderer.js";
|
||||
import { isSpecialMessageColor } from "@/utils/messageColors.js";
|
||||
import { parseBlock as parsePlainMarkdown } from "@/utils/markdownRenderer.js";
|
||||
|
||||
export default {
|
||||
name: "UxElementMessage",
|
||||
@@ -82,7 +97,10 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
inject: ["getWebsocket"],
|
||||
inject: {
|
||||
getWebsocket: { default: null },
|
||||
getMessageColor: { default: null },
|
||||
},
|
||||
emits: ["close"],
|
||||
data() {
|
||||
return {
|
||||
@@ -98,6 +116,22 @@ export default {
|
||||
alertColor() {
|
||||
return this.element?.color || this.element?.meta?.color || "muted";
|
||||
},
|
||||
resolvedColor() {
|
||||
// Tints that name scene-message types (e.g. "director",
|
||||
// "context_investigation") are resolved through getMessageColor so they
|
||||
// honor the user's appearance config — same path DirectorMessage uses.
|
||||
// Non-special tints (standard Vuetify palette names) return null and
|
||||
// fall through to the `text-<color>` class.
|
||||
if (!this.getMessageColor) return null;
|
||||
if (!isSpecialMessageColor(this.alertColor)) return null;
|
||||
return this.getMessageColor(this.alertColor) || null;
|
||||
},
|
||||
colorClass() {
|
||||
return this.resolvedColor ? null : `text-${this.alertColor}`;
|
||||
},
|
||||
colorStyle() {
|
||||
return this.resolvedColor ? { color: this.resolvedColor } : null;
|
||||
},
|
||||
alertIcon() {
|
||||
const icon = this.element?.icon || this.element?.meta?.icon;
|
||||
return icon ? icon : undefined;
|
||||
@@ -142,19 +176,38 @@ export default {
|
||||
parser() {
|
||||
const sceneConfig = this.appearanceConfig?.scene || {};
|
||||
const narratorStyles = sceneConfig.narrator_messages || {};
|
||||
|
||||
|
||||
// When the element carries a resolved scene-message color (e.g. a
|
||||
// "director" tint resolves to the configured director color), use it as
|
||||
// the parser's default/paragraph color instead of narrator's. Markup
|
||||
// categories configured with override_color:false cascade to this
|
||||
// default; categories with their own color keep it.
|
||||
const defaultStyles = this.resolvedColor
|
||||
? { ...narratorStyles, color: this.resolvedColor }
|
||||
: narratorStyles;
|
||||
|
||||
return new SceneTextParser({
|
||||
quotes: sceneConfig.quotes,
|
||||
emphasis: sceneConfig.emphasis || narratorStyles,
|
||||
parentheses: sceneConfig.parentheses || narratorStyles,
|
||||
brackets: sceneConfig.brackets || narratorStyles,
|
||||
default: narratorStyles,
|
||||
default: defaultStyles,
|
||||
});
|
||||
},
|
||||
renderedBody() {
|
||||
if (!this.element?.body) return "";
|
||||
return this.parser.parse(this.element.body);
|
||||
},
|
||||
renderedPlainBody() {
|
||||
if (!this.element?.body) return "";
|
||||
return parsePlainMarkdown(this.element.body);
|
||||
},
|
||||
isKnownKind() {
|
||||
return ["choice", "text_input", "notice"].includes(this.element?.kind);
|
||||
},
|
||||
applySceneColors() {
|
||||
return this.element?.apply_scene_colors === true;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
hasTimeoutTimer: {
|
||||
@@ -181,6 +234,7 @@ export default {
|
||||
defaultTitle() {
|
||||
if (this.element?.kind === "choice") return "Choose an option";
|
||||
if (this.element?.kind === "text_input") return "Enter text";
|
||||
if (this.element?.kind === "notice") return "Notice";
|
||||
return "Interaction";
|
||||
},
|
||||
_startTimer() {
|
||||
@@ -191,6 +245,11 @@ export default {
|
||||
// stop ticking once timer reaches zero
|
||||
if (this.hasTimeoutTimer && this.timeoutRemainingMs <= 0) {
|
||||
this._stopTimer();
|
||||
// Non-blocking elements are fire-and-forget on the backend, so the
|
||||
// client owns auto-dismissal when the timer expires.
|
||||
if (this.element?.blocking === false) {
|
||||
this.$emit("close", this.element?.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._rafId = requestAnimationFrame(tick);
|
||||
@@ -205,16 +264,20 @@ export default {
|
||||
},
|
||||
cancel() {
|
||||
if (!this.isClosable) return;
|
||||
const ws = this.getWebsocket();
|
||||
if (ws && this.element?.id) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "ux",
|
||||
action: "cancel",
|
||||
ux_id: this.element.id,
|
||||
kind: this.element?.kind,
|
||||
})
|
||||
);
|
||||
// Only blocking elements have a backend waiter to cancel; for non-blocking
|
||||
// elements, skip the ws round-trip to avoid leaving stale selection state.
|
||||
if (this.element?.blocking !== false) {
|
||||
const ws = this.getWebsocket();
|
||||
if (ws && this.element?.id) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "ux",
|
||||
action: "cancel",
|
||||
ux_id: this.element.id,
|
||||
kind: this.element?.kind,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
this.$emit("close", this.element?.id);
|
||||
},
|
||||
@@ -237,5 +300,39 @@ export default {
|
||||
.ux-element-body :deep(.scene-paragraph:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Plain (non-scene-parsed) body: vanilla markdown, inherits the element color
|
||||
but rendered subtly dimmer than the bold title for visual differentiation. */
|
||||
.ux-element-body-plain {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.ux-element-body-plain :deep(p) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.ux-element-body-plain :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Restore list markers — Vuetify's typography reset strips them inside alerts. */
|
||||
.ux-element-body :deep(ul),
|
||||
.ux-element-body-plain :deep(ul) {
|
||||
list-style: disc;
|
||||
padding-inline-start: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.ux-element-body :deep(ol),
|
||||
.ux-element-body-plain :deep(ol) {
|
||||
list-style: decimal;
|
||||
padding-inline-start: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.ux-element-body :deep(li),
|
||||
.ux-element-body-plain :deep(li) {
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-grey-lighten-1">
|
||||
<div v-if="!isMultiline">
|
||||
<v-text-field
|
||||
v-model="text"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
:color="tintColor"
|
||||
:placeholder="placeholder"
|
||||
hide-details
|
||||
@keydown.enter.prevent="submit"
|
||||
@@ -16,6 +17,7 @@
|
||||
v-model="text"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
:color="tintColor"
|
||||
:placeholder="placeholder"
|
||||
:rows="rows"
|
||||
hide-details
|
||||
|
||||
@@ -30,6 +30,13 @@ export default createVuetify({
|
||||
highlight5: colors.amber.lighten3,
|
||||
highlight6: colors.orange.lighten3,
|
||||
highlight7: colors.blue.lighten3,
|
||||
subdued1: colors.indigo.darken2,
|
||||
subdued2: colors.purple.darken2,
|
||||
subdued3: colors.lightGreen.darken2,
|
||||
subdued4: colors.red.darken2,
|
||||
subdued5: colors.amber.darken2,
|
||||
subdued6: colors.orange.darken2,
|
||||
subdued7: colors.blue.darken2,
|
||||
persona: colors.pink.lighten3,
|
||||
dirty: colors.orange.lighten2,
|
||||
instructions: colors.orange.lighten4,
|
||||
|
||||
16
talemate_frontend/src/utils/markdownRenderer.js
Normal file
16
talemate_frontend/src/utils/markdownRenderer.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// Shared vanilla markdown renderer for scene-message components that render
|
||||
// plain markdown (no scene-parser extensions, no color injection). Using a
|
||||
// single instance keeps options consistent across components — callers that
|
||||
// want scene-aware parsing should use SceneTextParser instead.
|
||||
|
||||
import { Marked } from "marked";
|
||||
|
||||
const renderer = new Marked({ breaks: true, gfm: true });
|
||||
|
||||
export function parseBlock(text) {
|
||||
return renderer.parse(text ?? "");
|
||||
}
|
||||
|
||||
export function parseInline(text) {
|
||||
return renderer.parseInline(text ?? "");
|
||||
}
|
||||
56
talemate_frontend/src/utils/messageColors.js
Normal file
56
talemate_frontend/src/utils/messageColors.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// Scene-message color resolution.
|
||||
//
|
||||
// Single source of truth for the set of configurable "special" message color
|
||||
// types and how their effective color/style is resolved from the user's
|
||||
// appearance config. Mirrors talemate.util.colors.SPECIAL_COLOR_NAMES on the
|
||||
// backend — keep in sync.
|
||||
|
||||
export const SPECIAL_MESSAGE_COLORS = [
|
||||
"narrator",
|
||||
"actor",
|
||||
"director",
|
||||
"time",
|
||||
"context_investigation",
|
||||
"information",
|
||||
];
|
||||
|
||||
// Canonical default colors used both as the resolution fallback (when the
|
||||
// user has not configured a color) and as the reset target in the appearance
|
||||
// config UI. Keyed by bare type name — both message types (narrator, actor,
|
||||
// director, time, context_investigation, information) and markup types
|
||||
// (quotes, parentheses, brackets, emphasis).
|
||||
export const DEFAULT_APPEARANCE_COLORS = {
|
||||
narrator: "#A180AE",
|
||||
actor: "#B39DDB",
|
||||
director: "#FF5722",
|
||||
time: "#FFECB3",
|
||||
context_investigation: "#D5C0A1",
|
||||
information: "#BA8574",
|
||||
quotes: "#FFFFFF",
|
||||
parentheses: "#DB9DC2",
|
||||
brackets: "#DC5D5D",
|
||||
emphasis: "#B39DDB",
|
||||
};
|
||||
|
||||
export function isSpecialMessageColor(name) {
|
||||
return SPECIAL_MESSAGE_COLORS.includes(name);
|
||||
}
|
||||
|
||||
// Resolve the effective color for a scene-message type against the user's
|
||||
// appearance config. Returns the configured color when set, otherwise the
|
||||
// canonical default.
|
||||
export function getMessageColor(appearanceConfig, typ) {
|
||||
const cfg = appearanceConfig?.scene?.[`${typ}_messages`];
|
||||
return cfg?.color || DEFAULT_APPEARANCE_COLORS[typ];
|
||||
}
|
||||
|
||||
// Build an inline CSS style string (color + optional italic/bold) for a
|
||||
// scene-message type from the user's appearance config.
|
||||
export function getMessageStyle(appearanceConfig, typ) {
|
||||
const cfg = appearanceConfig?.scene?.[`${typ}_messages`] || {};
|
||||
let styles = "";
|
||||
if (cfg.italic) styles += "font-style: italic;";
|
||||
if (cfg.bold) styles += "font-weight: bold;";
|
||||
styles += `color: ${getMessageColor(appearanceConfig, typ)};`;
|
||||
return styles;
|
||||
}
|
||||
@@ -1,29 +1,30 @@
|
||||
import { marked, Marked } from 'marked';
|
||||
import { DEFAULT_APPEARANCE_COLORS } from './messageColors.js';
|
||||
|
||||
const DEFAULTS = {
|
||||
quotes: {
|
||||
color: "#FFFFFF",
|
||||
color: DEFAULT_APPEARANCE_COLORS.quotes,
|
||||
italic: false,
|
||||
bold: false,
|
||||
},
|
||||
parentheses: {
|
||||
color: "#DB9DC2",
|
||||
color: DEFAULT_APPEARANCE_COLORS.parentheses,
|
||||
italic: false,
|
||||
bold: false,
|
||||
},
|
||||
brackets: {
|
||||
color: "#DC5D5D",
|
||||
color: DEFAULT_APPEARANCE_COLORS.brackets,
|
||||
italic: false,
|
||||
bold: true,
|
||||
},
|
||||
emphasis: {
|
||||
color: "#B39DDB",
|
||||
italic: false,
|
||||
color: DEFAULT_APPEARANCE_COLORS.emphasis,
|
||||
italic: false,
|
||||
bold: false,
|
||||
},
|
||||
default: {
|
||||
color: "#B39DDB",
|
||||
italic: true,
|
||||
color: DEFAULT_APPEARANCE_COLORS.emphasis,
|
||||
italic: true,
|
||||
bold: false,
|
||||
},
|
||||
prefix: {
|
||||
@@ -33,19 +34,14 @@ const DEFAULTS = {
|
||||
},
|
||||
}
|
||||
|
||||
// Default colors for different message types when no color is specified
|
||||
const MESSAGE_TYPE_DEFAULTS = {
|
||||
context_investigation: "#D5C0A1",
|
||||
}
|
||||
|
||||
export class SceneTextParser {
|
||||
constructor(config = {}) {
|
||||
// Determine default color:
|
||||
// 1. Use config.defaultColor if explicitly provided
|
||||
// 2. Use message type default if messageType is specified
|
||||
// 2. Use message type default from DEFAULT_APPEARANCE_COLORS if messageType is specified
|
||||
// 3. Otherwise use DEFAULTS.default.color
|
||||
const defaultColor = config.defaultColor ??
|
||||
(config.messageType && MESSAGE_TYPE_DEFAULTS[config.messageType]) ??
|
||||
const defaultColor = config.defaultColor ??
|
||||
(config.messageType && DEFAULT_APPEARANCE_COLORS[config.messageType]) ??
|
||||
DEFAULTS.default.color;
|
||||
|
||||
// Get the default message color from config.default if available
|
||||
|
||||
Reference in New Issue
Block a user