llm prompt template management ux

This commit is contained in:
vegu-ai-tools
2026-04-04 13:51:13 +03:00
parent 12a0652215
commit 6fcc337fa1
8 changed files with 684 additions and 7 deletions

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ logs/
# uv
.venv/
templates/llm-prompt/user/*.jinja2
templates/llm-prompt/std/user/
templates/world-state/*.yaml
tts/voice/piper/*.onnx
tts/voice/piper/*.json

View File

@@ -12,6 +12,7 @@
- "Prompt From Template Node: Added a dedupe property (default on) to control prompt deduplication. Useful for templates with structured repeated content like beat listings that should not be deduplicated."
- "Gemma 4 Prompt Template: Added a prompt template for Google's Gemma 4 model family, with support for toggling thinking mode via the reasoning tokens setting."
- "Advance Time: Moved time tools into a dedicated sub-component with grouped presets (Minutes, Hours, Days, Weeks, Months, Years) and a custom time dialog for arbitrary durations."
- "LLM Prompt Templates Manager: Added a dedicated UI tab for viewing, creating, editing, and deleting LLM prompt templates. Built-in templates are read-only and can be copied to user templates. User templates are stored in std/user/ and gitignored. GGUF/llama.cpp chat templates can be used directly."
fixes:
- "Message Regeneration: Fixed a bug where regenerating a message that fails (e.g., missing agent or function) would permanently remove the original message from the scene. The original message is now restored to both history and UI on failure, and unhandled exceptions during regeneration are caught."
- "Anthropic Client: Fixed generation error caused by missing required max_tokens parameter when response length capping is disabled. Now defaults to the model's API output token limit."
@@ -26,6 +27,7 @@
- "Encryption: Fixed API keys in agent action configs (e.g., OpenAI-compatible TTS, visual backends) being stored in plaintext. The encryption walker now handles the nested AgentActionConfig structure where the key is wrapped in a {value: ...} dict."
- "Agent Config: Stopped persisting unified_api_key fields to config.yaml. These are static reference pointers (e.g., 'openai.api_key') defined in code and never change, so saving them was unnecessary."
- "Contextual Generate: Fixed list-type generation prefilling an empty line after '1.', which caused some LLMs to produce empty lists."
- "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."
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."

View File

@@ -1,3 +1,4 @@
import datetime
import json
import os
import shutil
@@ -23,6 +24,9 @@ BASE_TEMPLATE_PATH = os.path.join(
# holds the default templates
STD_TEMPLATE_PATH = os.path.join(BASE_TEMPLATE_PATH, "std")
# user-supplied base templates (sits inside std/ but gitignored)
STD_USER_TEMPLATE_PATH = os.path.join(STD_TEMPLATE_PATH, "user")
# llm prompt templates provided by talemate
TALEMATE_TEMPLATE_PATH = os.path.join(BASE_TEMPLATE_PATH, "talemate")
@@ -42,6 +46,10 @@ def register_template_identifier(cls):
log = structlog.get_logger("talemate.model_prompts")
def _raise_exception(msg):
raise Exception(msg)
class PromptSpec(pydantic.BaseModel):
template: str | None = None
reasoning_pattern: str | None = None
@@ -77,7 +85,12 @@ class ModelPrompt:
@property
def std_templates(self) -> list[str]:
env = Environment(loader=FileSystemLoader(STD_TEMPLATE_PATH))
return sorted(env.list_templates())
all_templates = env.list_templates()
# Built-in: everything not under user/ subdirectory
builtin = [t for t in all_templates if not t.startswith("user/")]
# User-supplied: explicitly from user/ subdir
user = [t for t in all_templates if t.startswith("user/")]
return sorted(builtin) + sorted(user)
def __call__(
self,
@@ -112,9 +125,18 @@ class ModelPrompt:
spec.template = template_file
# Build GGUF/llama.cpp compatible messages list
messages = []
if system_message:
messages.append({"role": "system", "content": system_message})
messages.append({"role": "user", "content": user_message.strip()})
if coercion_message:
messages.append({"role": "assistant", "content": coercion_message})
return (
template.render(
{
# Talemate native vars
"system_message": system_message,
"prompt": prompt.strip(),
"user_message": user_message.strip(),
@@ -124,6 +146,15 @@ class ModelPrompt:
),
"reasoning_tokens": reasoning_tokens,
"spec": spec,
# GGUF/llama.cpp compatible vars
"messages": messages,
"bos_token": "",
"eos_token": "",
"add_generation_prompt": True,
"enable_thinking": reasoning_tokens > 0,
"thinking_budget": reasoning_tokens,
"strftime_now": lambda fmt: datetime.datetime.now().strftime(fmt),
"raise_exception": _raise_exception,
}
),
template_file,
@@ -193,19 +224,71 @@ class ModelPrompt:
def create_user_override(self, template_name: str, model_name: str):
"""
Will copy STD_TEMPLATE_PATH/template_name to USER_TEMPLATE_PATH/model_name.jinja2
Will copy a std template to USER_TEMPLATE_PATH/model_name.jinja2
Supports both built-in templates (e.g. "ChatML.jinja2") and
user-supplied templates (e.g. "user/MyTemplate.jinja2").
"""
template_name = template_name.split(".jinja2")[0]
cleaned_model_name = self.clean_model_name(model_name)
shutil.copyfile(
os.path.join(STD_TEMPLATE_PATH, template_name + ".jinja2"),
os.path.join(USER_TEMPLATE_PATH, cleaned_model_name + ".jinja2"),
)
if template_name.startswith("user/"):
safe_name = os.path.basename(template_name[5:])
source_path = os.path.join(
STD_USER_TEMPLATE_PATH, safe_name + ".jinja2"
)
else:
source_path = os.path.join(STD_TEMPLATE_PATH, template_name + ".jinja2")
return os.path.join(USER_TEMPLATE_PATH, cleaned_model_name + ".jinja2")
dest_path = os.path.join(USER_TEMPLATE_PATH, cleaned_model_name + ".jinja2")
shutil.copyfile(source_path, dest_path)
return dest_path
def _list_templates_in_dir(self, directory: str) -> list[dict]:
"""List .jinja2 templates in a directory with their content."""
if not os.path.isdir(directory):
return []
results = []
for fname in sorted(os.listdir(directory)):
if not fname.endswith(".jinja2"):
continue
fpath = os.path.join(directory, fname)
if not os.path.isfile(fpath):
continue
with open(fpath, "r", encoding="utf-8") as f:
results.append({"name": fname, "content": f.read()})
return results
def list_std_builtin_templates(self) -> list[dict]:
"""List built-in std/ templates with their content."""
return self._list_templates_in_dir(STD_TEMPLATE_PATH)
def list_std_user_templates(self) -> list[dict]:
"""List user-supplied templates in std/user/ with their content."""
return self._list_templates_in_dir(STD_USER_TEMPLATE_PATH)
def save_std_user_template(self, template_name: str, content: str) -> str:
"""Save/create a template in std/user/. Returns the file path."""
os.makedirs(STD_USER_TEMPLATE_PATH, exist_ok=True)
safe_name = os.path.basename(template_name)
if not safe_name.endswith(".jinja2"):
safe_name += ".jinja2"
fpath = os.path.join(STD_USER_TEMPLATE_PATH, safe_name)
with open(fpath, "w", encoding="utf-8") as f:
f.write(content)
return fpath
def delete_std_user_template(self, template_name: str) -> bool:
"""Delete a template from std/user/."""
safe_name = os.path.basename(template_name)
fpath = os.path.join(STD_USER_TEMPLATE_PATH, safe_name)
if os.path.isfile(fpath):
os.remove(fpath)
return True
return False
def query_hf_for_prompt_template_suggestion(self, model_name: str):
api = huggingface_hub.HfApi()

View File

@@ -42,6 +42,15 @@ class DetermineLLMTemplatePayload(pydantic.BaseModel):
client_name: str | None = None
class SaveLLMTemplatePayload(pydantic.BaseModel):
name: str
content: str
class DeleteLLMTemplatePayload(pydantic.BaseModel):
name: str
class ToggleClientPayload(pydantic.BaseModel):
name: str
state: bool
@@ -142,6 +151,57 @@ class ConfigPlugin(Plugin):
}
)
async def handle_list_llm_templates(self, data):
"""List all std/ built-in and std/user/ templates with content."""
self.websocket_handler.queue_put(
{
"type": "config",
"action": "llm_templates_list",
"data": {
"builtin": model_prompt.list_std_builtin_templates(),
"user": model_prompt.list_std_user_templates(),
},
}
)
async def handle_save_llm_template(self, data):
"""Save/create a user-supplied LLM prompt template in std/user/."""
payload = SaveLLMTemplatePayload(**data["data"])
try:
model_prompt.save_std_user_template(payload.name, payload.content)
log.info("Saved LLM template", name=payload.name)
self.websocket_handler.queue_put(
{
"type": "config",
"action": "save_llm_template_complete",
"data": {"success": True, "name": payload.name},
}
)
except Exception as e:
log.error("Failed to save LLM template", name=payload.name, error=str(e))
self.websocket_handler.queue_put(
{
"type": "config",
"action": "save_llm_template_complete",
"data": {"success": False, "error": str(e)},
}
)
async def handle_delete_llm_template(self, data):
"""Delete a user-supplied LLM prompt template from std/user/."""
payload = DeleteLLMTemplatePayload(**data["data"])
deleted = model_prompt.delete_std_user_template(payload.name)
log.info("Deleted LLM template", name=payload.name, deleted=deleted)
self.websocket_handler.queue_put(
{
"type": "config",
"action": "delete_llm_template_complete",
"data": {"success": deleted, "name": payload.name},
}
)
async def handle_set_llm_template(self, data):
payload = SetLLMTemplatePayload(**data["data"])

View File

@@ -108,6 +108,8 @@
<v-card-actions v-if="!waitingForTemplateSelection">
<v-btn @click.stop="determineBestTemplate" prepend-icon="mdi-web-box">Determine via
HuggingFace</v-btn>
<v-btn @click.stop="openLLMTemplates" prepend-icon="mdi-file-cog-outline">Manage
Templates</v-btn>
</v-card-actions>
</v-card>
<v-checkbox v-model="client.lock_template" hint="If checked, the prompt template will not longer automatically update." density="compact" color="primary">
@@ -364,6 +366,7 @@ export default {
'state',
'getWebsocket',
'registerMessageHandler',
'navigateToLLMTemplates',
],
data() {
return {
@@ -624,6 +627,10 @@ export default {
close() {
this.$emit('update:dialog', false);
},
openLLMTemplates() {
this.close();
this.navigateToLLMTemplates();
},
save() {
if (!this.validateName()) {

View File

@@ -856,6 +856,12 @@ export default {
},
callAgentTool: (actionName, args) => this.callAgentTool(actionName, args),
openDirectorConsole: () => this.toggleNavigation('directorConsole', true),
navigateToLLMTemplates: () => {
this.tab = 'prompts';
this.$nextTick(() => {
this.promptsMainTab = 'llm-templates';
});
},
};
},
methods: {

View File

@@ -0,0 +1,507 @@
<template>
<div class="llm-templates-tab">
<!-- Header -->
<div class="header d-flex align-center pa-3">
<div>
<span class="text-subtitle-1 font-weight-medium">
<v-icon start size="small">mdi-code-braces</v-icon>
LLM Prompt Templates
</span>
<span class="text-caption text-grey ml-2">
Base chat formatting templates for local LLM inference
</span>
</div>
<v-spacer></v-spacer>
<v-btn
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-plus"
@click="openNewDialog"
>
New Template
<v-tooltip activator="parent" location="top">Create a new user template in std/user/</v-tooltip>
</v-btn>
</div>
<v-divider></v-divider>
<!-- Main Content: List + Editor -->
<v-row no-gutters class="content-split">
<!-- List Panel -->
<v-col cols="auto" class="list-panel pa-2">
<div v-if="loading" class="d-flex justify-center align-center pa-8">
<v-progress-circular indeterminate color="primary" size="32"></v-progress-circular>
</div>
<template v-else>
<!-- User Templates -->
<div class="text-subtitle-2 text-grey mb-1">
<v-icon size="small" class="mr-1">mdi-account-edit</v-icon>
User Templates
<span class="text-caption">(editable)</span>
</div>
<v-list density="compact" class="mb-3" v-if="userTemplates.length > 0">
<v-list-item
v-for="tmpl in userTemplates"
:key="'user/' + tmpl.name"
:active="selectedKey === 'user/' + tmpl.name"
@click="selectTemplate('user', tmpl)"
class="template-item"
>
<template v-slot:prepend>
<v-icon size="small" color="primary">mdi-file-document-edit-outline</v-icon>
</template>
<v-list-item-title class="text-body-2">user/{{ tmpl.name }}</v-list-item-title>
</v-list-item>
</v-list>
<div v-else class="text-caption text-grey pa-2 mb-3">
No user templates yet. Click "New Template" or copy a built-in template.
</div>
<v-divider class="mb-2"></v-divider>
<!-- Built-in Templates -->
<div class="text-subtitle-2 text-grey mb-1">
<v-icon size="small" class="mr-1">mdi-lock-outline</v-icon>
Built-in Templates
<span class="text-caption">(read-only)</span>
</div>
<v-list density="compact">
<v-list-item
v-for="tmpl in builtinTemplates"
:key="'builtin/' + tmpl.name"
:active="selectedKey === 'builtin/' + tmpl.name"
@click="selectTemplate('builtin', tmpl)"
class="template-item"
>
<template v-slot:prepend>
<v-icon size="small" color="grey">mdi-file-document-outline</v-icon>
</template>
<v-list-item-title class="text-body-2">{{ tmpl.name }}</v-list-item-title>
</v-list-item>
</v-list>
</template>
</v-col>
<!-- Editor Panel -->
<v-col class="editor-panel pa-2">
<div class="text-subtitle-2 text-grey mb-2">
<v-icon size="small" class="mr-1">mdi-code-braces</v-icon>
{{ selectedSource === 'builtin' ? 'Preview' : 'Editor' }}
</div>
<v-card v-if="selectedTemplate" flat class="editor-container">
<v-card-subtitle class="pa-2 d-flex align-center">
<v-chip size="small" label :color="selectedSource === 'user' ? 'primary' : 'grey'" variant="tonal">
{{ selectedSource === 'user' ? 'user/' : '' }}{{ selectedTemplate.name }}
</v-chip>
<v-chip
v-if="selectedSource === 'builtin'"
size="small"
label
color="grey"
variant="outlined"
class="ml-2"
>
read-only
</v-chip>
<v-chip
v-if="isDirty"
size="small"
label
color="warning"
variant="tonal"
class="ml-2"
>
unsaved
</v-chip>
<v-spacer></v-spacer>
<div class="actions d-flex ga-2">
<v-btn
v-if="selectedSource === 'builtin'"
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-content-copy"
@click="copyToUser"
>
Copy to User Templates
</v-btn>
<v-btn
v-if="selectedSource === 'user'"
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-content-save"
:disabled="!isDirty"
:loading="saving"
@click="saveTemplate"
>
Save
</v-btn>
<v-btn
v-if="selectedSource === 'user'"
size="small"
variant="tonal"
color="error"
prepend-icon="mdi-delete"
@click="confirmDelete"
>
Delete
</v-btn>
</div>
</v-card-subtitle>
<v-card-text class="pa-0">
<Codemirror
v-model="editorContent"
:extensions="extensions"
:disabled="selectedSource === 'builtin'"
class="code-editor"
/>
</v-card-text>
</v-card>
<v-card v-else flat color="transparent" class="d-flex align-center justify-center" style="min-height: 300px;">
<v-card-text class="text-center text-grey">
<v-icon size="64" color="grey-darken-1">mdi-file-document-edit-outline</v-icon>
<div class="mt-2">Select a template to view or edit</div>
<div class="text-caption">User templates are fully editable. Built-in templates are read-only but can be copied.</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- New Template Dialog -->
<v-dialog v-model="showNewDialog" max-width="450">
<v-card>
<v-card-title>
<v-icon class="mr-2">mdi-file-plus</v-icon>
Create New LLM Template
</v-card-title>
<v-card-text>
<v-form ref="newForm" v-model="newFormValid" @submit.prevent="createTemplate">
<v-text-field
v-model="newTemplateName"
label="Template name"
:rules="[
v => !!v || 'Name is required',
v => validateFileName(v)
]"
required
autofocus
hint="e.g. MyModel. Extension .jinja2 will be added automatically."
persistent-hint
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showNewDialog = false">Cancel</v-btn>
<v-btn color="primary" variant="tonal" :disabled="!newFormValid" @click="createTemplate">Create</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Delete Confirmation -->
<ConfirmActionPrompt
ref="deletePrompt"
actionLabel="Delete Template"
:description="`Permanently delete user/${selectedTemplate?.name || ''}?`"
icon="mdi-delete"
color="error"
@confirm="deleteTemplate"
/>
<!-- Toast notification -->
<v-snackbar
v-model="showToast"
:color="toastColor"
:timeout="5000"
location="top"
>
{{ toastMessage }}
</v-snackbar>
</div>
</template>
<script>
import { Codemirror } from 'vue-codemirror';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { languages } from '@codemirror/language-data';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view';
import ConfirmActionPrompt from '../ConfirmActionPrompt.vue';
export default {
name: 'LLMTemplatesTab',
components: {
Codemirror,
ConfirmActionPrompt,
},
inject: [
'getWebsocket',
'registerMessageHandler',
'unregisterMessageHandler',
],
data() {
return {
builtinTemplates: [],
userTemplates: [],
loading: false,
saving: false,
// Selection state
selectedSource: null, // 'builtin' or 'user'
selectedTemplate: null, // {name, content}
editorContent: '',
originalContent: '',
// New template dialog
showNewDialog: false,
newTemplateName: '',
newFormValid: false,
// Toast
showToast: false,
toastMessage: '',
toastColor: 'success',
};
},
computed: {
selectedKey() {
if (!this.selectedTemplate || !this.selectedSource) return null;
return `${this.selectedSource}/${this.selectedTemplate.name}`;
},
isDirty() {
return this.selectedSource === 'user' && this.editorContent !== this.originalContent;
},
extensions() {
return [
markdown({
base: markdownLanguage,
codeLanguages: languages,
}),
oneDark,
EditorView.lineWrapping,
];
},
},
methods: {
requestTemplates() {
this.loading = true;
this.getWebsocket().send(JSON.stringify({
type: 'config',
action: 'list_llm_templates',
data: {},
}));
},
selectTemplate(source, tmpl) {
this.selectedSource = source;
this.selectedTemplate = tmpl;
this.editorContent = tmpl.content;
this.originalContent = tmpl.content;
},
copyToUser() {
if (!this.selectedTemplate) return;
const name = this.selectedTemplate.name;
const content = this.selectedTemplate.content;
// Check if user template with same name already exists
if (this.userTemplates.some(t => t.name === name)) {
this.showNotification(`User template "${name}" already exists. Delete it first or choose a different name.`, 'warning');
return;
}
this._pendingSelectUserTemplate = name;
this.saving = true;
this.getWebsocket().send(JSON.stringify({
type: 'config',
action: 'save_llm_template',
data: { name, content },
}));
},
saveTemplate() {
if (!this.selectedTemplate || !this.isDirty) return;
this.saving = true;
this.getWebsocket().send(JSON.stringify({
type: 'config',
action: 'save_llm_template',
data: {
name: this.selectedTemplate.name,
content: this.editorContent,
},
}));
},
confirmDelete() {
this.$refs.deletePrompt.initiateAction({});
},
deleteTemplate() {
if (!this.selectedTemplate) return;
this.getWebsocket().send(JSON.stringify({
type: 'config',
action: 'delete_llm_template',
data: { name: this.selectedTemplate.name },
}));
},
openNewDialog() {
this.newTemplateName = '';
this.showNewDialog = true;
},
createTemplate() {
if (!this.newFormValid || !this.newTemplateName) return;
let name = this.newTemplateName;
if (!name.endsWith('.jinja2')) {
name += '.jinja2';
}
this._pendingSelectUserTemplate = name;
this.getWebsocket().send(JSON.stringify({
type: 'config',
action: 'save_llm_template',
data: {
name,
content: '{#- GGUF/llama.cpp chat templates also work here (messages, bos_token, eos_token, add_generation_prompt, etc.) -#}\n{{ system_message }}\n\n{{ user_message }}\n\n{{ coercion_message }}\n',
},
}));
this.showNewDialog = false;
},
validateFileName(value) {
if (value == null) return true;
if (value.includes('/') || value.includes('\\')) {
return 'Name cannot contain directories';
}
if (/[<>:"|?*]/.test(value)) {
return 'Name contains invalid characters';
}
if (value.endsWith('.jinja2')) {
return 'Extension will be added automatically';
}
return true;
},
showNotification(message, color = 'success') {
this.toastMessage = message;
this.toastColor = color;
this.showToast = true;
},
handleMessage(data) {
if (data.type !== 'config') return;
switch (data.action) {
case 'llm_templates_list':
this.loading = false;
this.builtinTemplates = data.data.builtin || [];
this.userTemplates = data.data.user || [];
// Auto-select a user template if one was just created/copied
if (this._pendingSelectUserTemplate) {
const tmpl = this.userTemplates.find(t => t.name === this._pendingSelectUserTemplate);
if (tmpl) {
this.selectTemplate('user', tmpl);
}
this._pendingSelectUserTemplate = null;
}
break;
case 'save_llm_template_complete':
this.saving = false;
if (data.data.success) {
this.showNotification('Template saved successfully');
// Update the local content as saved
if (this.selectedSource === 'user' && this.selectedTemplate) {
this.originalContent = this.editorContent;
this.selectedTemplate.content = this.editorContent;
}
// Refresh the full list
this.requestTemplates();
} else {
this.showNotification(`Failed to save: ${data.data.error}`, 'error');
}
break;
case 'delete_llm_template_complete':
if (data.data.success) {
this.showNotification('Template deleted');
this.selectedTemplate = null;
this.selectedSource = null;
this.editorContent = '';
this.originalContent = '';
this.requestTemplates();
} else {
this.showNotification('Failed to delete template', 'error');
}
break;
}
},
},
mounted() {
this.registerMessageHandler(this.handleMessage);
this.requestTemplates();
},
unmounted() {
this.unregisterMessageHandler(this.handleMessage);
},
};
</script>
<style scoped>
.llm-templates-tab {
height: 100%;
}
.content-split {
height: calc(100vh - 385px);
min-height: 400px;
}
.list-panel {
border-right: 1px solid rgba(255, 255, 255, 0.1);
overflow-y: auto;
max-width: 350px;
min-width: 250px;
flex: 0 0 auto;
}
.template-item {
cursor: pointer;
}
.editor-panel {
overflow-y: auto;
}
.editor-container {
height: calc(100vh - 400px);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.editor-container .v-card-text {
flex: 1;
overflow: hidden;
}
.code-editor {
height: 100%;
font-size: 13px;
}
.code-editor :deep(.cm-editor) {
height: 100%;
}
.code-editor :deep(.cm-scroller) {
overflow: auto;
}
</style>

View File

@@ -26,6 +26,10 @@
Template Files
<v-icon v-if="outdatedCount > 0" size="x-small" color="warning" class="ml-1">mdi-alert</v-icon>
</v-tab>
<v-tab value="llm-templates">
<v-icon start>mdi-code-braces</v-icon>
LLM Prompt Templates
</v-tab>
<v-tab :disabled="!sceneLoaded" value="context-review">
<v-icon start>mdi-view-split-horizontal</v-icon>
Scene Context
@@ -158,6 +162,11 @@
</template>
</v-window-item>
<!-- LLM Prompt Templates Tab -->
<v-window-item value="llm-templates">
<LLMTemplatesTab />
</v-window-item>
<!-- Context Review Tab -->
<v-window-item value="context-review">
<SceneContextReviewInline
@@ -224,6 +233,7 @@
<script>
import ActiveTab from './ActiveTab.vue';
import GroupTab from './GroupTab.vue';
import LLMTemplatesTab from './LLMTemplatesTab.vue';
import PromptDetailView from './PromptDetailView.vue';
import SceneContextReviewInline from '../SceneContextReviewInline.vue';
@@ -232,6 +242,7 @@ export default {
components: {
ActiveTab,
GroupTab,
LLMTemplatesTab,
PromptDetailView,
SceneContextReviewInline,
},