mirror of
https://github.com/vegu-ai/talemate.git
synced 2026-09-01 19:48:52 +02:00
feat: Enhance keyboard modifier support for macOS
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
- "Narrate Progress: Fixed narration sometimes generating screenplay-style dialogue entries instead of continuous narrative prose."
|
||||
- "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."
|
||||
- "Node Graph: Fixed `data/MakeDict` and `data/MakeList` leaking their initial value as a shared mutable reference across graph executions. Downstream in-place mutations (e.g., writing into a dict fed into SetState) would persist on the node and appear on the next run as stale data. Both nodes now deep-copy their configured default before emitting it."
|
||||
- "macOS Modifiers: Ctrl+click affordances throughout the UI now also accept Cmd (Meta) on macOS, where the OS reserves Ctrl+click for the system context menu. Tooltips, hints, and docs label the modifier as Cmd on Mac and Ctrl elsewhere. Affects regenerate, scene tools (narrator/director/actor/creative), world state manager, contextual generate, message asset image, autocomplete (Ctrl+Enter), and history navigation. (#261)"
|
||||
improvements:
|
||||
- "System Prompt Override Indicators: The system prompt override list now shows a pencil icon next to entries that have an active override, making it easy to see which prompts have been customized."
|
||||
- "Search Strictness: The distance_mod embedding preset value is now a float (was int) with a range of 0.1–2.0, allowing both tighter and looser similarity matching. A 'Search Strictness' slider is now available in the Context Database UI, letting users tune search sensitivity on the fly. Changes persist to the active embedding preset immediately."
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
<!--- --8<-- [start:autocomplete] -->
|
||||
!!! tip "Autocomplete using AI"
|
||||
**:material-auto-fix: Autocomplete** - press `ctrl+Enter` while the cursor is in the input field to use AI to autocomplete the current content, you
|
||||
**:material-auto-fix: Autocomplete** - press `ctrl+Enter` (or `cmd+Enter` on macOS) while the cursor is in the input field to use AI to autocomplete the current content, you
|
||||
<!--- --8<-- [end:autocomplete] -->
|
||||
|
||||
<!--- --8<-- [start:generate_and_autocomplete] -->
|
||||
!!! tip "Generate using AI"
|
||||
**:material-auto-fix: Generate** - press the **generate** button on top of the input field to let the AI generate the content based on the existing details.
|
||||
|
||||
**:material-auto-fix: Autocomplete** - press `ctrl+Enter` while the cursor is in the input field to use AI to autocomplete the current content, you
|
||||
**:material-auto-fix: Autocomplete** - press `ctrl+Enter` (or `cmd+Enter` on macOS) while the cursor is in the input field to use AI to autocomplete the current content, you
|
||||
<!--- --8<-- [end:generate_and_autocomplete] -->
|
||||
|
||||
<!--- --8<-- [start:generation_templates_and_settings] -->
|
||||
|
||||
@@ -38,8 +38,8 @@ If your voice agent is disabled - indicated by the grey dot next to the agent -
|
||||
|
||||
 
|
||||
|
||||
!!! note "Ctrl click to toggle agent"
|
||||
You can use Ctrl click to toggle the agent on and off.
|
||||
!!! note "Ctrl/Cmd click to toggle agent"
|
||||
You can use Ctrl click (or Cmd click on macOS) to toggle the agent on and off.
|
||||
|
||||
## Voice Library Management
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Scene Tools
|
||||
|
||||
!!! note "Mac users"
|
||||
Throughout this guide, hold **Cmd** wherever **Ctrl** is mentioned. macOS reserves `Ctrl`+click for the system context menu, so Talemate also accepts the Command key as the primary modifier on every click and shortcut.
|
||||
|
||||
## Agent Activity Bar
|
||||
|
||||

|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
|
||||
<script>
|
||||
import AgentModal from './AgentModal.vue';
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
import AgentMessages from './AgentMessages.vue';
|
||||
|
||||
export default {
|
||||
@@ -192,7 +193,7 @@ export default {
|
||||
*/
|
||||
listItemClicked(event, agent, index) {
|
||||
// If the user is holding Ctrl (Windows/Linux) or Cmd (macOS)
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (isPrimaryModifier(event)) {
|
||||
// Only toggle agents that explicitly allow it
|
||||
if (agent.data && agent.data.has_toggle) {
|
||||
agent.enabled = !agent.enabled;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* Mixin for components that use AssetView to display and navigate through a list of assets
|
||||
*
|
||||
*
|
||||
* Requirements:
|
||||
* - Component must implement 'assets' computed property (Array)
|
||||
* - Component must implement 'getAssetSrc(assetId)' method (usually from VisualAssetsMixin)
|
||||
*/
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -32,7 +34,7 @@ export default {
|
||||
},
|
||||
|
||||
handleAssetClick(event, assetId, onMenuOpen) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (isPrimaryModifier(event)) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.viewAsset(assetId);
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
<script>
|
||||
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
|
||||
import { insertNewlineAtCursor } from '@/utils/textAreaUtils';
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
import MessageAssetImage from './MessageAssetImage.vue';
|
||||
import MessageAssetMixin from './MessageAssetMixin.js';
|
||||
export default {
|
||||
@@ -282,7 +283,7 @@ export default {
|
||||
// else -> submit
|
||||
// shift -> newline
|
||||
|
||||
if (event.ctrlKey) {
|
||||
if (isPrimaryModifier(event)) {
|
||||
this.autocompleteEdit();
|
||||
} else if (event.shiftKey) {
|
||||
insertNewlineAtCursor(this.$refs.textarea, this.editing_text, (v) => this.editing_text = v);
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
<script>
|
||||
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
|
||||
import { insertNewlineAtCursor } from '@/utils/textAreaUtils';
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
import MessageAssetImage from './MessageAssetImage.vue';
|
||||
import MessageAssetMixin from './MessageAssetMixin.js';
|
||||
|
||||
@@ -181,7 +182,7 @@ export default {
|
||||
// if ctrl -> autocomplete
|
||||
// else -> submit
|
||||
// shift -> newline
|
||||
if (event.ctrlKey) {
|
||||
if (isPrimaryModifier(event)) {
|
||||
this.autocompleteEdit();
|
||||
} else if (event.shiftKey) {
|
||||
insertNewlineAtCursor(this.$refs.textarea, this.editing_text, (v) => this.editing_text = v);
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
|
||||
// In-memory (non-persistent) per-uid selection memory, capped to 100 entries.
|
||||
// Uses Map insertion order as an LRU: re-setting a key moves it to the end.
|
||||
@@ -260,8 +261,8 @@ export default {
|
||||
this.dialog = true;
|
||||
this.busy = false;
|
||||
|
||||
// if ctrl key is pressed, open with instructions
|
||||
this.withInstructions = event.ctrlKey || this.requiresInstructions;
|
||||
// if ctrl/cmd key is pressed, open with instructions
|
||||
this.withInstructions = isPrimaryModifier(event) || this.requiresInstructions;
|
||||
|
||||
// if alt key is pressed, open with original
|
||||
this.withOriginal = event.altKey && this.rewriteEnabled;
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
<script>
|
||||
import AssetView from './AssetView.vue';
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
|
||||
export default {
|
||||
name: 'MessageAssetImage',
|
||||
@@ -164,8 +165,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
// Ctrl+click directly opens the image view
|
||||
if (event.ctrlKey && this.imageSrc) {
|
||||
// Ctrl/Cmd+click directly opens the image view
|
||||
if (isPrimaryModifier(event) && this.imageSrc) {
|
||||
this.openAssetView();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
<script>
|
||||
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
|
||||
import { insertNewlineAtCursor } from '@/utils/textAreaUtils';
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
import MessageAssetImage from './MessageAssetImage.vue';
|
||||
import MessageAssetMixin from './MessageAssetMixin.js';
|
||||
export default {
|
||||
@@ -225,7 +226,7 @@ export default {
|
||||
// else -> submit
|
||||
// shift -> newline
|
||||
|
||||
if (event.ctrlKey) {
|
||||
if (isPrimaryModifier(event)) {
|
||||
this.autocompleteEdit();
|
||||
} else if (event.shiftKey) {
|
||||
insertNewlineAtCursor(this.$refs.textarea, this.editing_text, (v) => this.editing_text = v);
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
:style="propertyEditorStyle"
|
||||
@keydown.capture="onPropertyEditorKeydown"
|
||||
></Codemirror>
|
||||
<span class="text-caption text-muted">(Ctrl+Enter to submit changes)</span>
|
||||
<span class="text-caption text-muted">({{ primaryModifierLabel }}+Enter to submit changes)</span>
|
||||
</v-card-text>
|
||||
<v-card-actions v-if="propertyEditorType === 'color'">
|
||||
<v-spacer></v-spacer>
|
||||
@@ -311,6 +311,7 @@
|
||||
<script>
|
||||
import { LGraphCanvas, LiteGraph } from 'litegraph.js';
|
||||
import { initializeGraphFromJSON, normalizeHexColor } from '@/utils/litegraphUtils'
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
import { convertGraphToJSON, convertSelectedGraphToJSON } from '@/utils/exportGraph.js'
|
||||
//import '@/utils/litegraphSearchBox'
|
||||
import { Codemirror } from 'vue-codemirror'
|
||||
@@ -367,6 +368,7 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
primaryModifierLabel,
|
||||
propertyEditor: false,
|
||||
propertyEditorType: 'text',
|
||||
propertyEditorValue: '',
|
||||
@@ -607,7 +609,7 @@ export default {
|
||||
|
||||
submitPropertyEditor(ev) {
|
||||
|
||||
if(ev && !(ev.ctrlKey || ev.metaKey)) {
|
||||
if(ev && !isPrimaryModifier(ev)) {
|
||||
return;
|
||||
}
|
||||
if(ev) {
|
||||
@@ -638,7 +640,7 @@ export default {
|
||||
},
|
||||
|
||||
onPropertyEditorKeydown(ev) {
|
||||
if ((ev.ctrlKey || ev.metaKey) && ev.key === 'Enter') {
|
||||
if (isPrimaryModifier(ev) && ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.submitPropertyEditor(ev);
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
v-model="input"
|
||||
ref="input_multiline"
|
||||
@keydown.enter.ctrl="proceed"
|
||||
@keydown.enter.meta="proceed"
|
||||
:placeholder="placeholder"
|
||||
:label="title"
|
||||
:rules="[rules.required]"
|
||||
messages="Press Ctrl+Enter to submit"
|
||||
:messages="`Press ${primaryModifierLabel}+Enter to submit`"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -31,10 +32,11 @@
|
||||
v-model="input"
|
||||
ref="input_text"
|
||||
@keydown.enter.ctrl="proceed"
|
||||
@keydown.enter.meta="proceed"
|
||||
:label="title"
|
||||
:placeholder="placeholder"
|
||||
:rules="[rules.required]"
|
||||
messages="Press Ctrl+Enter to submit"
|
||||
:messages="`Press ${primaryModifierLabel}+Enter to submit`"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -54,6 +56,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
|
||||
export default {
|
||||
name: "RequestInput",
|
||||
@@ -80,6 +83,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
primaryModifierLabel,
|
||||
open: false,
|
||||
valid: false,
|
||||
extra_params: {},
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
@click="handleViewImage"
|
||||
>
|
||||
<v-list-item-title>View Image</v-list-item-title>
|
||||
<v-list-item-subtitle>Ctrl+click</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>{{ primaryModifierLabel }}+click</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item
|
||||
@@ -375,6 +375,7 @@ import VisualReferenceCarousel from './VisualReferenceCarousel.vue';
|
||||
import ConfirmActionPrompt from './ConfirmActionPrompt.vue';
|
||||
import VisualAssetsMixin from './VisualAssetsMixin.js';
|
||||
import { isVisualAgentReady } from '../constants/visual.js';
|
||||
import { primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
|
||||
const MESSAGE_FLAGS = {
|
||||
NONE: 0,
|
||||
@@ -503,6 +504,7 @@ export default {
|
||||
emits: ['cancel-audio-queue'],
|
||||
data() {
|
||||
return {
|
||||
primaryModifierLabel,
|
||||
messages: [],
|
||||
selectedForkMessageId: null,
|
||||
defaultColors: {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<RequestInput
|
||||
ref="requestDirectedRegenerate"
|
||||
title="Directed regenerate"
|
||||
instructions="Provide instructions for regeneration. Ctrl+Enter submits."
|
||||
:instructions="`Provide instructions for regeneration. ${primaryModifierLabel}+Enter submits.`"
|
||||
icon="mdi-refresh"
|
||||
inputType="multiline"
|
||||
@continue="directedRegenerateContinue"
|
||||
@@ -107,7 +107,7 @@
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip :disabled="appBusy || !appReady" location="top"
|
||||
:text="'Redo most recent AI message.\n[Ctrl: Provide instructions, +Alt: Rewrite]'"
|
||||
:text="`Redo most recent AI message.\n[${primaryModifierLabel}: Provide instructions, +Alt: Rewrite]`"
|
||||
class="pre-wrap"
|
||||
max-width="300px">
|
||||
<template v-slot:activator="{ props }">
|
||||
@@ -119,7 +119,7 @@
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip :disabled="appBusy || !appReady" location="top"
|
||||
:text="'Redo most recent AI message (Nuke Option - use this to attempt to break out of repetition) \n[Ctrl: Provide instructions, +Alt: Rewrite]'"
|
||||
:text="`Redo most recent AI message (Nuke Option - use this to attempt to break out of repetition) \n[${primaryModifierLabel}: Provide instructions, +Alt: Rewrite]`"
|
||||
class="pre-wrap"
|
||||
max-width="300px">
|
||||
<template v-slot:activator="{ props }">
|
||||
@@ -223,6 +223,7 @@ import SceneToolsSettings from './SceneToolsSettings.vue';
|
||||
import SceneToolsSave from './SceneToolsSave.vue';
|
||||
import SceneToolsTime from './SceneToolsTime.vue';
|
||||
import RequestInput from './RequestInput.vue';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
export default {
|
||||
|
||||
name: 'SceneTools',
|
||||
@@ -284,6 +285,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
primaryModifierLabel,
|
||||
commandActive: false,
|
||||
commandName: null,
|
||||
autoSave: true,
|
||||
@@ -368,9 +370,9 @@ export default {
|
||||
},
|
||||
|
||||
regenerate(event) {
|
||||
// if ctrl is pressed use directed regenerate
|
||||
let withDirection = event.ctrlKey;
|
||||
let method = event.altKey || event.metaKey ? "edit" : "replace";
|
||||
// primary modifier (Ctrl/Cmd) opens the directed regenerate dialog
|
||||
let withDirection = isPrimaryModifier(event);
|
||||
let method = event.altKey ? "edit" : "replace";
|
||||
const nuke_repetition = 0.0;
|
||||
|
||||
if (withDirection) {
|
||||
@@ -390,9 +392,9 @@ export default {
|
||||
},
|
||||
|
||||
regenerateNuke(event) {
|
||||
// if ctrl is pressed use directed regenerate
|
||||
let withDirection = event.ctrlKey;
|
||||
let method = event.altKey || event.metaKey ? "edit" : "replace";
|
||||
// primary modifier (Ctrl/Cmd) opens the directed regenerate dialog
|
||||
let withDirection = isPrimaryModifier(event);
|
||||
let method = event.altKey ? "edit" : "replace";
|
||||
const nuke_repetition = 0.5;
|
||||
|
||||
if (withDirection) {
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
<v-list-item v-for="npcName in npcCharacters" :key="npcName"
|
||||
@click="(ev) => { actionGenerateActingAction(ev, null, {character: npcName}) }" prepend-icon="mdi-comment-account-outline">
|
||||
<v-list-item-title>Actor Action ({{ npcName }})</v-list-item-title>
|
||||
<v-list-item-subtitle>Generate Actor Action <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-subtitle>
|
||||
<v-list-item-subtitle>Generate Actor Action <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="actionGenerateActingAction" prepend-icon="mdi-comment-text-outline">
|
||||
<v-list-item-title>Actor Action</v-list-item-title>
|
||||
<v-list-item-subtitle>Generate Actor Action <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-subtitle>
|
||||
<v-list-item-subtitle>Generate Actor Action <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
<script>
|
||||
import RequestInput from './RequestInput.vue';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
|
||||
export default {
|
||||
name: "SceneToolsActor",
|
||||
@@ -42,6 +43,7 @@ export default {
|
||||
inject: ['getWebsocket'],
|
||||
data() {
|
||||
return {
|
||||
primaryModifierLabel,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -69,7 +71,7 @@ export default {
|
||||
|
||||
actionGenerateActingAction(ev, actingDirection="", params) {
|
||||
|
||||
if (ev.ctrlKey) {
|
||||
if (isPrimaryModifier(ev)) {
|
||||
this.requestDirection({action: 'GenerateActingAction', ...params});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="secondary">mdi-exit-run</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Take out of scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">Ctrl: no narration</v-chip></v-list-item-title>
|
||||
<v-list-item-title>Take out of scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">{{ primaryModifierLabel }}: no narration</v-chip></v-list-item-title>
|
||||
<v-list-item-subtitle>Make {{ character }} a passive character.</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="secondary">mdi-human-greeting</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Call into scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">Ctrl: no narration</v-chip></v-list-item-title>
|
||||
<v-list-item-title>Call into scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">{{ primaryModifierLabel }}: no narration</v-chip></v-list-item-title>
|
||||
<v-list-item-subtitle>Make {{ character }} an active character.</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="warning">mdi-human-greeting</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Introduce {{ character }}<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Advanced</v-chip></v-list-item-title>
|
||||
<v-list-item-title>Introduce {{ character }}<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Advanced</v-chip></v-list-item-title>
|
||||
<v-list-item-subtitle>Make {{ character }} an active character.</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
<script>
|
||||
|
||||
import WorldStateManagerTemplateApplicator from './WorldStateManagerTemplateApplicator.vue';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
export default {
|
||||
name: 'SceneToolsCreative',
|
||||
components: {
|
||||
@@ -186,6 +187,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
primaryModifierLabel,
|
||||
dialogIntroduceCharacter: false,
|
||||
newIntroduction: null,
|
||||
introduction: {
|
||||
@@ -246,12 +248,12 @@ export default {
|
||||
},
|
||||
|
||||
activateCharacter(ev, name) {
|
||||
let modifyNoNarration = ev.ctrlKey;
|
||||
let modifyNoNarration = isPrimaryModifier(ev);
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'director', action: 'activate_character', character_name: name, never_narrate: modifyNoNarration }));
|
||||
},
|
||||
|
||||
deactivateCharacter(ev, name) {
|
||||
let modifyNoNarration = ev.ctrlKey;
|
||||
let modifyNoNarration = isPrimaryModifier(ev);
|
||||
this.getWebsocket().send(JSON.stringify({ type: 'director', action: 'deactivate_character', character_name: name, never_narrate: modifyNoNarration }));
|
||||
},
|
||||
|
||||
@@ -276,7 +278,7 @@ export default {
|
||||
},
|
||||
|
||||
introduceCharacter(ev, name) {
|
||||
let advanced = ev.ctrlKey;
|
||||
let advanced = isPrimaryModifier(ev);
|
||||
let payload = {};
|
||||
|
||||
if(typeof name === 'string' && !advanced) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@click="actionRequestDynamicChoices"
|
||||
prepend-icon="mdi-tournament"
|
||||
>
|
||||
<v-list-item-title>Generate dynamic actions<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
|
||||
<v-list-item-title>Generate dynamic actions<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
|
||||
<v-list-item-subtitle>{{ getActAsCharacterName() }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<!-- Trigger scene direction turn -->
|
||||
@@ -23,7 +23,7 @@
|
||||
prepend-icon="mdi-movie-play"
|
||||
:disabled="!sceneDirectionEnabled"
|
||||
>
|
||||
<v-list-item-title>Scene direction turn<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
|
||||
<v-list-item-title>Scene direction turn<v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
|
||||
<v-list-item-subtitle>Manually trigger a scene direction turn</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<!-- Generate long progress -->
|
||||
@@ -156,6 +156,7 @@
|
||||
|
||||
<script>
|
||||
import RequestInput from './RequestInput.vue';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
|
||||
export default {
|
||||
name: "SceneToolsDirector",
|
||||
@@ -171,6 +172,7 @@ export default {
|
||||
inject: ['getWebsocket', 'getActAsCharacterName', 'openDirectorConsole', 'openAgentSettings'],
|
||||
data() {
|
||||
return {
|
||||
primaryModifierLabel,
|
||||
scenePlanDialog: false,
|
||||
scenePlanInstructions: '',
|
||||
scenePlanBeats: 8,
|
||||
@@ -222,7 +224,7 @@ export default {
|
||||
|
||||
actionRequestDynamicChoices(ev, instructions="") {
|
||||
|
||||
if (ev.ctrlKey) {
|
||||
if (isPrimaryModifier(ev)) {
|
||||
this.requestDirection({action: 'RequestDynamicChoices'});
|
||||
return;
|
||||
}
|
||||
@@ -245,7 +247,7 @@ export default {
|
||||
|
||||
actionSceneDirectionTurn(ev, instructions="") {
|
||||
|
||||
if (ev.ctrlKey) {
|
||||
if (isPrimaryModifier(ev)) {
|
||||
this.requestDirection({action: 'SceneDirectionTurn'});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
@click="actionProgress"
|
||||
prepend-icon="mdi-script-text-play"
|
||||
>
|
||||
<v-list-item-title>Progress Story <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
|
||||
<v-list-item-title>Progress Story <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
|
||||
<v-btn @click.stop="(e) => { actionProgress(e, PROGRESS_SHORTCUT_MINOR) }" variant="tonal" color="highlight2" class="mr-1" size="x-small" :prepend-icon="'mdi-dice-multiple'">Minor</v-btn>
|
||||
<v-btn @click.stop="(e) => { actionProgress(e, PROGRESS_SHORTCUT_MAJOR) }" variant="tonal" color="highlight2" class="mr-1" size="x-small" :prepend-icon="'mdi-dice-multiple'">Major</v-btn>
|
||||
<v-btn @click.stop="(e) => { actionProgress(e, PROGRESS_SHORTCUT_CURVEBALL) }" variant="tonal" color="highlight2" size="x-small" :prepend-icon="'mdi-dice-multiple'">Curveball</v-btn>
|
||||
@@ -28,7 +28,7 @@
|
||||
@click="actionNarrateEnvironment"
|
||||
prepend-icon="mdi-waves"
|
||||
>
|
||||
<v-list-item-title>Narrate Environment <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
|
||||
<v-list-item-title>Narrate Environment <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Look at Scene -->
|
||||
@@ -37,7 +37,7 @@
|
||||
@click="actionLookAtScene"
|
||||
prepend-icon="mdi-image-filter-hdr"
|
||||
>
|
||||
<v-list-item-title>Look at Scene <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
|
||||
<v-list-item-title>Look at Scene <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Look at NPCs -->
|
||||
@@ -47,7 +47,7 @@
|
||||
@click="(ev) => { actionLookAtCharacter(ev, null, {character: npc_name}) }"
|
||||
prepend-icon="mdi-account-eye"
|
||||
>
|
||||
<v-list-item-title>Look at {{ npc_name }} <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">Ctrl: Provide direction</v-chip></v-list-item-title>
|
||||
<v-list-item-title>Look at {{ npc_name }} <v-chip variant="text" color="highlight5" class="ml-1" size="x-small">{{ primaryModifierLabel }}: Provide direction</v-chip></v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Query -->
|
||||
@@ -76,6 +76,7 @@
|
||||
|
||||
<script>
|
||||
import RequestInput from './RequestInput.vue';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
|
||||
const PROGRESS_SHORTCUT_MINOR = "Introduce a minor change to the story.";
|
||||
const PROGRESS_SHORTCUT_MAJOR = "Introduce a major shift in the story.";
|
||||
@@ -96,6 +97,7 @@ export default {
|
||||
PROGRESS_SHORTCUT_MINOR,
|
||||
PROGRESS_SHORTCUT_MAJOR,
|
||||
PROGRESS_SHORTCUT_CURVEBALL,
|
||||
primaryModifierLabel,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -146,7 +148,7 @@ export default {
|
||||
|
||||
actionProgress(ev, narrativeDirection="") {
|
||||
|
||||
if (ev.ctrlKey) {
|
||||
if (isPrimaryModifier(ev)) {
|
||||
this.requestDirection({action: 'Progress', input: narrativeDirection});
|
||||
return;
|
||||
}
|
||||
@@ -168,7 +170,7 @@ export default {
|
||||
|
||||
actionNarrateEnvironment(ev, narrativeDirection="") {
|
||||
|
||||
if (ev.ctrlKey) {
|
||||
if (isPrimaryModifier(ev)) {
|
||||
this.requestDirection({action: 'NarrateEnvironment'});
|
||||
return;
|
||||
}
|
||||
@@ -191,7 +193,7 @@ export default {
|
||||
|
||||
actionLookAtScene(ev, narrativeDirection="") {
|
||||
|
||||
if (ev.ctrlKey) {
|
||||
if (isPrimaryModifier(ev)) {
|
||||
this.requestDirection({action: 'LookAtScene'});
|
||||
return;
|
||||
}
|
||||
@@ -213,8 +215,8 @@ export default {
|
||||
*/
|
||||
|
||||
actionLookAtCharacter(ev, narrativeDirection="", params) {
|
||||
|
||||
if (ev.ctrlKey) {
|
||||
|
||||
if (isPrimaryModifier(ev)) {
|
||||
this.requestDirection({action: 'LookAtCharacter', ...params});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { VIS_TYPE_OPTIONS } from '../constants/visual';
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
export default {
|
||||
name: 'SceneToolsVisual',
|
||||
props: {
|
||||
@@ -183,7 +184,7 @@ export default {
|
||||
return null;
|
||||
},
|
||||
handleVisualize(character_name, event, vis_type = 'CHARACTER_CARD') {
|
||||
const ctrlPressed = event.ctrlKey || event.metaKey;
|
||||
const ctrlPressed = isPrimaryModifier(event);
|
||||
const altPressed = event.altKey;
|
||||
|
||||
if (ctrlPressed) {
|
||||
|
||||
@@ -294,7 +294,9 @@
|
||||
ref="messageInput"
|
||||
@keydown.enter.prevent="sendMessage"
|
||||
@keydown.ctrl.up.prevent="onHistoryUp"
|
||||
@keydown.meta.up.prevent="onHistoryUp"
|
||||
@keydown.ctrl.down.prevent="onHistoryDown"
|
||||
@keydown.meta.down.prevent="onHistoryDown"
|
||||
@keydown.tab.prevent="cycleActAs"
|
||||
:hint="messageInputLongHint()"
|
||||
:disabled="busy || !ready || uxInteractionActive || isInputDisabled()"
|
||||
@@ -386,6 +388,7 @@
|
||||
|
||||
<script>
|
||||
import AIClient from './AIClient.vue';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
import AIAgent from './AIAgent.vue';
|
||||
import AgentActivityBar from './AgentActivityBar.vue';
|
||||
import LoadScene from './LoadScene.vue';
|
||||
@@ -1318,8 +1321,8 @@ export default {
|
||||
|
||||
sendMessage(event) {
|
||||
|
||||
// if ctrl+enter is pressed, request autocomplete
|
||||
if (event.ctrlKey && event.key === 'Enter') {
|
||||
// if ctrl/cmd+enter is pressed, request autocomplete
|
||||
if (isPrimaryModifier(event) && event.key === 'Enter') {
|
||||
return this.autocomplete();
|
||||
}
|
||||
|
||||
@@ -1525,7 +1528,7 @@ export default {
|
||||
},
|
||||
|
||||
autocompleteInfoMessage(active) {
|
||||
return active ? 'Generating ...' : "Ctrl+Enter to autocomplete";
|
||||
return active ? 'Generating ...' : `${primaryModifierLabel}+Enter to autocomplete`;
|
||||
},
|
||||
|
||||
requestAppConfig() {
|
||||
@@ -1803,7 +1806,7 @@ export default {
|
||||
},
|
||||
|
||||
messageInputLongHint() {
|
||||
const DIALOG_HINT = "Ctrl+Enter to autocomplete, Shift+Enter for newline, Ctrl+Up/Down for history, Tab to act as another character. Start messages with '@' to do an action. (e.g., '@look at the door')";
|
||||
const DIALOG_HINT = `${primaryModifierLabel}+Enter to autocomplete, Shift+Enter for newline, ${primaryModifierLabel}+Up/Down for history, Tab to act as another character. Start messages with '@' to do an action. (e.g., '@look at the door')`;
|
||||
|
||||
if(this.waitingForInput) {
|
||||
if(this.inputRequestInfo.reason === "talk") {
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
|
||||
<script>
|
||||
import { VIS_TYPE_OPTIONS } from '../constants/visual.js';
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
import VisualReferenceImages from './VisualReferenceImages.vue';
|
||||
import CoverBBoxEditor from './CoverBBoxEditor.vue';
|
||||
export default {
|
||||
@@ -392,7 +393,7 @@ export default {
|
||||
},
|
||||
handleAnalyzeClick(event) {
|
||||
// If Ctrl/Cmd is pressed, open dialog; otherwise quick analyze
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (isPrimaryModifier(event)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.showAnalyzeDialog = true;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
:loading="analyzing"
|
||||
>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
Analyze the image using AI. (Ctrl: set instructions)
|
||||
Analyze the image using AI. ({{ primaryModifierLabel }}: set instructions)
|
||||
</v-tooltip>
|
||||
Analyze
|
||||
</v-btn>
|
||||
@@ -158,6 +158,7 @@ import ConfirmActionPrompt from './ConfirmActionPrompt.vue';
|
||||
import VisualLibraryUpload from './VisualLibraryUpload.vue';
|
||||
import VisualAssetsTree from './VisualAssetsTree.vue';
|
||||
import { debounce } from 'lodash';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
|
||||
export default {
|
||||
name: 'VisualLibraryScene',
|
||||
@@ -175,6 +176,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
primaryModifierLabel,
|
||||
base64ById: {},
|
||||
assetSearchInput: '',
|
||||
assetSearch: '',
|
||||
@@ -399,7 +401,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
// If Ctrl/Cmd is pressed, open dialog; otherwise quick analyze
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (isPrimaryModifier(event)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.showAnalyzeDialog = true;
|
||||
|
||||
@@ -115,10 +115,10 @@
|
||||
<!-- GENERATE CHANGE SUGGESTIONS -->
|
||||
<div>
|
||||
<v-list-item>
|
||||
<v-tooltip max-width="300" :text="`Generate change suggestions for ${character.name}. This will provide a list of suggestions for changes to the character, based on the progression of the story thus far. [Ctrl: Provide instructions]`">
|
||||
<v-tooltip max-width="300" :text="`Generate change suggestions for ${character.name}. This will provide a list of suggestions for changes to the character, based on the progression of the story thus far. [${primaryModifierLabel}: Provide instructions]`">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
@click.stop="(event) => { suggestChanges(character.name, event.ctrlKey)}"
|
||||
<v-btn
|
||||
@click.stop="(event) => { suggestChanges(character.name, isPrimaryModifier(event))}"
|
||||
v-bind="props"
|
||||
variant="tonal"
|
||||
:disabled="appBusy || !appReady"
|
||||
@@ -350,6 +350,7 @@ import WorldStateManagerCharacterActor from './WorldStateManagerCharacterActor.v
|
||||
import WorldStateManagerCharacterCreator from './WorldStateManagerCharacterCreator.vue';
|
||||
import WorldStateManagerCharacterVisuals from './WorldStateManagerCharacterVisuals.vue';
|
||||
import { MAX_CONTENT_WIDTH, FOLDER_NAME_MAX_LENGTH } from '@/constants';
|
||||
import { isPrimaryModifier, primaryModifierLabel } from '@/utils/keyboardModifiers';
|
||||
|
||||
export default {
|
||||
name: 'WorldStateManagerCharacter',
|
||||
@@ -397,6 +398,7 @@ export default {
|
||||
folderInput: '',
|
||||
folderMenuOpen: false,
|
||||
MAX_CONTENT_WIDTH,
|
||||
primaryModifierLabel,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -447,6 +449,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isPrimaryModifier,
|
||||
reset() {
|
||||
this.selected = null;
|
||||
this.character = null;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="text-primary">{{ pendingDelete ? 'Deleting...' : 'Regenerating...' }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-btn v-if="hasSourceEntries" :disabled="editing || locked" prepend-icon="mdi-refresh" color="primary" @click="(ev) => regenerateEntry(ev.ctrlKey)">Regenerate</v-btn>
|
||||
<v-btn v-if="hasSourceEntries" :disabled="editing || locked" prepend-icon="mdi-refresh" color="primary" @click="(ev) => regenerateEntry(isPrimaryModifier(ev))">Regenerate</v-btn>
|
||||
<v-btn v-if="hasSourceEntries" :disabled="editing || locked" color="primary" prepend-icon="mdi-magnify-expand" @click="toggleSourceEntries">{{ entry.source_entries ? 'Collapse' : 'Inspect' }}</v-btn>
|
||||
<v-btn v-if="hasSourceEntries" :disabled="editing || locked" prepend-icon="mdi-clock-plus-outline" color="primary" @click="insertingTimePassage = true">Time Passage</v-btn>
|
||||
<ConfirmActionInline
|
||||
@@ -106,6 +106,7 @@ class HistoryEntry(pydantic.BaseModel):
|
||||
*/
|
||||
|
||||
import { SceneTextParser } from '@/utils/sceneMessageRenderer';
|
||||
import { isPrimaryModifier } from '@/utils/keyboardModifiers';
|
||||
import ContextualGenerate from './ContextualGenerate.vue';
|
||||
import ConfirmActionInline from './ConfirmActionInline.vue';
|
||||
|
||||
@@ -165,6 +166,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isPrimaryModifier,
|
||||
handleEnter(ev) {
|
||||
if(!ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
|
||||
19
talemate_frontend/src/utils/keyboardModifiers.js
Normal file
19
talemate_frontend/src/utils/keyboardModifiers.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// Helpers for cross-platform keyboard modifier handling.
|
||||
//
|
||||
// On macOS, Ctrl+click is intercepted by the OS as a context-menu (right-click)
|
||||
// gesture, so any "hold Ctrl while clicking" affordance is unreachable for Mac
|
||||
// users. Treat the Cmd (Meta) key as the equivalent primary modifier so the
|
||||
// same interactions work on every platform.
|
||||
|
||||
export const isMac = typeof navigator !== 'undefined'
|
||||
&& /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent || '');
|
||||
|
||||
// True when the user is holding the platform's primary modifier (Ctrl on
|
||||
// Windows/Linux, Cmd on macOS). Accept both on every platform so external
|
||||
// keyboards keep working.
|
||||
export function isPrimaryModifier(event) {
|
||||
return !!event && (event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
// Human-readable label for the primary modifier, for use in tooltips.
|
||||
export const primaryModifierLabel = isMac ? 'Cmd' : 'Ctrl';
|
||||
@@ -4,6 +4,7 @@ import { CommentNode } from './commentNode.js';
|
||||
import { trackRecentNodes } from './recentNodes.js';
|
||||
import { handleFitGroupToNodes, handleDuplicateGroup, handleVerticalSnapGroup, handleCreateGroupFromSelectedNodes } from './groupInteractions.js';
|
||||
import { handleWatchNodeShortcut, handleSetStateNodeShortcut, handleStageNodeShortcut } from './graphConnectionUtil.js';
|
||||
import { isPrimaryModifier } from './keyboardModifiers.js';
|
||||
|
||||
const UNRESOLVED = "<class 'talemate.game.engine.nodes.core.UNRESOLVED'>";
|
||||
|
||||
@@ -1605,7 +1606,7 @@ LGraphCanvas.prototype.processKey = function(e) {
|
||||
var key_code = e.keyCode;
|
||||
|
||||
// Ctrl+C to copy
|
||||
if (e.type == "keydown" && (e.ctrlKey || e.metaKey) && key_code == 67) {
|
||||
if (e.type == "keydown" && isPrimaryModifier(e) && key_code == 67) {
|
||||
if (this.selected_nodes) {
|
||||
// Store the selected nodes for copy
|
||||
var selected_list = [];
|
||||
@@ -1672,7 +1673,7 @@ LGraphCanvas.prototype.processKey = function(e) {
|
||||
block_default = true;
|
||||
}
|
||||
// Ctrl+V to paste
|
||||
else if (e.type == "keydown" && (e.ctrlKey || e.metaKey) && key_code == 86) {
|
||||
else if (e.type == "keydown" && isPrimaryModifier(e) && key_code == 86) {
|
||||
if (LiteGraph._clipboard_data && LiteGraph._clipboard_data.nodes && LiteGraph._clipboard_data.nodes.length) {
|
||||
this.graph.beforeChange();
|
||||
|
||||
@@ -1835,8 +1836,8 @@ LGraphCanvas.prototype.processMouseDown = function(e) {
|
||||
|
||||
if (isClickOnTitle) {
|
||||
let handled = false;
|
||||
// --- Ctrl+Click: Fit Group to Nodes ---
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
// --- Ctrl/Cmd+Click: Fit Group to Nodes ---
|
||||
if (isPrimaryModifier(e)) {
|
||||
handled = handleFitGroupToNodes(group, this);
|
||||
}
|
||||
// --- Shift+Click: Duplicate Group ---
|
||||
|
||||
Reference in New Issue
Block a user