fix(desktop): harden prompt window, fix pet skip, scale presets, attachments, textarea resize
- Stop pet window from skipping on click/drag release by anchoring resizes to the pet's current foot position and removing the post-drag resize. - Use screen coordinates and remove throttle for smooth scale-handle dragging. - Reintroduce abbreviated scale preset dropdown (XXXS-XXXL) alongside the slider. - Fix prompt-window attachment busy-lock, use visually-hidden file input, and support binary files as data URLs. - Tie prompt textarea height to the resized window; Enter sends, Ctrl+Enter inserts newline. - Harden prompt window: new chat opens message view, preload files unpacked from asar.
This commit is contained in:
parent
03ae162716
commit
41920b0f4b
7 changed files with 111 additions and 71 deletions
|
|
@ -26,6 +26,12 @@ extraResources:
|
|||
asar: true
|
||||
asarUnpack:
|
||||
- node_modules/**
|
||||
- control-center-preload.cjs
|
||||
- pet-preload.cjs
|
||||
- prompt-window-preload.cjs
|
||||
- plugin-sdk-preload.cjs
|
||||
- plugin-command-form-preload.cjs
|
||||
- panel-preload.cjs
|
||||
npmRebuild: false
|
||||
publish: null
|
||||
|
||||
|
|
|
|||
|
|
@ -129,10 +129,6 @@ const applyScalePreview = (scale) => {
|
|||
};
|
||||
|
||||
const sendScalePreview = (scale) => {
|
||||
if (scalePreviewTimer) return;
|
||||
scalePreviewTimer = setTimeout(() => {
|
||||
scalePreviewTimer = null;
|
||||
}, 120);
|
||||
ipcRenderer.send("openpets:pet-scale-preview", { scale });
|
||||
};
|
||||
|
||||
|
|
@ -446,7 +442,7 @@ const installMouseInterop = () => {
|
|||
}
|
||||
}
|
||||
if (scaling) {
|
||||
const newScale = scaleStartValue + (event.clientY - scaleStartY) * 0.003;
|
||||
const newScale = scaleStartValue + (event.screenY - scaleStartY) * 0.003;
|
||||
const clamped = applyScalePreview(newScale);
|
||||
sendScalePreview(clamped);
|
||||
}
|
||||
|
|
@ -461,7 +457,7 @@ const installMouseInterop = () => {
|
|||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
scaling = true;
|
||||
scaleStartY = event.clientY;
|
||||
scaleStartY = event.screenY;
|
||||
scaleStartValue = getCurrentSpriteScale();
|
||||
setInteractiveHit(true);
|
||||
ipcRenderer.send("openpets:pet-scale-start");
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ export interface OnboardingPreferenceLike {
|
|||
}
|
||||
|
||||
export const petScaleOptions = [
|
||||
{ label: "XXXS", value: 0.16 },
|
||||
{ label: "XXS", value: 0.24 },
|
||||
{ label: "XS", value: 0.32 },
|
||||
{ label: "Small", value: 0.44 },
|
||||
{ label: "Medium", value: 0.56 },
|
||||
{ label: "Large", value: 0.72 },
|
||||
{ label: "S", value: 0.44 },
|
||||
{ label: "M", value: 0.56 },
|
||||
{ label: "L", value: 0.72 },
|
||||
{ label: "XL", value: 0.88 },
|
||||
{ label: "XXL", value: 1.04 },
|
||||
{ label: "XXXL", value: 1.20 },
|
||||
|
|
|
|||
|
|
@ -433,10 +433,7 @@ function installMousePassthroughAndDrag(window: BrowserWindow, hooks: PetWindowI
|
|||
dragging = null;
|
||||
petWindowDragging.set(window, false);
|
||||
debug("pet.window", "drag end", { windowId, position: window.isDestroyed() ? null : readWindowPosition(window) });
|
||||
if (!window.isDestroyed()) {
|
||||
const scale = getAppStateSnapshot().preferences.petScale as PetScaleValue;
|
||||
resizePetWindowForDisplay(window, scale, petWindowLastDisplay.get(window) ?? null);
|
||||
}
|
||||
// Do not resize/re-anchor after a drag — the user already positioned the window.
|
||||
if (wasDragging) onPetEvent?.("pet:dragEnd", {});
|
||||
};
|
||||
|
||||
|
|
@ -1469,33 +1466,41 @@ function sendBubbleLayout(window: BrowserWindow, layout: PetWindowBubbleLayout):
|
|||
});
|
||||
}
|
||||
|
||||
function resizePetWindowForDisplay(window: BrowserWindow, scale: PetScaleValue, display: PetTransientDisplay | null): void {
|
||||
if (window.isDestroyed()) return;
|
||||
|
||||
const nextSize = getPetWindowSize(window, scale, display);
|
||||
const anchor = readWindowPosition(window);
|
||||
const nextY = nextSize.bubbleBelow
|
||||
? anchor.y
|
||||
: anchor.y - Math.max(0, nextSize.height - defaultPetWindowSize.height);
|
||||
const nextBounds = {
|
||||
x: anchor.x - Math.round((nextSize.width - defaultPetWindowSize.width) / 2),
|
||||
y: nextY,
|
||||
width: nextSize.width,
|
||||
height: nextSize.height,
|
||||
};
|
||||
function computePetWindowBounds(window: BrowserWindow, nextSize: { readonly width: number; readonly height: number }): Electron.Rectangle | null {
|
||||
if (window.isDestroyed()) return null;
|
||||
const currentBounds = window.getBounds();
|
||||
const petBottom = 22;
|
||||
const petCenterX = currentBounds.x + Math.round(currentBounds.width / 2);
|
||||
const petFootY = currentBounds.y + currentBounds.height - petBottom;
|
||||
const nextX = petCenterX - Math.round(nextSize.width / 2);
|
||||
const nextY = petFootY - (nextSize.height - petBottom);
|
||||
const displayForBounds = screen.getDisplayNearestPoint({
|
||||
x: anchor.x + Math.round(defaultPetWindowSize.width / 2),
|
||||
y: anchor.y + defaultPetWindowSize.height,
|
||||
x: petCenterX,
|
||||
y: currentBounds.y + currentBounds.height,
|
||||
}) ?? screen.getPrimaryDisplay();
|
||||
const workArea = displayForBounds.workArea;
|
||||
const maxX = workArea.x + Math.max(0, workArea.width - nextSize.width);
|
||||
const maxY = workArea.y + Math.max(0, workArea.height - nextSize.height);
|
||||
window.setBounds({
|
||||
x: Math.min(Math.max(nextBounds.x, workArea.x), maxX),
|
||||
y: Math.min(Math.max(nextBounds.y, workArea.y), maxY),
|
||||
return {
|
||||
x: clampNumber(nextX, workArea.x, maxX),
|
||||
y: clampNumber(nextY, workArea.y, maxY),
|
||||
width: nextSize.width,
|
||||
height: nextSize.height,
|
||||
}, false);
|
||||
};
|
||||
}
|
||||
|
||||
function resizePetWindowForDisplay(window: BrowserWindow, scale: PetScaleValue, display: PetTransientDisplay | null): void {
|
||||
if (window.isDestroyed()) return;
|
||||
|
||||
const nextSize = getPetWindowSize(window, scale, display);
|
||||
const currentBounds = window.getBounds();
|
||||
if (currentBounds.width === nextSize.width && currentBounds.height === nextSize.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextBounds = computePetWindowBounds(window, nextSize);
|
||||
if (!nextBounds) return;
|
||||
window.setBounds(nextBounds, false);
|
||||
sendBubbleLayout(window, nextSize);
|
||||
}
|
||||
|
||||
|
|
@ -1506,26 +1511,9 @@ function resizePetWindowForScale(window: BrowserWindow, scale: PetScaleValue): v
|
|||
if (currentBounds.width === nextSize.width && currentBounds.height === nextSize.height) {
|
||||
return;
|
||||
}
|
||||
const anchor = readWindowPosition(window);
|
||||
const nextBounds = {
|
||||
x: anchor.x - Math.round((nextSize.width - defaultPetWindowSize.width) / 2),
|
||||
y: anchor.y - Math.max(0, nextSize.height - defaultPetWindowSize.height),
|
||||
width: nextSize.width,
|
||||
height: nextSize.height,
|
||||
};
|
||||
const displayForBounds = screen.getDisplayNearestPoint({
|
||||
x: anchor.x + Math.round(defaultPetWindowSize.width / 2),
|
||||
y: anchor.y + defaultPetWindowSize.height,
|
||||
}) ?? screen.getPrimaryDisplay();
|
||||
const workArea = displayForBounds.workArea;
|
||||
const maxX = workArea.x + Math.max(0, workArea.width - nextSize.width);
|
||||
const maxY = workArea.y + Math.max(0, workArea.height - nextSize.height);
|
||||
window.setBounds({
|
||||
x: Math.min(Math.max(nextBounds.x, workArea.x), maxX),
|
||||
y: Math.min(Math.max(nextBounds.y, workArea.y), maxY),
|
||||
width: nextSize.width,
|
||||
height: nextSize.height,
|
||||
}, false);
|
||||
const nextBounds = computePetWindowBounds(window, nextSize);
|
||||
if (!nextBounds) return;
|
||||
window.setBounds(nextBounds, false);
|
||||
}
|
||||
|
||||
interface PetWindowBubbleLayout {
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ function buildPromptWindowUrl(): string {
|
|||
.prompt-shell{display:flex;align-items:stretch;min-width:0;border:1px solid var(--border-strong);border-radius:14px;background:var(--surface-bg);overflow:hidden}
|
||||
.prompt-shell:focus-within{border-color:#73a6ff;box-shadow:0 0 0 3px rgba(59,130,246,.14)}
|
||||
.input-wrap{flex:1 1 auto;min-width:0;display:flex;flex-direction:column}
|
||||
.prompt-input{width:100%;box-sizing:border-box;border:none;background:transparent;color:var(--text-primary);outline:none;resize:none;padding:10px 13px 5px;min-height:48px;max-height:96px;font:500 12px/1.5 inherit;overflow:auto;flex:1 1 auto}
|
||||
.prompt-input{width:100%;box-sizing:border-box;border:none;background:transparent;color:var(--text-primary);outline:none;resize:none;padding:10px 13px 5px;min-height:48px;font:500 12px/1.5 inherit;overflow:auto;flex:1 1 auto}
|
||||
.shell.is-compact .prompt-input{min-height:36px}
|
||||
.send-button{width:42px;min-width:42px;align-self:stretch;border:none;border-left:1px solid var(--button-border);border-radius:0;padding:0;background:var(--primary-bg);color:var(--primary-text);box-shadow:none}
|
||||
.send-button:hover:not(:disabled){transform:translateY(-1px)}
|
||||
|
|
@ -353,8 +353,9 @@ function buildPromptWindowUrl(): string {
|
|||
.attach-button:hover:not(:disabled){background:var(--button-hover);color:var(--text-primary)}
|
||||
.attach-button:disabled{opacity:.55;cursor:default}
|
||||
.attach-button svg{width:15px;height:15px}
|
||||
#fileInput{position:absolute;opacity:0;width:0;height:0;pointer-events:none}
|
||||
@media (max-width:460px){.shell{padding:7px 7px 5px}.history-list{min-height:72px}.conversations-list{min-height:72px}.prompt-input{min-height:46px}.send-button{width:38px;min-width:38px}}
|
||||
</style></head><body><div class="shell" id="shell"><div class="editor-shell" id="editorShell" hidden><div id="conversations" class="conversations-list" hidden aria-label="Conversations"></div><div id="history" class="history-list" aria-label="Conversation history"></div></div><div class="toolbar"><div class="toolbar-actions"><button class="icon-button" id="editor" type="button" aria-label="Toggle editor" title="Editor"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M8 9h8"/><path d="M8 13h5"/></svg></button><button class="icon-button" id="newChat" type="button" aria-label="Start new chat" title="New chat"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button><button class="icon-button" id="historyButton" type="button" aria-label="Open history" title="History"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/><path d="M12 7v6l4 2"/></svg></button><button class="icon-button" id="settings" type="button" aria-label="Open settings" title="Settings"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1 1 0 0 0 .2 1.1l.1.1a1.2 1.2 0 0 1 0 1.7l-1.2 1.2a1.2 1.2 0 0 1-1.7 0l-.1-.1a1 1 0 0 0-1.1-.2 1 1 0 0 0-.6.9V20a1.2 1.2 0 0 1-1.2 1.2h-1.7A1.2 1.2 0 0 1 10.9 20v-.1a1 1 0 0 0-.6-.9 1 1 0 0 0-1.1.2l-.1.1a1.2 1.2 0 0 1-1.7 0l-1.2-1.2a1.2 1.2 0 0 1 0-1.7l.1-.1a1 1 0 0 0 .2-1.1 1 1 0 0 0-.9-.6H4A1.2 1.2 0 0 1 2.8 13v-2A1.2 1.2 0 0 1 4 9.8h.1a1 1 0 0 0 .9-.6 1 1 0 0 0-.2-1.1l-.1-.1a1.2 1.2 0 0 1 0-1.7l1.2-1.2a1.2 1.2 0 0 1 1.7 0l.1.1a1 1 0 0 0 1.1.2 1 1 0 0 0 .6-.9V4A1.2 1.2 0 0 1 10.6 2.8h1.7A1.2 1.2 0 0 1 13.5 4v.1a1 1 0 0 0 .6.9 1 1 0 0 0 1.1-.2l.1-.1a1.2 1.2 0 0 1 1.7 0l1.2 1.2a1.2 1.2 0 0 1 0 1.7l-.1.1a1 1 0 0 0-.2 1.1 1 1 0 0 0 .9.6H20a1.2 1.2 0 0 1 1.2 1.2v2A1.2 1.2 0 0 1 20 14.2h-.1a1 1 0 0 0-.9.8z"/></svg></button><button class="icon-button" id="close" type="button" aria-label="Close chat window" title="Close"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M6 6l12 12"/><path d="M18 6L6 18"/></svg></button></div></div><div class="composer"><input type="file" id="fileInput" multiple style="display:none"><div class="attachments" id="attachments" hidden></div><div class="prompt-shell" id="promptShell"><button class="attach-button" id="attach" type="button" aria-label="Attach file" title="Attach file"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg></button><div class="input-wrap"><textarea id="prompt" class="prompt-input" placeholder="Ask anything." aria-label="Prompt input"></textarea><div class="feedback" id="feedback" aria-live="polite"></div></div><button class="send-button" id="send" type="button" aria-label="Send prompt" title="Send"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h12"/><path d="M13 6l6 6-6 6"/></svg></button></div></div><div class="resize-grip" id="resizeGrip" aria-hidden="true"></div></div></div><script>
|
||||
</style></head><body><div class="shell" id="shell"><div class="editor-shell" id="editorShell" hidden><div id="conversations" class="conversations-list" hidden aria-label="Conversations"></div><div id="history" class="history-list" aria-label="Conversation history"></div></div><div class="toolbar"><div class="toolbar-actions"><button class="icon-button" id="editor" type="button" aria-label="Toggle editor" title="Editor"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M8 9h8"/><path d="M8 13h5"/></svg></button><button class="icon-button" id="newChat" type="button" aria-label="Start new chat" title="New chat"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button><button class="icon-button" id="historyButton" type="button" aria-label="Open history" title="History"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/><path d="M12 7v6l4 2"/></svg></button><button class="icon-button" id="settings" type="button" aria-label="Open settings" title="Settings"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1 1 0 0 0 .2 1.1l.1.1a1.2 1.2 0 0 1 0 1.7l-1.2 1.2a1.2 1.2 0 0 1-1.7 0l-.1-.1a1 1 0 0 0-1.1-.2 1 1 0 0 0-.6.9V20a1.2 1.2 0 0 1-1.2 1.2h-1.7A1.2 1.2 0 0 1 10.9 20v-.1a1 1 0 0 0-.6-.9 1 1 0 0 0-1.1.2l-.1.1a1.2 1.2 0 0 1-1.7 0l-1.2-1.2a1.2 1.2 0 0 1 0-1.7l.1-.1a1 1 0 0 0 .2-1.1 1 1 0 0 0-.9-.6H4A1.2 1.2 0 0 1 2.8 13v-2A1.2 1.2 0 0 1 4 9.8h.1a1 1 0 0 0 .9-.6 1 1 0 0 0-.2-1.1l-.1-.1a1.2 1.2 0 0 1 0-1.7l1.2-1.2a1.2 1.2 0 0 1 1.7 0l.1.1a1 1 0 0 0 1.1.2 1 1 0 0 0 .6-.9V4A1.2 1.2 0 0 1 10.6 2.8h1.7A1.2 1.2 0 0 1 13.5 4v.1a1 1 0 0 0 .6.9 1 1 0 0 0 1.1-.2l.1-.1a1.2 1.2 0 0 1 1.7 0l1.2 1.2a1.2 1.2 0 0 1 0 1.7l-.1.1a1 1 0 0 0-.2 1.1 1 1 0 0 0 .9.6H20a1.2 1.2 0 0 1 1.2 1.2v2A1.2 1.2 0 0 1 20 14.2h-.1a1 1 0 0 0-.9.8z"/></svg></button><button class="icon-button" id="close" type="button" aria-label="Close chat window" title="Close"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M6 6l12 12"/><path d="M18 6L6 18"/></svg></button></div></div><div class="composer"><input type="file" id="fileInput" multiple><div class="attachments" id="attachments" hidden></div><div class="prompt-shell" id="promptShell"><button class="attach-button" id="attach" type="button" aria-label="Attach file" title="Attach file"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg></button><div class="input-wrap"><textarea id="prompt" class="prompt-input" placeholder="Ask anything." aria-label="Prompt input"></textarea><div class="feedback" id="feedback" aria-live="polite"></div></div><button class="send-button" id="send" type="button" aria-label="Send prompt" title="Send"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h12"/><path d="M13 6l6 6-6 6"/></svg></button></div></div><div class="resize-grip" id="resizeGrip" aria-hidden="true"></div></div></div><script>
|
||||
const shellEl = document.getElementById('shell');
|
||||
const api = window.openPetsPromptWindow;
|
||||
const editorShellEl = document.getElementById('editorShell');
|
||||
|
|
@ -536,11 +537,27 @@ function buildPromptWindowUrl(): string {
|
|||
setBusy(busy);
|
||||
};
|
||||
|
||||
const isTextLikeFile = (file) => {
|
||||
if (typeof file.type === 'string' && file.type.startsWith('text/')) return true;
|
||||
const textExtensions = new Set(['txt','md','markdown','json','js','ts','jsx','tsx','mjs','cjs','html','htm','css','scss','sass','xml','yaml','yml','csv','log','ini','conf','sh','bash','zsh','ps1','bat','cmd','py','rb','go','rs','java','kt','swift','cpp','c','h','hpp','cs','php','sql','lua','pl','r']);
|
||||
const ext = String(file.name).split('.').pop().toLowerCase();
|
||||
return textExtensions.has(ext);
|
||||
};
|
||||
|
||||
const readAttachmentFile = (file) => new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve({ name: String(file.name), size: file.size, text: String(reader.result || '').slice(0, maxAttachmentChars) });
|
||||
reader.onerror = () => resolve({ name: String(file.name), size: file.size, text: '' });
|
||||
reader.readAsText(file);
|
||||
const asText = isTextLikeFile(file);
|
||||
reader.onload = () => {
|
||||
const raw = reader.result || '';
|
||||
const text = asText ? String(raw).slice(0, maxAttachmentChars) : String(raw).slice(0, maxAttachmentChars);
|
||||
resolve({ name: String(file.name), size: file.size, text, isText: asText });
|
||||
};
|
||||
reader.onerror = () => resolve({ name: String(file.name), size: file.size, text: '', isText: asText });
|
||||
if (asText) {
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
const queueWindowResize = (width, height, anchor) => {
|
||||
|
|
@ -618,12 +635,22 @@ function buildPromptWindowUrl(): string {
|
|||
};
|
||||
|
||||
const adjustPromptHeight = () => {
|
||||
const promptShell = document.getElementById('promptShell');
|
||||
const inputWrap = promptEl && promptEl.parentElement;
|
||||
if (!promptShell || !inputWrap) return;
|
||||
const available = Math.max(36, promptShell.clientHeight - (feedbackEl && feedbackEl.offsetHeight ? feedbackEl.offsetHeight + 4 : 0));
|
||||
promptEl.style.maxHeight = String(available) + 'px';
|
||||
promptEl.style.height = 'auto';
|
||||
const nextHeight = Math.min(Math.max(promptEl.scrollHeight, 60), 136);
|
||||
const minHeight = shellEl && shellEl.classList.contains('is-compact') ? 36 : 48;
|
||||
const nextHeight = Math.min(Math.max(promptEl.scrollHeight, minHeight), available);
|
||||
promptEl.style.height = String(nextHeight) + 'px';
|
||||
fitWindowToContent();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
adjustPromptHeight();
|
||||
});
|
||||
|
||||
const renderState = () => {
|
||||
applyThemeMode(state && state.themeMode ? state.themeMode : 'system');
|
||||
if (editorVisible) {
|
||||
|
|
@ -648,10 +675,14 @@ function buildPromptWindowUrl(): string {
|
|||
};
|
||||
|
||||
const submitPrompt = async () => {
|
||||
if (busy || !state || !state.hasCredential || (!promptEl.value.trim() && attachments.length === 0)) return;
|
||||
if (busy || !api || !state || !state.hasCredential || (!promptEl.value.trim() && attachments.length === 0)) return;
|
||||
let promptText = promptEl.value.trim();
|
||||
if (attachments.length > 0) {
|
||||
const blocks = attachments.map((file) => '[Attached file: ' + file.name + ' (' + file.size + ' bytes)]\n' + file.text);
|
||||
const blocks = attachments.map((file) => {
|
||||
const header = '[Attached file: ' + file.name + ' (' + file.size + ' bytes)]';
|
||||
const body = file.isText === false ? 'data-url:' + file.text : file.text;
|
||||
return header + '\n' + body;
|
||||
});
|
||||
const attachmentBlock = blocks.join('\n\n---\n\n');
|
||||
promptText = promptText ? promptText + '\n\n---\n\n' + attachmentBlock : attachmentBlock;
|
||||
}
|
||||
|
|
@ -660,6 +691,7 @@ function buildPromptWindowUrl(): string {
|
|||
state = await api.submitPrompt(promptText);
|
||||
promptEl.value = '';
|
||||
attachments = [];
|
||||
setBusy(false);
|
||||
renderAttachments();
|
||||
setFeedback('', '', '');
|
||||
renderState();
|
||||
|
|
@ -695,6 +727,7 @@ function buildPromptWindowUrl(): string {
|
|||
if (editorVisible && editorView === 'conversations') {
|
||||
renderConversations();
|
||||
}
|
||||
setEditorVisible(true, { view: 'messages' });
|
||||
} catch (error) {
|
||||
setFeedback(normalizeErrorMessage(error), 'error', 'request');
|
||||
setBusy(false);
|
||||
|
|
@ -736,10 +769,12 @@ function buildPromptWindowUrl(): string {
|
|||
Promise.all(files.map(readAttachmentFile)).then((next) => {
|
||||
attachments.push(...next);
|
||||
fileInput.value = '';
|
||||
setBusy(false);
|
||||
renderAttachments();
|
||||
}).catch(() => {
|
||||
fileInput.value = '';
|
||||
setBusy(false);
|
||||
renderAttachments();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1367,18 +1367,32 @@ function SettingsView({ onThemeModeChange }: { onThemeModeChange: (mode: ThemeMo
|
|||
<strong>{t("settings.general.petScale.title")}</strong>
|
||||
<small>{t("settings.general.petScale.description")}</small>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 min-w-[180px]">
|
||||
<input
|
||||
className="settings-slider"
|
||||
type="range"
|
||||
min={0.16}
|
||||
max={10}
|
||||
step={0.01}
|
||||
<div className="flex flex-col items-stretch gap-2 min-w-[180px]">
|
||||
<select
|
||||
className="settings-select"
|
||||
value={settings?.preferences.petScale ?? 0.56}
|
||||
disabled={!settings || !!busy}
|
||||
onChange={(event) => patchPreferences({ petScale: Number(event.target.value) }, t("settings.toast.petScaleSaved"))}
|
||||
/>
|
||||
<span className="text-sm font-semibold text-navy w-16 text-right tabular-nums">{(settings?.preferences.petScale ?? 0.56).toFixed(2)}x</span>
|
||||
>
|
||||
{(settings?.petScaleOptions ?? []).map((option) => (
|
||||
<option key={option.value} value={option.value} title={`Pet scale: ${option.label}`}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
className="settings-slider"
|
||||
type="range"
|
||||
min={0.16}
|
||||
max={10}
|
||||
step={0.01}
|
||||
value={settings?.preferences.petScale ?? 0.56}
|
||||
disabled={!settings || !!busy}
|
||||
onChange={(event) => patchPreferences({ petScale: Number(event.target.value) }, t("settings.toast.petScaleSaved"))}
|
||||
/>
|
||||
<span className="text-sm font-semibold text-navy w-16 text-right tabular-nums">{(settings?.preferences.petScale ?? 0.56).toFixed(2)}x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-row">
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ assert.equal(preferencePatch.openDefaultPetOnLaunch, true);
|
|||
assert.equal(preferencePatch.speechBubblesEnabled, true);
|
||||
|
||||
assert.equal(defaultPetScale, 0.56);
|
||||
assert.deepEqual(petScaleOptions.map((option) => option.value), [0.24, 0.32, 0.44, 0.56, 0.72, 0.88, 1.04, 1.2]);
|
||||
assert.deepEqual(petScaleOptions.map((option) => option.value), [0.16, 0.24, 0.32, 0.44, 0.56, 0.72, 0.88, 1.04, 1.2]);
|
||||
assert.equal(normalizePetScale(0.24), 0.24);
|
||||
assert.equal(normalizePetScale(0.56), 0.56);
|
||||
assert.equal(normalizePetScale(1.2), 1.2);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue