mirror of
https://github.com/lossless-group/perplexed-plugin.git
synced 2026-07-22 06:49:50 +00:00
improve(modals): apply wide-modal pattern to LM Studio + Text Enhancement modals
Brings LMStudioModal, TextEnhancementModal, and TextEnhancementWithImagesModal to the same shape as Ask Perplexity / Ask Claude / Ask Perplexica: modalEl.addClass(...) wide-modal unlock, sectioned header/section/footer layout, BEM-scoped class names, native Obsidian Setting components for dropdowns and toggles, custom textareas where Setting is a poor fit, Cmd/Ctrl+Enter submit, mobile breakpoint at 600px. LMStudioModal: model dropdown with live taglines, Max Tokens input, Temperature slider (0-2 with dynamic tooltip), optional System Prompt textarea, Images + Stream toggles. TextEnhancementModal + TextEnhancementWithImagesModal: read-only Selected Text textarea (background-secondary tint to distinguish from editable input) + editable prompt pre-filled from the plugin template, button busy-state during the call. Bonus: TextEnhancementWithImagesModal CSS class name now matches the file/class name (was 'get-related-images-modal' on the .ts side while the file was text-enhancement-with-images-modal.css). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19b300c10c
commit
ebebfa9f21
7 changed files with 881 additions and 1188 deletions
|
|
@ -1,162 +1,217 @@
|
|||
import type { App, Editor } from 'obsidian';
|
||||
import { Modal, Notice } from 'obsidian';
|
||||
import { Modal, Notice, Setting } from 'obsidian';
|
||||
import type { LMStudioService, LMStudioOptions } from '../services/lmStudioService';
|
||||
import type { PromptsService } from '../services/promptsService';
|
||||
|
||||
const LMSTUDIO_MODELS: Array<{ value: string; label: string; tagline: string }> = [
|
||||
{ value: 'ibm/granite-3.2-8b', label: 'IBM Granite 3.2 8B', tagline: 'IBM Granite — strong on code and structured reasoning.' },
|
||||
{ value: 'microsoft/phi-4-reasoning-plus', label: 'Phi-4 Reasoning Plus', tagline: 'Microsoft Phi-4 — small, fast model tuned for chain-of-thought reasoning.' },
|
||||
{ value: 'google/gemma-3-12b', label: 'Gemma 3 12B', tagline: 'Google Gemma 3 — solid all-rounder open model.' },
|
||||
{ value: 'meta-llama/llama-3.2-3b-instruct', label: 'Llama 3.2 3B Instruct', tagline: 'Meta Llama 3.2 — small instruction-tuned model, fastest of the bundled choices.' },
|
||||
{ value: 'custom-model', label: 'custom-model', tagline: 'Generic placeholder — LM Studio will route this to whichever model is currently loaded.' },
|
||||
];
|
||||
|
||||
const DEFAULT_MODEL = 'ibm/granite-3.2-8b';
|
||||
const DEFAULT_MAX_TOKENS = 2048;
|
||||
const DEFAULT_TEMPERATURE = 0.7;
|
||||
|
||||
export class LMStudioModal extends Modal {
|
||||
private editor: Editor;
|
||||
private lmStudioService: LMStudioService;
|
||||
private promptsService: PromptsService;
|
||||
private queryInput!: HTMLTextAreaElement;
|
||||
private modelSelect!: HTMLSelectElement;
|
||||
private streamToggle!: HTMLInputElement;
|
||||
private maxTokensInput!: HTMLInputElement;
|
||||
private temperatureInput!: HTMLInputElement;
|
||||
private systemPromptInput!: HTMLTextAreaElement;
|
||||
private imagesToggle!: HTMLInputElement;
|
||||
|
||||
constructor(app: App, editor: Editor, lmStudioService: LMStudioService, promptsService: PromptsService) {
|
||||
private query = '';
|
||||
private model: string = DEFAULT_MODEL;
|
||||
private systemPrompt = '';
|
||||
private maxTokens: number = DEFAULT_MAX_TOKENS;
|
||||
private temperature: number = DEFAULT_TEMPERATURE;
|
||||
private images = false;
|
||||
private stream = true;
|
||||
|
||||
private modelDescEl: HTMLElement | null = null;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
editor: Editor,
|
||||
lmStudioService: LMStudioService,
|
||||
promptsService: PromptsService
|
||||
) {
|
||||
super(app);
|
||||
this.editor = editor;
|
||||
this.lmStudioService = lmStudioService;
|
||||
this.promptsService = promptsService;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.addClass('lmstudio-modal');
|
||||
contentEl.createEl('h2', {text: 'Ask LM Studio'});
|
||||
|
||||
const form = contentEl.createEl('form');
|
||||
|
||||
// Query input
|
||||
const queryDiv = form.createDiv({cls: 'setting-item'});
|
||||
queryDiv.createEl('label', {text: 'Your Question'});
|
||||
this.queryInput = queryDiv.createEl('textarea', {
|
||||
cls: 'text-input',
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl, modalEl } = this;
|
||||
modalEl.addClass('lmstudio-modal');
|
||||
contentEl.empty();
|
||||
|
||||
// ----- Header -----
|
||||
const header = contentEl.createDiv({ cls: 'lmstudio-modal__header' });
|
||||
header.createEl('h2', { text: 'Ask LM Studio', cls: 'lmstudio-modal__title' });
|
||||
header.createEl('p', {
|
||||
cls: 'lmstudio-modal__subtitle',
|
||||
text: 'Run queries against your local LM Studio server. Streams into the active note at the cursor.',
|
||||
});
|
||||
|
||||
// ----- Question -----
|
||||
const querySection = contentEl.createDiv({ cls: 'lmstudio-modal__section' });
|
||||
querySection.createEl('label', {
|
||||
text: 'Question',
|
||||
cls: 'lmstudio-modal__label',
|
||||
attr: { for: 'lmstudio-modal-query' },
|
||||
});
|
||||
const queryTextarea = querySection.createEl('textarea', {
|
||||
cls: 'lmstudio-modal__textarea',
|
||||
attr: {
|
||||
rows: '4',
|
||||
placeholder: this.promptsService.getLMStudioQueryPlaceholder()
|
||||
}
|
||||
});
|
||||
|
||||
// Model selection
|
||||
const modelDiv = form.createDiv({cls: 'setting-item'});
|
||||
modelDiv.createEl('label', {text: 'Model'});
|
||||
this.modelSelect = modelDiv.createEl('select', {cls: 'dropdown'});
|
||||
// Use common LM Studio models - these would be dynamically loaded ideally
|
||||
['ibm/granite-3.2-8b', 'microsoft/phi-4-reasoning-plus', 'google/gemma-3-12b', 'meta-llama/llama-3.2-3b-instruct', 'custom-model'].forEach(model => {
|
||||
const option = this.modelSelect.createEl('option', {value: model, text: model});
|
||||
if (model === 'ibm/granite-3.2-8b') option.selected = true;
|
||||
});
|
||||
|
||||
// System prompt
|
||||
const systemDiv = form.createDiv({cls: 'setting-item'});
|
||||
systemDiv.createEl('label', {text: 'System Prompt (Optional)'});
|
||||
this.systemPromptInput = systemDiv.createEl('textarea', {
|
||||
cls: 'text-input system-prompt-input',
|
||||
attr: {
|
||||
rows: '2',
|
||||
placeholder: this.promptsService.getLMStudioSystemPromptPlaceholder()
|
||||
}
|
||||
});
|
||||
|
||||
// Max tokens
|
||||
const maxTokensDiv = form.createDiv({cls: 'setting-item'});
|
||||
maxTokensDiv.createEl('label', {text: 'Max Tokens'});
|
||||
this.maxTokensInput = maxTokensDiv.createEl('input', {
|
||||
type: 'number',
|
||||
value: '2048',
|
||||
cls: 'text-input'
|
||||
});
|
||||
|
||||
// Temperature
|
||||
const tempDiv = form.createDiv({cls: 'setting-item'});
|
||||
tempDiv.createEl('label', {text: 'Temperature (0.0 - 2.0)'});
|
||||
this.temperatureInput = tempDiv.createEl('input', {
|
||||
type: 'number',
|
||||
value: '0.7',
|
||||
attr: {
|
||||
step: '0.1',
|
||||
min: '0',
|
||||
max: '2'
|
||||
id: 'lmstudio-modal-query',
|
||||
rows: '6',
|
||||
placeholder: this.promptsService.getLMStudioQueryPlaceholder(),
|
||||
},
|
||||
cls: 'text-input'
|
||||
});
|
||||
queryTextarea.value = this.query;
|
||||
queryTextarea.addEventListener('input', () => {
|
||||
this.query = queryTextarea.value;
|
||||
});
|
||||
queryTextarea.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void this.onSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
// Images toggle
|
||||
const imagesDiv = form.createDiv({cls: 'setting-item'});
|
||||
const imagesLabel = imagesDiv.createEl('label');
|
||||
this.imagesToggle = imagesLabel.createEl('input', {type: 'checkbox'});
|
||||
this.imagesToggle.checked = false;
|
||||
imagesLabel.createSpan({text: ' Include Images'});
|
||||
|
||||
// Add description for images toggle
|
||||
const imagesDesc = imagesDiv.createDiv({cls: 'setting-item-description images-description'});
|
||||
imagesDesc.textContent = this.promptsService.getImagesToggleGenericDescription();
|
||||
// ----- Model -----
|
||||
const modelSection = contentEl.createDiv({ cls: 'lmstudio-modal__section' });
|
||||
modelSection.createEl('h3', { text: 'Model', cls: 'lmstudio-modal__section-title' });
|
||||
|
||||
// Stream toggle
|
||||
const streamDiv = form.createDiv({cls: 'setting-item'});
|
||||
const streamLabel = streamDiv.createEl('label');
|
||||
this.streamToggle = streamLabel.createEl('input', {type: 'checkbox'});
|
||||
this.streamToggle.checked = true;
|
||||
streamLabel.createSpan({text: ' Stream response'});
|
||||
|
||||
const buttonDiv = contentEl.createDiv({cls: 'setting-item'});
|
||||
const askButton = buttonDiv.createEl('button', {
|
||||
const modelSetting = new Setting(modelSection)
|
||||
.setName('Model')
|
||||
.setDesc(this.modelTagline(this.model))
|
||||
.addDropdown(dd => {
|
||||
LMSTUDIO_MODELS.forEach(({ value, label }) => dd.addOption(value, label));
|
||||
dd.setValue(this.model);
|
||||
dd.onChange((value) => {
|
||||
this.model = value;
|
||||
if (this.modelDescEl) this.modelDescEl.textContent = this.modelTagline(value);
|
||||
});
|
||||
});
|
||||
this.modelDescEl = modelSetting.descEl;
|
||||
|
||||
new Setting(modelSection)
|
||||
.setName('Max Tokens')
|
||||
.setDesc('Upper bound on response length. Local models cap lower than cloud — 2048 is a safe default.')
|
||||
.addText(t => t
|
||||
.setValue(String(this.maxTokens))
|
||||
.onChange(v => {
|
||||
const parsed = parseInt(v, 10);
|
||||
this.maxTokens = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_TOKENS;
|
||||
}));
|
||||
|
||||
new Setting(modelSection)
|
||||
.setName('Temperature')
|
||||
.setDesc('Higher = more creative / varied. 0.7 is the conventional default. Slide to 0 for deterministic output.')
|
||||
.addSlider(s => s
|
||||
.setLimits(0, 2, 0.1)
|
||||
.setValue(this.temperature)
|
||||
.setDynamicTooltip()
|
||||
.onChange(v => { this.temperature = v; }));
|
||||
|
||||
// ----- System Prompt (multi-line, doesn't fit a Setting row) -----
|
||||
const systemSection = contentEl.createDiv({ cls: 'lmstudio-modal__section' });
|
||||
systemSection.createEl('h3', { text: 'System Prompt (Optional)', cls: 'lmstudio-modal__section-title' });
|
||||
systemSection.createEl('p', {
|
||||
cls: 'lmstudio-modal__hint',
|
||||
text: 'Override the default system prompt for this request. Leave blank to use the plugin default.',
|
||||
});
|
||||
const systemTextarea = systemSection.createEl('textarea', {
|
||||
cls: 'lmstudio-modal__textarea lmstudio-modal__textarea--system',
|
||||
attr: {
|
||||
rows: '3',
|
||||
placeholder: this.promptsService.getLMStudioSystemPromptPlaceholder(),
|
||||
},
|
||||
});
|
||||
systemTextarea.value = this.systemPrompt;
|
||||
systemTextarea.addEventListener('input', () => {
|
||||
this.systemPrompt = systemTextarea.value;
|
||||
});
|
||||
|
||||
// ----- Returns -----
|
||||
const returnsSection = contentEl.createDiv({ cls: 'lmstudio-modal__section' });
|
||||
returnsSection.createEl('h3', { text: 'Include in response', cls: 'lmstudio-modal__section-title' });
|
||||
|
||||
new Setting(returnsSection)
|
||||
.setName('Images')
|
||||
.setDesc(this.promptsService.getImagesToggleGenericDescription())
|
||||
.addToggle(t => t
|
||||
.setValue(this.images)
|
||||
.onChange(v => { this.images = v; }));
|
||||
|
||||
// ----- Behavior -----
|
||||
const behaviorSection = contentEl.createDiv({ cls: 'lmstudio-modal__section' });
|
||||
behaviorSection.createEl('h3', { text: 'Behavior', cls: 'lmstudio-modal__section-title' });
|
||||
|
||||
new Setting(behaviorSection)
|
||||
.setName('Stream Response')
|
||||
.setDesc('Write tokens into the note as they arrive — recommended for long answers and slow local models.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.stream)
|
||||
.onChange(v => { this.stream = v; }));
|
||||
|
||||
// ----- Footer -----
|
||||
const footer = contentEl.createDiv({ cls: 'lmstudio-modal__footer' });
|
||||
const cancelBtn = footer.createEl('button', {
|
||||
text: 'Cancel',
|
||||
cls: 'lmstudio-modal__button',
|
||||
});
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
|
||||
const askBtn = footer.createEl('button', {
|
||||
text: 'Ask LM Studio',
|
||||
cls: 'mod-cta'
|
||||
cls: 'lmstudio-modal__button mod-cta',
|
||||
});
|
||||
|
||||
form.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
void this.onSubmit();
|
||||
};
|
||||
|
||||
askButton.onclick = () => this.onSubmit();
|
||||
|
||||
// Focus on the query input
|
||||
setTimeout(() => this.queryInput.focus(), 100);
|
||||
askBtn.addEventListener('click', () => void this.onSubmit());
|
||||
|
||||
setTimeout(() => queryTextarea.focus(), 50);
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
const query = this.queryInput.value.trim();
|
||||
if (!query) {
|
||||
|
||||
private modelTagline(value: string): string {
|
||||
return LMSTUDIO_MODELS.find(m => m.value === value)?.tagline ?? '';
|
||||
}
|
||||
|
||||
private async onSubmit(): Promise<void> {
|
||||
const trimmed = this.query.trim();
|
||||
if (!trimmed) {
|
||||
new Notice(this.promptsService.getEnterQuestionNotice());
|
||||
return;
|
||||
}
|
||||
|
||||
// If images are enabled, add image markers to the query
|
||||
let processedQuery = query;
|
||||
if (this.imagesToggle.checked) {
|
||||
processedQuery = `${query}
|
||||
|
||||
${this.promptsService.getImageReferencesPrompt()}`;
|
||||
let processedQuery = trimmed;
|
||||
if (this.images) {
|
||||
processedQuery = `${trimmed}\n\n${this.promptsService.getImageReferencesPrompt()}`;
|
||||
}
|
||||
|
||||
const options: LMStudioOptions = {
|
||||
max_tokens: parseInt(this.maxTokensInput.value) || 2048,
|
||||
temperature: parseFloat(this.temperatureInput.value) || 0.7,
|
||||
return_images: this.imagesToggle.checked
|
||||
max_tokens: this.maxTokens,
|
||||
temperature: this.temperature,
|
||||
return_images: this.images,
|
||||
};
|
||||
|
||||
const systemPrompt = this.systemPromptInput.value.trim();
|
||||
if (systemPrompt) {
|
||||
options.system_prompt = systemPrompt;
|
||||
const trimmedSystemPrompt = this.systemPrompt.trim();
|
||||
if (trimmedSystemPrompt) {
|
||||
options.system_prompt = trimmedSystemPrompt;
|
||||
}
|
||||
|
||||
this.close();
|
||||
await this.lmStudioService.queryLMStudio(
|
||||
processedQuery,
|
||||
this.modelSelect.value,
|
||||
this.streamToggle.checked,
|
||||
this.editor,
|
||||
processedQuery,
|
||||
this.model,
|
||||
this.stream,
|
||||
this.editor,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,120 +8,141 @@ export class TextEnhancementModal extends Modal {
|
|||
protected perplexityService: PerplexityService;
|
||||
protected promptsService: PromptsService;
|
||||
protected selectedText: string;
|
||||
protected promptTextArea!: HTMLTextAreaElement;
|
||||
protected enhanceButton!: HTMLButtonElement;
|
||||
|
||||
constructor(app: App, editor: Editor, perplexityService: PerplexityService, promptsService: PromptsService, selectedText: string) {
|
||||
private prompt: string;
|
||||
private enhanceBtn!: HTMLButtonElement;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
editor: Editor,
|
||||
perplexityService: PerplexityService,
|
||||
promptsService: PromptsService,
|
||||
selectedText: string
|
||||
) {
|
||||
super(app);
|
||||
this.editor = editor;
|
||||
this.perplexityService = perplexityService;
|
||||
this.promptsService = promptsService;
|
||||
this.selectedText = selectedText;
|
||||
this.prompt = this.promptsService.getEnhanceTemplate(this.selectedText);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.addClass('text-enhancement-modal');
|
||||
contentEl.createEl('h2', {text: 'Enhance Text with Perplexity'});
|
||||
|
||||
const form = contentEl.createEl('form');
|
||||
|
||||
// Original text display
|
||||
const originalTextDiv = form.createDiv({cls: 'setting-item'});
|
||||
originalTextDiv.createEl('label', {text: 'Selected Text'});
|
||||
const originalTextArea = originalTextDiv.createEl('textarea', {
|
||||
cls: 'text-input',
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl, modalEl } = this;
|
||||
modalEl.addClass('text-enhancement-modal');
|
||||
contentEl.empty();
|
||||
|
||||
// ----- Header -----
|
||||
const header = contentEl.createDiv({ cls: 'text-enhancement-modal__header' });
|
||||
header.createEl('h2', { text: 'Enhance Text with Perplexity', cls: 'text-enhancement-modal__title' });
|
||||
header.createEl('p', {
|
||||
cls: 'text-enhancement-modal__subtitle',
|
||||
text: 'Rewrite or expand selected text via Perplexity (sonar-pro). Streams into the active note at the cursor with Citations.',
|
||||
});
|
||||
|
||||
// ----- Selected Text (read-only) -----
|
||||
const selectedSection = contentEl.createDiv({ cls: 'text-enhancement-modal__section' });
|
||||
selectedSection.createEl('label', {
|
||||
text: 'Selected Text',
|
||||
cls: 'text-enhancement-modal__label',
|
||||
});
|
||||
const selectedTextarea = selectedSection.createEl('textarea', {
|
||||
cls: 'text-enhancement-modal__textarea text-enhancement-modal__textarea--readonly',
|
||||
attr: {
|
||||
rows: '6',
|
||||
readonly: 'readonly',
|
||||
},
|
||||
});
|
||||
selectedTextarea.value = this.selectedText;
|
||||
|
||||
// ----- Enhancement Prompt (editable) -----
|
||||
const promptSection = contentEl.createDiv({ cls: 'text-enhancement-modal__section' });
|
||||
promptSection.createEl('label', {
|
||||
text: 'Enhancement Prompt',
|
||||
cls: 'text-enhancement-modal__label',
|
||||
attr: { for: 'text-enhancement-modal-prompt' },
|
||||
});
|
||||
promptSection.createEl('p', {
|
||||
cls: 'text-enhancement-modal__hint',
|
||||
text: 'Pre-filled from the plugin template — edit to refine the rewrite instructions before submitting.',
|
||||
});
|
||||
const promptTextarea = promptSection.createEl('textarea', {
|
||||
cls: 'text-enhancement-modal__textarea',
|
||||
attr: {
|
||||
id: 'text-enhancement-modal-prompt',
|
||||
rows: '8',
|
||||
readonly: 'readonly'
|
||||
placeholder: 'Enter your enhancement prompt…',
|
||||
},
|
||||
});
|
||||
promptTextarea.value = this.prompt;
|
||||
promptTextarea.addEventListener('input', () => {
|
||||
this.prompt = promptTextarea.value;
|
||||
});
|
||||
promptTextarea.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void this.onSubmit();
|
||||
}
|
||||
});
|
||||
originalTextArea.value = this.selectedText;
|
||||
originalTextArea.style.backgroundColor = 'var(--background-secondary)';
|
||||
originalTextArea.style.color = 'var(--text-muted)';
|
||||
|
||||
// Editable prompt
|
||||
const promptDiv = form.createDiv({cls: 'setting-item'});
|
||||
promptDiv.createEl('label', {text: 'Enhancement Prompt'});
|
||||
this.promptTextArea = promptDiv.createEl('textarea', {
|
||||
cls: 'text-input',
|
||||
attr: {
|
||||
rows: '8',
|
||||
placeholder: 'Enter your enhancement prompt...'
|
||||
}
|
||||
});
|
||||
// Pre-populate with the enhancement prompt template
|
||||
const enhancementPrompt = this.promptsService.getEnhanceTemplate(this.selectedText);
|
||||
this.promptTextArea.value = enhancementPrompt;
|
||||
|
||||
// Buttons container
|
||||
const buttonsDiv = form.createDiv({cls: 'setting-item'});
|
||||
buttonsDiv.style.display = 'flex';
|
||||
buttonsDiv.style.gap = '10px';
|
||||
buttonsDiv.style.flexWrap = 'wrap';
|
||||
|
||||
// Enhance button
|
||||
this.enhanceButton = buttonsDiv.createEl('button', {
|
||||
text: 'Enhance Text',
|
||||
cls: 'mod-cta'
|
||||
});
|
||||
this.enhanceButton.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
await this.enhanceText();
|
||||
};
|
||||
|
||||
// Close button
|
||||
const closeButton = buttonsDiv.createEl('button', {
|
||||
// ----- Footer -----
|
||||
const footer = contentEl.createDiv({ cls: 'text-enhancement-modal__footer' });
|
||||
const cancelBtn = footer.createEl('button', {
|
||||
text: 'Cancel',
|
||||
cls: 'mod-cta'
|
||||
cls: 'text-enhancement-modal__button',
|
||||
});
|
||||
closeButton.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
};
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
|
||||
this.enhanceBtn = footer.createEl('button', {
|
||||
text: 'Enhance Text',
|
||||
cls: 'text-enhancement-modal__button mod-cta',
|
||||
});
|
||||
this.enhanceBtn.addEventListener('click', () => void this.onSubmit());
|
||||
|
||||
setTimeout(() => promptTextarea.focus(), 50);
|
||||
}
|
||||
|
||||
private async enhanceText(): Promise<void> {
|
||||
private async onSubmit(): Promise<void> {
|
||||
const trimmed = this.prompt.trim();
|
||||
if (!trimmed) {
|
||||
new Notice('Please enter an enhancement prompt.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.enhanceButton.disabled = true;
|
||||
this.enhanceButton.textContent = 'Enhancing...';
|
||||
|
||||
// Use the editable prompt from the textarea
|
||||
const enhancementPrompt = this.promptTextArea.value;
|
||||
|
||||
// Close the modal immediately so user can see the streaming content
|
||||
this.enhanceBtn.disabled = true;
|
||||
this.enhanceBtn.textContent = 'Enhancing…';
|
||||
|
||||
this.close();
|
||||
|
||||
// Insert a new line before the enhanced text to make it clear what's new
|
||||
const currentPosition = this.editor.getCursor();
|
||||
this.editor.replaceRange('\n\n', currentPosition);
|
||||
|
||||
// Call Perplexity service directly with the real editor, always streaming
|
||||
|
||||
const cursor = this.editor.getCursor();
|
||||
this.editor.replaceRange('\n\n', cursor);
|
||||
|
||||
await this.perplexityService.queryPerplexity(
|
||||
enhancementPrompt,
|
||||
'sonar-pro', // Use default model
|
||||
true, // Always stream
|
||||
trimmed,
|
||||
'sonar-pro',
|
||||
true,
|
||||
this.editor,
|
||||
{
|
||||
return_citations: true,
|
||||
return_images: false,
|
||||
return_related_questions: false
|
||||
return_related_questions: false,
|
||||
}
|
||||
);
|
||||
|
||||
new Notice('Text enhancement completed!');
|
||||
|
||||
|
||||
new Notice('Text enhancement completed.');
|
||||
} catch (error) {
|
||||
console.error('Error enhancing text:', error);
|
||||
new Notice('Failed to enhance text. Check console for details.');
|
||||
} finally {
|
||||
this.enhanceButton.disabled = false;
|
||||
this.enhanceButton.textContent = 'Enhance Text';
|
||||
if (this.enhanceBtn) {
|
||||
this.enhanceBtn.disabled = false;
|
||||
this.enhanceBtn.textContent = 'Enhance Text';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,120 +8,144 @@ export class TextEnhancementWithImagesModal extends Modal {
|
|||
protected perplexityService: PerplexityService;
|
||||
protected promptsService: PromptsService;
|
||||
protected selectedText: string;
|
||||
protected promptTextArea!: HTMLTextAreaElement;
|
||||
protected enhanceButton!: HTMLButtonElement;
|
||||
|
||||
constructor(app: App, editor: Editor, perplexityService: PerplexityService, promptsService: PromptsService, selectedText: string) {
|
||||
private prompt: string;
|
||||
private fetchBtn!: HTMLButtonElement;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
editor: Editor,
|
||||
perplexityService: PerplexityService,
|
||||
promptsService: PromptsService,
|
||||
selectedText: string
|
||||
) {
|
||||
super(app);
|
||||
this.editor = editor;
|
||||
this.perplexityService = perplexityService;
|
||||
this.promptsService = promptsService;
|
||||
this.selectedText = selectedText;
|
||||
this.prompt = this.promptsService.getEnhanceWithImagesTemplate(this.selectedText);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.addClass('get-related-images-modal');
|
||||
contentEl.createEl('h2', {text: 'Get Related Images'});
|
||||
|
||||
const form = contentEl.createEl('form');
|
||||
|
||||
// Original text display
|
||||
const originalTextDiv = form.createDiv({cls: 'setting-item'});
|
||||
originalTextDiv.createEl('label', {text: 'Selected Text'});
|
||||
const originalTextArea = originalTextDiv.createEl('textarea', {
|
||||
cls: 'text-input',
|
||||
attr: {
|
||||
rows: '8',
|
||||
readonly: 'readonly'
|
||||
}
|
||||
});
|
||||
originalTextArea.value = this.selectedText;
|
||||
originalTextArea.style.backgroundColor = 'var(--background-secondary)';
|
||||
originalTextArea.style.color = 'var(--text-muted)';
|
||||
|
||||
// Editable prompt
|
||||
const promptDiv = form.createDiv({cls: 'setting-item'});
|
||||
promptDiv.createEl('label', {text: 'Image Request Prompt'});
|
||||
this.promptTextArea = promptDiv.createEl('textarea', {
|
||||
cls: 'text-input',
|
||||
attr: {
|
||||
rows: '8',
|
||||
placeholder: 'Enter your image request prompt...'
|
||||
}
|
||||
});
|
||||
// Pre-populate with the image request prompt template
|
||||
const imageRequestPrompt = this.promptsService.getEnhanceWithImagesTemplate(this.selectedText);
|
||||
this.promptTextArea.value = imageRequestPrompt;
|
||||
onOpen(): void {
|
||||
const { contentEl, modalEl } = this;
|
||||
modalEl.addClass('text-enhancement-with-images-modal');
|
||||
contentEl.empty();
|
||||
|
||||
// Buttons container
|
||||
const buttonsDiv = form.createDiv({cls: 'setting-item'});
|
||||
buttonsDiv.style.display = 'flex';
|
||||
buttonsDiv.style.gap = '10px';
|
||||
buttonsDiv.style.flexWrap = 'wrap';
|
||||
|
||||
// Enhance button
|
||||
this.enhanceButton = buttonsDiv.createEl('button', {
|
||||
// ----- Header -----
|
||||
const header = contentEl.createDiv({ cls: 'text-enhancement-with-images-modal__header' });
|
||||
header.createEl('h2', {
|
||||
text: 'Get Related Images',
|
||||
cls: 'mod-cta'
|
||||
cls: 'text-enhancement-with-images-modal__title',
|
||||
});
|
||||
this.enhanceButton.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
await this.enhanceTextWithImages();
|
||||
};
|
||||
|
||||
// Close button
|
||||
const closeButton = buttonsDiv.createEl('button', {
|
||||
header.createEl('p', {
|
||||
cls: 'text-enhancement-with-images-modal__subtitle',
|
||||
text: 'Find images related to selected text via Perplexity (sonar-pro). Streams image markers into the active note at the cursor.',
|
||||
});
|
||||
|
||||
// ----- Selected Text (read-only) -----
|
||||
const selectedSection = contentEl.createDiv({ cls: 'text-enhancement-with-images-modal__section' });
|
||||
selectedSection.createEl('label', {
|
||||
text: 'Selected Text',
|
||||
cls: 'text-enhancement-with-images-modal__label',
|
||||
});
|
||||
const selectedTextarea = selectedSection.createEl('textarea', {
|
||||
cls: 'text-enhancement-with-images-modal__textarea text-enhancement-with-images-modal__textarea--readonly',
|
||||
attr: {
|
||||
rows: '6',
|
||||
readonly: 'readonly',
|
||||
},
|
||||
});
|
||||
selectedTextarea.value = this.selectedText;
|
||||
|
||||
// ----- Image Request Prompt (editable) -----
|
||||
const promptSection = contentEl.createDiv({ cls: 'text-enhancement-with-images-modal__section' });
|
||||
promptSection.createEl('label', {
|
||||
text: 'Image Request Prompt',
|
||||
cls: 'text-enhancement-with-images-modal__label',
|
||||
attr: { for: 'text-enhancement-with-images-modal-prompt' },
|
||||
});
|
||||
promptSection.createEl('p', {
|
||||
cls: 'text-enhancement-with-images-modal__hint',
|
||||
text: 'Pre-filled from the plugin template — edit to refine which kinds of images to surface.',
|
||||
});
|
||||
const promptTextarea = promptSection.createEl('textarea', {
|
||||
cls: 'text-enhancement-with-images-modal__textarea',
|
||||
attr: {
|
||||
id: 'text-enhancement-with-images-modal-prompt',
|
||||
rows: '8',
|
||||
placeholder: 'Enter your image request prompt…',
|
||||
},
|
||||
});
|
||||
promptTextarea.value = this.prompt;
|
||||
promptTextarea.addEventListener('input', () => {
|
||||
this.prompt = promptTextarea.value;
|
||||
});
|
||||
promptTextarea.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void this.onSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
// ----- Footer -----
|
||||
const footer = contentEl.createDiv({ cls: 'text-enhancement-with-images-modal__footer' });
|
||||
const cancelBtn = footer.createEl('button', {
|
||||
text: 'Cancel',
|
||||
cls: 'mod-cta'
|
||||
cls: 'text-enhancement-with-images-modal__button',
|
||||
});
|
||||
closeButton.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
};
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
|
||||
this.fetchBtn = footer.createEl('button', {
|
||||
text: 'Get Related Images',
|
||||
cls: 'text-enhancement-with-images-modal__button mod-cta',
|
||||
});
|
||||
this.fetchBtn.addEventListener('click', () => void this.onSubmit());
|
||||
|
||||
setTimeout(() => promptTextarea.focus(), 50);
|
||||
}
|
||||
|
||||
private async enhanceTextWithImages(): Promise<void> {
|
||||
private async onSubmit(): Promise<void> {
|
||||
const trimmed = this.prompt.trim();
|
||||
if (!trimmed) {
|
||||
new Notice('Please enter an image request prompt.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.enhanceButton.disabled = true;
|
||||
this.enhanceButton.textContent = 'Getting Related Images...';
|
||||
|
||||
// Use the editable prompt from the textarea
|
||||
const imagePrompt = this.promptTextArea.value;
|
||||
|
||||
// Close the modal immediately so user can see the streaming content
|
||||
this.fetchBtn.disabled = true;
|
||||
this.fetchBtn.textContent = 'Getting Related Images…';
|
||||
|
||||
this.close();
|
||||
|
||||
// Insert a new line before the images to make it clear what's new
|
||||
const currentPosition = this.editor.getCursor();
|
||||
this.editor.replaceRange('\n\n', currentPosition);
|
||||
|
||||
// Call Perplexity service directly with the real editor, always streaming and with images enabled
|
||||
|
||||
const cursor = this.editor.getCursor();
|
||||
this.editor.replaceRange('\n\n', cursor);
|
||||
|
||||
await this.perplexityService.queryPerplexity(
|
||||
imagePrompt,
|
||||
'sonar-pro', // Use default model
|
||||
true, // Always stream
|
||||
trimmed,
|
||||
'sonar-pro',
|
||||
true,
|
||||
this.editor,
|
||||
{
|
||||
return_citations: false, // No citations needed for image-only response
|
||||
return_images: true, // Enable images for this enhancement
|
||||
return_related_questions: false
|
||||
return_citations: false,
|
||||
return_images: true,
|
||||
return_related_questions: false,
|
||||
}
|
||||
);
|
||||
|
||||
new Notice('Related images added successfully!');
|
||||
|
||||
|
||||
new Notice('Related images added successfully.');
|
||||
} catch (error) {
|
||||
console.error('Error getting related images:', error);
|
||||
new Notice('Failed to get related images. Check console for details.');
|
||||
} finally {
|
||||
this.enhanceButton.disabled = false;
|
||||
this.enhanceButton.textContent = 'Get Related Images';
|
||||
if (this.fetchBtn) {
|
||||
this.fetchBtn.disabled = false;
|
||||
this.fetchBtn.textContent = 'Get Related Images';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,193 @@
|
|||
/* LMStudioModal Styles */
|
||||
/* ============================================================
|
||||
LM Studio Modal — local LLM query composer
|
||||
Built on Obsidian's native Setting components for theming
|
||||
parity, with custom textareas for the question + system prompt.
|
||||
============================================================ */
|
||||
|
||||
.lmstudio-modal .text-input {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
padding: 12px;
|
||||
.lmstudio-modal {
|
||||
width: 90vw;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.lmstudio-modal .system-prompt-input {
|
||||
.lmstudio-modal .modal-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ----- Header ----- */
|
||||
|
||||
.lmstudio-modal__header {
|
||||
padding: 24px 28px 16px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.lmstudio-modal__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.lmstudio-modal__subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ----- Sections ----- */
|
||||
|
||||
.lmstudio-modal__section {
|
||||
padding: 18px 28px 4px;
|
||||
}
|
||||
|
||||
.lmstudio-modal__section + .lmstudio-modal__section {
|
||||
border-top: 1px solid var(--background-modifier-border-hover);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.lmstudio-modal__section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lmstudio-modal__section .setting-item {
|
||||
padding: 10px 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.lmstudio-modal__section .setting-item + .setting-item {
|
||||
border-top: 1px solid var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
/* ----- Inline hint paragraph ----- */
|
||||
|
||||
.lmstudio-modal__hint {
|
||||
margin: -4px 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ----- Question label + textareas ----- */
|
||||
|
||||
.lmstudio-modal__label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lmstudio-modal__textarea {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
}
|
||||
min-height: 120px;
|
||||
padding: 12px 14px;
|
||||
font-family: var(--font-text);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
resize: vertical;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.lmstudio-modal__textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 3px var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.lmstudio-modal__textarea::placeholder {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
/* System-prompt textarea is shorter than the main question textarea */
|
||||
.lmstudio-modal__textarea--system {
|
||||
min-height: 72px;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ----- Footer / actions ----- */
|
||||
|
||||
.lmstudio-modal__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px 28px 24px;
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary);
|
||||
border-bottom-left-radius: var(--modal-radius, 8px);
|
||||
border-bottom-right-radius: var(--modal-radius, 8px);
|
||||
}
|
||||
|
||||
.lmstudio-modal__button {
|
||||
padding: 8px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
|
||||
}
|
||||
|
||||
.lmstudio-modal__button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.lmstudio-modal__button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.lmstudio-modal__button.mod-cta {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.lmstudio-modal__button.mod-cta:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
border-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.lmstudio-modal__button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ----- Responsive ----- */
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.lmstudio-modal {
|
||||
width: 95vw;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.lmstudio-modal__header,
|
||||
.lmstudio-modal__section,
|
||||
.lmstudio-modal__footer {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.lmstudio-modal__footer {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.lmstudio-modal__button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,106 +1,180 @@
|
|||
/* ============================================================
|
||||
Text Enhancement Modal — selection rewrite/expand composer
|
||||
Built on the unified wide-modal pattern (see
|
||||
context-v/issues/Widen-Modals-in-Obsidian-using-CSS.md).
|
||||
============================================================ */
|
||||
|
||||
.text-enhancement-modal {
|
||||
max-width: 800px;
|
||||
width: 90vw;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.text-enhancement-modal .setting-item {
|
||||
margin-bottom: 20px;
|
||||
.text-enhancement-modal .modal-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.text-enhancement-modal .setting-item label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
/* ----- Header ----- */
|
||||
|
||||
.text-enhancement-modal__header {
|
||||
padding: 24px 28px 16px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.text-enhancement-modal__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.text-enhancement-modal .text-input {
|
||||
width: 40%;
|
||||
min-height: 100px;
|
||||
font-family: var(--font-monospace);
|
||||
.text-enhancement-modal__subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ----- Sections ----- */
|
||||
|
||||
.text-enhancement-modal__section {
|
||||
padding: 18px 28px 4px;
|
||||
}
|
||||
|
||||
.text-enhancement-modal__section + .text-enhancement-modal__section {
|
||||
border-top: 1px solid var(--background-modifier-border-hover);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ----- Inline hint paragraph ----- */
|
||||
|
||||
.text-enhancement-modal__hint {
|
||||
margin: -4px 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ----- Labels + textareas ----- */
|
||||
|
||||
.text-enhancement-modal__label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.text-enhancement-modal__textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 12px 14px;
|
||||
font-family: var(--font-text);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
resize: vertical;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.text-enhancement-modal .dropdown {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
.text-enhancement-modal__textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 3px var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.text-enhancement-modal__textarea::placeholder {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
/* Read-only "Selected Text" display — visually distinct from editable input */
|
||||
.text-enhancement-modal__textarea--readonly {
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-muted);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.text-enhancement-modal__textarea--readonly:focus {
|
||||
border-color: var(--background-modifier-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ----- Footer / actions ----- */
|
||||
|
||||
.text-enhancement-modal__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px 28px 24px;
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary);
|
||||
border-bottom-left-radius: var(--modal-radius, 8px);
|
||||
border-bottom-right-radius: var(--modal-radius, 8px);
|
||||
}
|
||||
|
||||
.text-enhancement-modal__button {
|
||||
padding: 8px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.text-enhancement-modal button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
|
||||
}
|
||||
|
||||
.text-enhancement-modal button:disabled {
|
||||
.text-enhancement-modal__button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.text-enhancement-modal__button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.text-enhancement-modal__button.mod-cta {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.text-enhancement-modal__button.mod-cta:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
border-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.text-enhancement-modal__button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.text-enhancement-modal button.mod-cta {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
/* ----- Responsive ----- */
|
||||
|
||||
.text-enhancement-modal button.mod-cta:hover:not(:disabled) {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.text-enhancement-modal button.mod-warning {
|
||||
background-color: var(--text-error);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.text-enhancement-modal button.mod-warning:hover:not(:disabled) {
|
||||
background-color: var(--text-error-hover);
|
||||
}
|
||||
|
||||
.text-enhancement-modal input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.text-enhancement-modal .setting-item-description {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Original text area styling */
|
||||
.text-enhancement-modal .text-input[readonly] {
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
/* Enhanced text area styling */
|
||||
.text-enhancement-modal .text-input:not([readonly]) {
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 600px) {
|
||||
.text-enhancement-modal {
|
||||
width: 95vw;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.text-enhancement-modal button {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.text-enhancement-modal__header,
|
||||
.text-enhancement-modal__section,
|
||||
.text-enhancement-modal__footer {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.text-enhancement-modal .setting-item {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.text-enhancement-modal__footer {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.text-enhancement-modal__button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,179 @@
|
|||
.get-related-images-modal {
|
||||
max-width: 600px;
|
||||
/* ============================================================
|
||||
Text Enhancement With Images Modal — selection → related images
|
||||
Built on the unified wide-modal pattern (see
|
||||
context-v/issues/Widen-Modals-in-Obsidian-using-CSS.md).
|
||||
============================================================ */
|
||||
|
||||
.text-enhancement-with-images-modal {
|
||||
width: 90vw;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.get-related-images-modal .setting-item {
|
||||
margin-bottom: 20px;
|
||||
.text-enhancement-with-images-modal .modal-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.get-related-images-modal .setting-item label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
/* ----- Header ----- */
|
||||
|
||||
.text-enhancement-with-images-modal__header {
|
||||
padding: 24px 28px 16px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.get-related-images-modal .text-input {
|
||||
width: 40%;
|
||||
.text-enhancement-with-images-modal__subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ----- Sections ----- */
|
||||
|
||||
.text-enhancement-with-images-modal__section {
|
||||
padding: 18px 28px 4px;
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__section + .text-enhancement-with-images-modal__section {
|
||||
border-top: 1px solid var(--background-modifier-border-hover);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ----- Inline hint paragraph ----- */
|
||||
|
||||
.text-enhancement-with-images-modal__hint {
|
||||
margin: -4px 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ----- Labels + textareas ----- */
|
||||
|
||||
.text-enhancement-with-images-modal__label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 12px;
|
||||
padding: 12px 14px;
|
||||
font-family: var(--font-text);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
resize: vertical;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 3px var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__textarea::placeholder {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__textarea--readonly {
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-muted);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__textarea--readonly:focus {
|
||||
border-color: var(--background-modifier-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ----- Footer / actions ----- */
|
||||
|
||||
.text-enhancement-with-images-modal__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px 28px 24px;
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary);
|
||||
border-bottom-left-radius: var(--modal-radius, 8px);
|
||||
border-bottom-right-radius: var(--modal-radius, 8px);
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__button {
|
||||
padding: 8px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.get-related-images-modal .text-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 2px var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.get-related-images-modal .text-input[readonly] {
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.get-related-images-modal button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
|
||||
}
|
||||
|
||||
.get-related-images-modal button.mod-cta {
|
||||
.text-enhancement-with-images-modal__button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__button.mod-cta {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.get-related-images-modal button.mod-cta:hover {
|
||||
.text-enhancement-with-images-modal__button.mod-cta:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
border-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.get-related-images-modal button:disabled {
|
||||
opacity: 0.6;
|
||||
.text-enhancement-with-images-modal__button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.get-related-images-modal h2 {
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-normal);
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
/* ----- Responsive ----- */
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.text-enhancement-with-images-modal {
|
||||
width: 95vw;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__header,
|
||||
.text-enhancement-with-images-modal__section,
|
||||
.text-enhancement-with-images-modal__footer {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__footer {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.text-enhancement-with-images-modal__button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
771
styles.css
771
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue