diff --git a/components/studio2/Bench.vue b/components/studio2/Bench.vue
index e9566db..f11742f 100644
--- a/components/studio2/Bench.vue
+++ b/components/studio2/Bench.vue
@@ -124,12 +124,16 @@
ref="promptArea"
v-model="form.promptSections.action"
class="field-area"
- placeholder="Describe the image or the shot. Blank line between prompts for a batch."
+ :placeholder="batchMode ? 'Separate prompts with two blank lines.' : 'Describe the image or the shot.'"
@select="capturePromptCaret"
@click="capturePromptCaret"
@keyup="capturePromptCaret"
/>
-
Enhance prompt
@@ -353,6 +357,7 @@
:selected="selected"
:folder-id="form.folderId"
:preview-hidden="!eyeOpen"
+ :is-hidden="isHidden"
@close="libraryOpen=false"
@select="selectOutput"
@use-image="onLibraryUseImage"
@@ -407,7 +412,7 @@
-
+ {{ previewHidden ? 'Hidden' : '' }}
{{ entry.kind === 'clip' ? 'Video' : entry.kind === 'track' ? 'Music' : 'Image' }}
diff --git a/components/studio2/Mobile.vue b/components/studio2/Mobile.vue
index 7cece3b..0ae9289 100644
--- a/components/studio2/Mobile.vue
+++ b/components/studio2/Mobile.vue
@@ -139,6 +139,7 @@
:selected="ctx.selected"
:folder-id="ctx.form?.folderId"
:preview-hidden="!ctx.eyeOpen"
+ :is-hidden="ctx.isHidden"
@close="ctx.closeLibrary()"
@select="ctx.selectOutput"
@use-image="ctx.onLibraryUseImage"
@@ -223,7 +224,17 @@
Lyrics
- Prompt
+ Prompt
+
+
Enhance prompt
Turbo
6-step Viggle distill. CFG stays 1. Enhance prompt recommended.
@@ -355,6 +366,25 @@
Clear finished
+
+
+
+
+
@@ -364,12 +394,33 @@ export type Studio2MobileCtx = Record
const props = defineProps<{ ctx: Studio2MobileCtx }>()
const sheet = ref('')
const touchStart = ref<{ x: number; y: number } | null>(null)
+const promptEditorOpen = ref(false)
+const promptEditorDraft = ref('')
+const promptEditorEl = ref(null)
+
+function openPromptEditor() {
+ promptEditorDraft.value = String(props.ctx.form?.promptSections?.action || '')
+ promptEditorOpen.value = true
+ nextTick(() => {
+ promptEditorEl.value?.focus()
+ const el = promptEditorEl.value
+ if (el) {
+ const n = el.value.length
+ el.setSelectionRange(n, n)
+ }
+ })
+}
+function closePromptEditor() {
+ if (props.ctx.form?.promptSections) props.ctx.form.promptSections.action = promptEditorDraft.value
+ promptEditorOpen.value = false
+}
function toggleSheet(id: string) {
sheet.value = sheet.value === id ? '' : id
}
function closeMenus() {
sheet.value = ''
+ if (promptEditorOpen.value) closePromptEditor()
}
function runAction(kind: 'use-image' | 'use-prompt' | 'copy' | 'copy-raw' | 'load-input' | 'load-input-raw' | 'delete') {
sheet.value = ''
@@ -726,4 +777,41 @@ watch(() => props.ctx.form.mode, () => { sheet.value = '' })
.m-job-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.m-job-progress-bar { flex: 1; height: 4px; accent-color: var(--amber); }
.m-link { border: 0; background: transparent; color: var(--amber); text-align: left; }
+.m-prompt-editor {
+ position: fixed;
+ inset: 0;
+ z-index: 90;
+ display: flex;
+ flex-direction: column;
+ background: #0e0e10;
+ padding: env(safe-area-inset-top, 0) env(safe-area-inset-right, 0) env(safe-area-inset-bottom, 0) env(safe-area-inset-left, 0);
+}
+.m-prompt-editor-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--line);
+}
+.m-prompt-editor-head button {
+ border: 0;
+ background: transparent;
+ color: var(--amber);
+ font-size: 16px;
+ font-weight: 600;
+}
+.m-prompt-editor-area {
+ flex: 1;
+ width: 100%;
+ margin: 0;
+ padding: 16px;
+ border: 0;
+ resize: none;
+ background: #0e0e10;
+ color: #f3f1ea;
+ font: inherit;
+ font-size: 17px;
+ line-height: 1.45;
+ outline: none;
+}
diff --git a/shared/studio2/styles.mjs b/shared/studio2/styles.mjs
index 1135c30..958491e 100644
--- a/shared/studio2/styles.mjs
+++ b/shared/studio2/styles.mjs
@@ -9,5 +9,13 @@ export function normalizeStyles(value={}) {
}
export function toggleStyle(value,chip,negative=false) { const next=normalizeStyles(value),key=negative?'negative':'positive',other=negative?'positive':'negative';next[other]=next[other].filter(s=>s!==chip);next[key]=next[key].includes(chip)?next[key].filter(s=>s!==chip):[...next[key],chip];return next }
export function stylePrompt(value,negative=false){return normalizeStyles(value)[negative?'negative':'positive'].join(', ')}
+/** Single blank line (mobile / video / edit batchText). */
export function parseBatch(value){return String(value||'').split(/\r?\n\s*\r?\n/).map(s=>s.trim()).filter(Boolean)}
+/**
+ * Desktop image Batch toggle: split only on a double blank line (two empty lines → \n\n\n+).
+ * A single newline or a single blank line stays inside the same prompt.
+ */
+export function parseBatchDoubleBlank(value){
+ return String(value||'').split(/\r?\n(?:[ \t]*\r?\n){2,}/).map(s=>s.trim()).filter(Boolean)
+}
diff --git a/shared/studio2/viewer.mjs b/shared/studio2/viewer.mjs
index 80a6325..dc3edad 100644
--- a/shared/studio2/viewer.mjs
+++ b/shared/studio2/viewer.mjs
@@ -6,7 +6,10 @@ export function folderAssets(library, jobs, folderId) {
export function newestOutput(jobs, seen, folderId) {
return jobs.filter(j=>j.request.folderId===folderId).flatMap(j=>j.outputs || []).filter(a=>!seen.has(a.id)).sort((a,b)=>(a.studio2?.savedAt || 0)-(b.studio2?.savedAt || 0)).at(-1) || null
}
-export function reconcileSelection(selected, assets) { return assets.find(a=>a.id===selected?.id) || assets.at(-1) || null }
+export function reconcileSelection(selected, assets) {
+ if (!selected?.id) return assets.at(-1) || null
+ return assets.find(a => a.id === selected.id) || selected
+}
export function activeJobs(jobs) { return jobs.filter(j=>['waking','enhancing','submitting','rendering','saving'].includes(j.state)) }
export function previewHidden(asset, revealed, xaigen) {
if (!xaigen || !asset?.id) return false
diff --git a/tests/studio2-bench.test.mjs b/tests/studio2-bench.test.mjs
index f173da0..5a19964 100644
--- a/tests/studio2-bench.test.mjs
+++ b/tests/studio2-bench.test.mjs
@@ -4,7 +4,7 @@ import {readFileSync} from 'node:fs'
import ts from 'typescript'
import {folderAssets,reconcileSelection,activeJobs,previewHidden} from '../shared/studio2/viewer.mjs'
import {useAsHero,useAsSource,restoreAll,hydrateStudio2Job} from '../shared/studio2/input.mjs'
-import {toggleStyle,parseBatch,stylePrompt} from '../shared/studio2/styles.mjs'
+import {toggleStyle,parseBatch,parseBatchDoubleBlank,stylePrompt} from '../shared/studio2/styles.mjs'
import {compilePrompt,validateRequest} from '../shared/studio2/contracts.mjs'
import {cachedLoras} from '../shared/studio2/lora-cache.mjs'
import {resolveQwen21Size} from '../shared/studio2/qwen21-size.mjs'
@@ -79,6 +79,12 @@ test('image chips compile positive and negative independently; video chips do no
assert.match(q.compiledPrompt,/Photorealistic/);assert.doesNotMatch(q.compiledPrompt,/Comic|pores|tripod|hold|VIDEO ONLY/i);assert.deepEqual(q.imageStyles,styles)
assert.doesNotMatch(compilePrompt(q.promptSections,false,q),/Picture 1/)
assert.deepEqual(parseBatch('A wrapped\nsentence\n\nSecond shot\r\n\r\nThird'),['A wrapped\nsentence','Second shot','Third'])
+ assert.deepEqual(
+ parseBatchDoubleBlank('Prompt 1 starts here\nBal blaglas\nsldkjf lkaj\n\nsldkfjs lkdfj\nsdlfkjsdkljf\n\n\nPrompt 2 starts here flaskjflaskdj'),
+ ['Prompt 1 starts here\nBal blaglas\nsldkjf lkaj\n\nsldkfjs lkdfj\nsdlfkjsdkljf','Prompt 2 starts here flaskjflaskdj']
+ )
+ assert.deepEqual(parseBatchDoubleBlank('one\n\ntwo'),['one\n\ntwo'])
+ assert.deepEqual(parseBatchDoubleBlank('one\n\n\ntwo\n\n\n\nthree\n'),['one','two','three'])
})
test('qwen21 supports Generate and Edit with 25/1 defaults and no locks',()=>{
const q=validateRequest({mode:'generate',engine:'qwen21',folderId:'f',promptSections:{action:'a red bicycle'},settings:{aspect:'1:1'}})