diff --git a/README.md b/README.md index af0a1b7..08513df 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Perplexed: AI Content Generation for Obsidian **Perplexed** is an Obsidian plugin that enables AI-powered content generation with source citations using [Perplexity](https://www.perplexity.ai/), [Anthropic Claude](https://www.anthropic.com/), and [Perplexica / Vane](https://github.com/ItzCrazyKns/Vane) (self-hosted). This plugin brings research-grade AI capabilities directly into your Obsidian workspace, allowing you to generate well-cited content for your notes. ## 🎯 Key Features - +![Perplexed UI Modal interface](https://i.imgur.com/jaZ4UfS.png) - **Source-Cited AI Responses**: Get AI-generated content with proper citations and references - Default format: > ```markdown diff --git a/esbuild.config.mjs b/esbuild.config.mjs index c6dc1cb..65d6276 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,6 @@ import esbuild from 'esbuild'; import process from 'node:process'; -import builtins from 'builtin-modules'; +import { builtinModules as builtins } from 'node:module'; const banner = `/* * Content Farm Plugin for Obsidian diff --git a/eslint.config.mjs b/eslint.config.mjs index 8af63b2..65570e8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,15 +1,21 @@ import js from "@eslint/js"; import tseslint from "typescript-eslint"; import globals from "globals"; +import obsidianmd from "eslint-plugin-obsidianmd"; export default tseslint.config( { - ignores: ["node_modules/", "main.js", "**/*.mjs"], + ignores: ["node_modules/", "main.js", "**/*.mjs", "test-*.sh"], }, js.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.strict, + // Obsidian community-plugin rules — mirrors what ObsidianReviewBot + // enforces server-side at submission time. Keep this enabled so + // violations surface in `pnpm build`, not in the marketplace PR review. + ...obsidianmd.configs.recommended, { + files: ["**/*.ts"], languageOptions: { parserOptions: { ecmaVersion: "latest", @@ -18,6 +24,7 @@ export default tseslint.config( }, globals: { ...globals.node, + ...globals.browser, }, }, rules: { @@ -30,9 +37,44 @@ export default tseslint.config( "@typescript-eslint/no-unnecessary-type-assertion": "error", "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-base-to-string": "error", + "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/consistent-type-imports": "error", + // Bot's exact ban: only warn / error / debug allowed. + "no-console": ["error", { allow: ["warn", "error", "debug"] }], + // Override the obsidianmd recommended sentence-case rule with our + // brand allowlist — these are real proper nouns and should remain + // capitalized in UI text. `enforceCamelCaseLower` is left off so + // proper nouns aren't lowercased mid-string. `allowAutoFix` lets + // `eslint --fix` apply suggestions for the trivial cases. + "obsidianmd/ui/sentence-case": [ + "error", + { + brands: [ + "Perplexity", + "Perplexica", + "Vane", + "Claude", + "Anthropic", + "LM Studio", + "LMStudio", + "Imgur", + "ImageKit", + "OpenAI", + "Ollama", + "Sonar", + "Llama", + "GPT", + "YAML", + "JSON", + "URL", + "API", + ], + acronyms: ["AI", "ID", "URL", "API", "JSON", "YAML", "HTTP", "HTTPS", "GPU", "CPU"], + allowAutoFix: true, + }, + ], }, }, ); diff --git a/main.ts b/main.ts index 702ffff..d433acb 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,5 @@ import type { App, Editor} from 'obsidian'; import { Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; -import * as dotenv from 'dotenv'; // Import services import { PerplexityService } from './src/services/perplexityService'; @@ -19,8 +18,6 @@ import { ArticleGeneratorModal } from './src/modals/ArticleGeneratorModal'; import { TextEnhancementModal } from './src/modals/TextEnhancementModal'; import { TextEnhancementWithImagesModal } from './src/modals/TextEnhancementWithImagesModal'; -// Load environment variables -dotenv.config({ path: `${process.cwd()}/.env` }); interface PerplexedPluginSettings { mySetting: string; @@ -104,8 +101,8 @@ const DEFAULT_SETTINGS: PerplexedPluginSettings = { "maxTokens": 2048, "temperature": 0.7 }`, - perplexityApiKey: process.env.PERPLEXITY_API_KEY || '', - anthropicApiKey: process.env.ANTHROPIC_API_KEY || '', + perplexityApiKey: '', + anthropicApiKey: '', claudeDefaultModel: 'claude-opus-4-7', perplexityRequestTemplate: `{ "model": "llama-3.1-sonar-small-128k-online", @@ -295,15 +292,15 @@ export default class PerplexedPlugin extends Plugin { async onload(): Promise { try { - console.log('Perplexed Plugin: Starting initialization...'); + console.debug('Perplexed Plugin: Starting initialization...'); await this.loadSettings(); - console.log('Perplexed Plugin: Settings loaded successfully'); + console.debug('Perplexed Plugin: Settings loaded successfully'); // Initialize prompts service first try { this.promptsService = new PromptsService(this.settings.prompts); - console.log('Perplexed Plugin: PromptsService initialized successfully'); + console.debug('Perplexed Plugin: PromptsService initialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to initialize PromptsService:', error); new Notice('Failed to initialize PromptsService'); @@ -320,7 +317,7 @@ export default class PerplexedPlugin extends Plugin { requestTemplate: this.settings.perplexityRequestTemplate, headerPosition: this.settings.headerPosition }); - console.log('Perplexed Plugin: PerplexityService initialized successfully'); + console.debug('Perplexed Plugin: PerplexityService initialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to initialize PerplexityService:', error); new Notice('Failed to initialize PerplexityService'); @@ -335,7 +332,7 @@ export default class PerplexedPlugin extends Plugin { promptsService: this.promptsService, requestTemplate: this.settings.requestBodyTemplate }); - console.log('Perplexed Plugin: PerplexicaService initialized successfully'); + console.debug('Perplexed Plugin: PerplexicaService initialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to initialize PerplexicaService:', error); new Notice('Failed to initialize PerplexicaService'); @@ -348,7 +345,7 @@ export default class PerplexedPlugin extends Plugin { promptsService: this.promptsService, requestTemplate: this.settings.lmStudioRequestTemplate }); - console.log('Perplexed Plugin: LMStudioService initialized successfully'); + console.debug('Perplexed Plugin: LMStudioService initialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to initialize LMStudioService:', error); new Notice('Failed to initialize LMStudioService'); @@ -361,7 +358,7 @@ export default class PerplexedPlugin extends Plugin { promptsService: this.promptsService, headerPosition: this.settings.headerPosition, }); - console.log('Perplexed Plugin: ClaudeService initialized successfully'); + console.debug('Perplexed Plugin: ClaudeService initialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to initialize ClaudeService:', error); new Notice('Failed to initialize ClaudeService'); @@ -373,71 +370,71 @@ export default class PerplexedPlugin extends Plugin { this.perplexicaService = null; this.lmStudioService = null; this.claudeService = null; - console.log('Perplexed Plugin: Skipping service initialization due to PromptsService failure'); + console.debug('Perplexed Plugin: Skipping service initialization due to PromptsService failure'); } // Debug: Log current settings - console.log('Perplexed Plugin: Current Perplexica Path:', this.settings.perplexicaEndpoint); - console.log('Perplexed Plugin: Full settings:', JSON.stringify(this.settings, null, 2)); + console.debug('Perplexed Plugin: Current Perplexica Path:', this.settings.perplexicaEndpoint); + console.debug('Perplexed Plugin: Full settings:', JSON.stringify(this.settings, null, 2)); // This adds a settings tab so the user can configure various aspects of the plugin this.addSettingTab(new PerplexedSettingTab(this.app, this)); - console.log('Perplexed Plugin: Settings tab added successfully'); + console.debug('Perplexed Plugin: Settings tab added successfully'); // Register commands with error handling try { this.registerPerplexicaCommands(); - console.log('Perplexed Plugin: Perplexica commands registered successfully'); + console.debug('Perplexed Plugin: Perplexica commands registered successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to register Perplexica commands:', error); } try { this.registerPerplexityCommands(); - console.log('Perplexed Plugin: Perplexity commands registered successfully'); + console.debug('Perplexed Plugin: Perplexity commands registered successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to register Perplexity commands:', error); } try { this.registerLMStudioCommands(); - console.log('Perplexed Plugin: LM Studio commands registered successfully'); + console.debug('Perplexed Plugin: LM Studio commands registered successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to register LM Studio commands:', error); } try { this.registerClaudeCommands(); - console.log('Perplexed Plugin: Claude commands registered successfully'); + console.debug('Perplexed Plugin: Claude commands registered successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to register Claude commands:', error); } try { this.registerArticleGeneratorCommands(); - console.log('Perplexed Plugin: Article generator commands registered successfully'); + console.debug('Perplexed Plugin: Article generator commands registered successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to register article generator commands:', error); } try { this.registerTextEnhancementCommands(); - console.log('Perplexed Plugin: Text enhancement commands registered successfully'); + console.debug('Perplexed Plugin: Text enhancement commands registered successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to register text enhancement commands:', error); } try { this.registerTextEnhancementWithImagesCommands(); - console.log('Perplexed Plugin: Get related images commands registered successfully'); + console.debug('Perplexed Plugin: Get related images commands registered successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to register get related images commands:', error); } - // Add debug command to check command status + // Diagnostic action: log registered commands to the console. this.addCommand({ - id: 'debug-commands', - name: 'Debug: Check Perplexed Commands', + id: 'debug-status', + name: 'Debug: log registered actions', callback: () => { this.debugCommands(); } @@ -446,27 +443,28 @@ export default class PerplexedPlugin extends Plugin { // Add command to reset prompts to defaults this.addCommand({ id: 'reset-prompts', - name: 'Reset Prompts to Default', + name: 'Reset prompts to default', callback: async () => { await this.resetPromptsToDefault(); } }); - // Add command to reinitialize services + // Reinitialize all provider services (Perplexity / Perplexica / + // LM Studio / Claude). Useful after editing settings. this.addCommand({ id: 'reinitialize-services', - name: 'Reinitialize Perplexed Services', + name: 'Reinitialize provider services', callback: async () => { await this.reinitializeServices(); } }); - console.log('Perplexed Plugin: Initialization completed successfully'); - new Notice('Perplexed Plugin loaded successfully'); + console.debug('Perplexed Plugin: Initialization completed successfully'); + new Notice('Perplexed plugin loaded successfully'); } catch (error) { console.error('Perplexed Plugin: Critical initialization error:', error); - new Notice('Perplexed Plugin failed to load properly'); + new Notice('Perplexed plugin failed to load properly'); } } @@ -476,7 +474,7 @@ export default class PerplexedPlugin extends Plugin { } private async loadSettings() { - const savedData = await this.loadData(); + const savedData: Partial = (await this.loadData()) as Partial ?? {}; this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData); // Ensure new fields are always present (migration for existing users) @@ -568,10 +566,10 @@ export default class PerplexedPlugin extends Plugin { // Command to show current settings this.addCommand({ id: 'show-perplexica-settings', - name: 'Show Perplexica / Vane Settings', + name: 'Show Perplexica / Vane settings', callback: () => { new Notice(`Current Perplexica / Vane URL: ${this.settings.perplexicaEndpoint}`); - console.log('Perplexica Settings:', this.settings); + console.debug('Perplexica Settings:', this.settings); } }); @@ -625,10 +623,10 @@ export default class PerplexedPlugin extends Plugin { // Command to show current Perplexity settings this.addCommand({ id: 'show-perplexity-settings', - name: 'Show Perplexity Settings', + name: 'Show Perplexity settings', callback: () => { new Notice(`Current Perplexity URL: ${this.settings.perplexityEndpoint}`); - console.log('Perplexity Settings:', this.settings); + console.debug('Perplexity Settings:', this.settings); } }); @@ -660,19 +658,19 @@ export default class PerplexedPlugin extends Plugin { // Add a fallback command that shows service status this.addCommand({ id: 'perplexity-service-status', - name: 'Check Perplexity Service Status', + name: 'Check Perplexity service status', callback: () => { if (this.perplexityService) { new Notice('Perplexity service is initialized and ready'); - console.log('Perplexity service status: OK'); + console.debug('Perplexity service status: OK'); } else { - new Notice('Perplexity service is NOT initialized. Check console for errors.'); + new Notice('Perplexity service is not initialized. Check console for errors.'); console.error('Perplexity service status: FAILED'); } } }); - console.log('Perplexed Plugin: Perplexity commands registered successfully'); + console.debug('Perplexed Plugin: Perplexity commands registered successfully'); } catch (error) { console.error('Perplexed Plugin: Error registering Perplexity commands:', error); throw error; @@ -698,14 +696,14 @@ export default class PerplexedPlugin extends Plugin { this.addCommand({ id: 'claude-service-status', - name: 'Check Claude Service Status', + name: 'Check Claude service status', callback: () => { if (this.claudeService && this.settings.anthropicApiKey) { new Notice('Claude service is initialized and an API key is configured.'); } else if (this.claudeService) { new Notice('Claude service is initialized but no API key is set.'); } else { - new Notice('Claude service is NOT initialized.'); + new Notice('Claude service is not initialized.'); } }, }); @@ -734,10 +732,10 @@ export default class PerplexedPlugin extends Plugin { // Command to show current LM Studio settings this.addCommand({ id: 'show-lmstudio-settings', - name: 'Show LM Studio Settings', + name: 'Show LM Studio settings', callback: () => { new Notice(`Current LM Studio URL: ${this.settings.lmStudioEndpoint}`); - console.log('LM Studio Settings:', this.settings); + console.debug('LM Studio Settings:', this.settings); } }); @@ -771,7 +769,7 @@ export default class PerplexedPlugin extends Plugin { // Register Article Generator command this.addCommand({ id: 'generate-article', - name: 'Generate One-Page Article', + name: 'Generate one-page article', editorCallback: (editor: Editor) => { try { if (!this.perplexityService) { @@ -787,7 +785,7 @@ export default class PerplexedPlugin extends Plugin { new ArticleGeneratorModal(this.app, editor, this.perplexityService, this.promptsService).open(); } catch (error) { console.error('Error opening Article Generator modal:', error); - new Notice('Failed to open Article Generator modal. Check console for details.'); + new Notice('Failed to open article generator modal. Check console for details.'); } } }); @@ -797,7 +795,7 @@ export default class PerplexedPlugin extends Plugin { // Register Text Enhancement command this.addCommand({ id: 'enhance-text', - name: 'Enhance Selected Text with Perplexity', + name: 'Enhance selected text with Perplexity', editorCallback: (editor: Editor) => { try { const selectedText = editor.getSelection(); @@ -820,7 +818,7 @@ export default class PerplexedPlugin extends Plugin { new TextEnhancementModal(this.app, editor, this.perplexityService, this.promptsService, selectedText).open(); } catch (error) { console.error('Error opening Text Enhancement modal:', error); - new Notice('Failed to open Text Enhancement modal. Check console for details.'); + new Notice('Failed to open text enhancement modal. Check console for details.'); } } }); @@ -830,7 +828,7 @@ export default class PerplexedPlugin extends Plugin { // Register Get Related Images command this.addCommand({ id: 'enhance-text-with-images', - name: 'Get Related Images for Selected Text', + name: 'Get related images for selected text', editorCallback: (editor: Editor) => { try { const selectedText = editor.getSelection(); @@ -853,21 +851,21 @@ export default class PerplexedPlugin extends Plugin { new TextEnhancementWithImagesModal(this.app, editor, this.perplexityService, this.promptsService, selectedText).open(); } catch (error) { console.error('Error opening Get Related Images modal:', error); - new Notice('Failed to open Get Related Images modal. Check console for details.'); + new Notice('Failed to open get related images modal. Check console for details.'); } } }); } private debugCommands(): void { - console.log('=== Perplexed Plugin Debug Information ==='); - console.log('Plugin instance:', this); - console.log('Settings:', this.settings); - console.log('Services status:'); - console.log('- PromptsService:', this.promptsService ? 'Initialized' : 'NOT INITIALIZED'); - console.log('- PerplexityService:', this.perplexityService ? 'Initialized' : 'NOT INITIALIZED'); - console.log('- PerplexicaService:', this.perplexicaService ? 'Initialized' : 'NOT INITIALIZED'); - console.log('- LMStudioService:', this.lmStudioService ? 'Initialized' : 'NOT INITIALIZED'); + console.debug('=== Perplexed Plugin Debug Information ==='); + console.debug('Plugin instance:', this); + console.debug('Settings:', this.settings); + console.debug('Services status:'); + console.debug('- PromptsService:', this.promptsService ? 'Initialized' : 'NOT INITIALIZED'); + console.debug('- PerplexityService:', this.perplexityService ? 'Initialized' : 'NOT INITIALIZED'); + console.debug('- PerplexicaService:', this.perplexicaService ? 'Initialized' : 'NOT INITIALIZED'); + console.debug('- LMStudioService:', this.lmStudioService ? 'Initialized' : 'NOT INITIALIZED'); // Check if commands are registered in Obsidian const registeredCommands = this.app.commands.commands; @@ -880,20 +878,20 @@ export default class PerplexedPlugin extends Plugin { cmd.includes('enhance-text') ); - console.log('Registered Perplexed commands:', perplexedCommands); + console.debug('Registered Perplexed commands:', perplexedCommands); if (perplexedCommands.length === 0) { - new Notice('No Perplexed commands found! Check console for details.'); + new Notice('No perplexed commands found! Check console for details.'); } else { new Notice(`Found ${perplexedCommands.length} Perplexed commands. Check console for details.`); } - console.log('=== End Debug Information ==='); + console.debug('=== End Debug Information ==='); } private async resetPromptsToDefault(): Promise { try { - console.log('Perplexed Plugin: Resetting prompts to default...'); + console.debug('Perplexed Plugin: Resetting prompts to default...'); new Notice('Resetting prompts to default values...'); // Reset all prompt settings to default values @@ -905,11 +903,11 @@ export default class PerplexedPlugin extends Plugin { // Reinitialize the prompts service with new settings if (this.promptsService) { this.promptsService.updateSettings(this.settings.prompts); - console.log('Perplexed Plugin: PromptsService updated with default settings'); + console.debug('Perplexed Plugin: PromptsService updated with default settings'); } new Notice('✅ Prompts reset to default values successfully'); - console.log('Perplexed Plugin: Prompts reset to default successfully'); + console.debug('Perplexed Plugin: Prompts reset to default successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to reset prompts to default:', error); @@ -919,13 +917,13 @@ export default class PerplexedPlugin extends Plugin { private async reinitializeServices(): Promise { try { - console.log('Perplexed Plugin: Reinitializing services...'); - new Notice('Reinitializing Perplexed services...'); + console.debug('Perplexed Plugin: Reinitializing services...'); + new Notice('Reinitializing perplexed services...'); // Reinitialize prompts service first try { this.promptsService = new PromptsService(this.settings.prompts); - console.log('Perplexed Plugin: PromptsService reinitialized successfully'); + console.debug('Perplexed Plugin: PromptsService reinitialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to reinitialize PromptsService:', error); this.promptsService = null; @@ -941,7 +939,7 @@ export default class PerplexedPlugin extends Plugin { requestTemplate: this.settings.perplexityRequestTemplate, headerPosition: this.settings.headerPosition }); - console.log('Perplexed Plugin: PerplexityService reinitialized successfully'); + console.debug('Perplexed Plugin: PerplexityService reinitialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to reinitialize PerplexityService:', error); this.perplexityService = null; @@ -955,7 +953,7 @@ export default class PerplexedPlugin extends Plugin { promptsService: this.promptsService, requestTemplate: this.settings.requestBodyTemplate }); - console.log('Perplexed Plugin: PerplexicaService reinitialized successfully'); + console.debug('Perplexed Plugin: PerplexicaService reinitialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to reinitialize PerplexicaService:', error); this.perplexicaService = null; @@ -967,7 +965,7 @@ export default class PerplexedPlugin extends Plugin { promptsService: this.promptsService, requestTemplate: this.settings.lmStudioRequestTemplate }); - console.log('Perplexed Plugin: LMStudioService reinitialized successfully'); + console.debug('Perplexed Plugin: LMStudioService reinitialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to reinitialize LMStudioService:', error); this.lmStudioService = null; @@ -979,7 +977,7 @@ export default class PerplexedPlugin extends Plugin { promptsService: this.promptsService, headerPosition: this.settings.headerPosition, }); - console.log('Perplexed Plugin: ClaudeService reinitialized successfully'); + console.debug('Perplexed Plugin: ClaudeService reinitialized successfully'); } catch (error) { console.error('Perplexed Plugin: Failed to reinitialize ClaudeService:', error); this.claudeService = null; @@ -990,11 +988,11 @@ export default class PerplexedPlugin extends Plugin { this.perplexicaService = null; this.lmStudioService = null; this.claudeService = null; - console.log('Perplexed Plugin: Skipping service reinitialization due to PromptsService failure'); + console.debug('Perplexed Plugin: Skipping service reinitialization due to PromptsService failure'); } new Notice('Services reinitialization completed. Check console for details.'); - console.log('Perplexed Plugin: Services reinitialization completed'); + console.debug('Perplexed Plugin: Services reinitialization completed'); } catch (error) { console.error('Perplexed Plugin: Error during services reinitialization:', error); @@ -1024,8 +1022,7 @@ class PerplexedSettingTab extends PluginSettingTab { containerEl.empty(); // Perplexity Section - const perplexityHeader = containerEl.createEl('h3', { text: 'Perplexity (Remote Service)' }); - perplexityHeader.style.color = 'var(--text-accent)'; + new Setting(containerEl).setName("Perplexity (remote service)").setHeading(); containerEl.createEl('p', { text: 'Configure settings for the hosted Perplexity AI service', cls: 'setting-item-description' @@ -1035,7 +1032,7 @@ class PerplexedSettingTab extends PluginSettingTab { .setName('Endpoint') .setDesc('API endpoint for Perplexity service') .addText(text => text - .setPlaceholder('https://api.perplexity.ai/chat/completions') + .setPlaceholder('HTTPS://API.Perplexity.ai/chat/completions') .setValue(this.plugin.settings.perplexityEndpoint) .onChange(async (value: string) => { this.plugin.settings.perplexityEndpoint = value; @@ -1044,10 +1041,10 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('API Key') + .setName('API key') .setDesc('Your Perplexity API key (required for remote service)') .addText(text => text - .setPlaceholder('pplx-xxxxxxxxxxxxxxxxxxxxx') + .setPlaceholder('Pplx-xxxxxxxxxxxxxxxxxxxxx') .setValue(this.plugin.settings.perplexityApiKey) .onChange(async (value: string) => { this.plugin.settings.perplexityApiKey = value; @@ -1056,7 +1053,7 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('Header Position') + .setName('Header position') .setDesc('Where to place the query header in generated articles') .addDropdown(dropdown => dropdown .addOption('top', 'Top of article') @@ -1070,22 +1067,20 @@ class PerplexedSettingTab extends PluginSettingTab { // Perplexity Request Template const perplexityJsonSetting = new Setting(containerEl) - .setName('Request Body Template') + .setName('Request body template') .setDesc('JSON template for Perplexity API requests'); // Create a textarea element for Perplexity - const perplexityTextArea = document.createElement('textarea'); + const perplexityTextArea = activeDocument.createEl('textarea'); perplexityTextArea.rows = 10; perplexityTextArea.cols = 50; - perplexityTextArea.style.width = '100%'; - perplexityTextArea.style.minHeight = '300px'; - perplexityTextArea.style.fontFamily = 'monospace'; + perplexityTextArea.addClass('perplexed-json-textarea'); perplexityTextArea.placeholder = 'Enter Perplexity JSON request template...'; // Set initial value if it exists if (this.plugin.settings.perplexityRequestTemplate) { try { - const config = JSON.parse(this.plugin.settings.perplexityRequestTemplate); + const config: unknown = JSON.parse(this.plugin.settings.perplexityRequestTemplate); perplexityTextArea.value = JSON.stringify(config, null, 2); } catch (e) { // If not valid JSON, use as is @@ -1094,27 +1089,26 @@ class PerplexedSettingTab extends PluginSettingTab { } // Add input event listener for Perplexity - perplexityTextArea.addEventListener('input', async () => { + perplexityTextArea.addEventListener('input', () => void (async () => { this.plugin.settings.perplexityRequestTemplate = perplexityTextArea.value; await this.plugin.saveSettings(); - }); + })()); // Add the textarea to the setting perplexityJsonSetting.settingEl.appendChild(perplexityTextArea); // Claude (Anthropic) Section - const claudeHeader = containerEl.createEl('h3', { text: 'Claude (Anthropic)' }); - claudeHeader.style.color = 'var(--text-accent)'; + new Setting(containerEl).setName("Claude (Anthropic)").setHeading(); containerEl.createEl('p', { text: 'Configure Claude API access. Web-search citations supported in this iteration; document-grounded citations are deferred.', cls: 'setting-item-description' }); new Setting(containerEl) - .setName('Anthropic API Key') + .setName('Anthropic API key') .setDesc('Your Anthropic API key. Read from ANTHROPIC_API_KEY in .env if set; can be overridden here.') .addText(text => text - .setPlaceholder('sk-ant-...') + .setPlaceholder('Sk-ant-...') .setValue(this.plugin.settings.anthropicApiKey) .onChange(async (value) => { this.plugin.settings.anthropicApiKey = value; @@ -1122,13 +1116,13 @@ class PerplexedSettingTab extends PluginSettingTab { })); new Setting(containerEl) - .setName('Default Claude Model') - .setDesc('Default model used by the Ask Claude command. Recommended: claude-opus-4-7.') + .setName('Default Claude model') + .setDesc('Default model used by the ask Claude command. Recommended: Claude-opus-4-7.') .addDropdown(dropdown => dropdown - .addOption('claude-opus-4-7', 'claude-opus-4-7 (recommended)') - .addOption('claude-opus-4-6', 'claude-opus-4-6') - .addOption('claude-sonnet-4-6', 'claude-sonnet-4-6') - .addOption('claude-haiku-4-5', 'claude-haiku-4-5') + .addOption('claude-opus-4-7', 'Claude-opus-4-7 (recommended)') + .addOption('claude-opus-4-6', 'Claude-opus-4-6') + .addOption('claude-sonnet-4-6', 'Claude-sonnet-4-6') + .addOption('claude-haiku-4-5', 'Claude-haiku-4-5') .setValue(this.plugin.settings.claudeDefaultModel) .onChange(async (value) => { this.plugin.settings.claudeDefaultModel = value; @@ -1136,8 +1130,7 @@ class PerplexedSettingTab extends PluginSettingTab { })); // Perplexica / Vane Section - const perplexicaHeader = containerEl.createEl('h3', { text: 'Perplexica / Vane (self-hosted)' }); - perplexicaHeader.style.color = 'var(--text-accent)'; + new Setting(containerEl).setName("Perplexica / Vane (self-hosted)").setHeading(); containerEl.createEl('p', { text: 'Configure settings for your local Perplexica / Vane installation', cls: 'setting-item-description' @@ -1147,7 +1140,7 @@ class PerplexedSettingTab extends PluginSettingTab { .setName('Endpoint') .setDesc('API endpoint for your local Perplexica / Vane instance') .addText(text => text - .setPlaceholder('http://localhost:3030/api/search') + .setPlaceholder('HTTP://localhost:3030/API/search') .setValue(this.plugin.settings.perplexicaEndpoint) .onChange(async (value: string) => { this.plugin.settings.perplexicaEndpoint = value; @@ -1156,10 +1149,10 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('Fallback Container Path') - .setDesc('Alternative endpoint for Docker container setups') + .setName('Fallback container path') + .setDesc('Alternative endpoint for docker container setups') .addText(text => text - .setPlaceholder('http://host.docker.internal:3030/api/search') + .setPlaceholder('HTTP://host.docker.internal:3030/API/search') .setValue(this.plugin.settings.localLLMPath) .onChange(async (value: string) => { this.plugin.settings.localLLMPath = value; @@ -1168,10 +1161,10 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('Default Model') + .setName('Default model') .setDesc('Default AI model for Perplexica / Vane to use') .addText(text => text - .setPlaceholder('llama3.2:latest') + .setPlaceholder('Llama3.2:latest') .setValue(this.plugin.settings.defaultModel) .onChange(async (value: string) => { this.plugin.settings.defaultModel = value; @@ -1181,22 +1174,20 @@ class PerplexedSettingTab extends PluginSettingTab { // Perplexica Request Template const perplexicaJsonSetting = new Setting(containerEl) - .setName('Request Body Template') + .setName('Request body template') .setDesc('JSON template for Perplexica / Vane API requests'); // Create a textarea element for Perplexica - const perplexicaTextArea = document.createElement('textarea'); + const perplexicaTextArea = activeDocument.createEl('textarea'); perplexicaTextArea.rows = 10; perplexicaTextArea.cols = 50; - perplexicaTextArea.style.width = '100%'; - perplexicaTextArea.style.minHeight = '300px'; - perplexicaTextArea.style.fontFamily = 'monospace'; + perplexicaTextArea.addClass('perplexed-json-textarea'); perplexicaTextArea.placeholder = 'Enter Perplexica JSON request template...'; // Set initial value if it exists if (this.plugin.settings.requestBodyTemplate) { try { - const config = JSON.parse(this.plugin.settings.requestBodyTemplate); + const config: unknown = JSON.parse(this.plugin.settings.requestBodyTemplate); perplexicaTextArea.value = JSON.stringify(config, null, 2); } catch (e) { // If not valid JSON, use as is @@ -1205,17 +1196,16 @@ class PerplexedSettingTab extends PluginSettingTab { } // Add input event listener for Perplexica - perplexicaTextArea.addEventListener('input', async () => { + perplexicaTextArea.addEventListener('input', () => void (async () => { this.plugin.settings.requestBodyTemplate = perplexicaTextArea.value; await this.plugin.saveSettings(); - }); + })()); // Add the textarea to the setting perplexicaJsonSetting.settingEl.appendChild(perplexicaTextArea); // LM Studio Section - const lmStudioHeader = containerEl.createEl('h3', { text: 'LM Studio (Local Models)' }); - lmStudioHeader.style.color = 'var(--text-accent)'; + new Setting(containerEl).setName("LM Studio (local models)").setHeading(); containerEl.createEl('p', { text: 'Configure settings for your local LM Studio installation with loaded models', cls: 'setting-item-description' @@ -1225,7 +1215,7 @@ class PerplexedSettingTab extends PluginSettingTab { .setName('Endpoint') .setDesc('API endpoint for your local LM Studio instance') .addText(text => text - .setPlaceholder('http://localhost:1234/v1/chat/completions') + .setPlaceholder('HTTP://localhost:1234/v1/chat/completions') .setValue(this.plugin.settings.lmStudioEndpoint) .onChange(async (value: string) => { this.plugin.settings.lmStudioEndpoint = value; @@ -1234,10 +1224,10 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('Default Model') + .setName('Default model') .setDesc('Default model name for LM Studio to use') .addText(text => text - .setPlaceholder('ibm/granite-3.2-8b') + .setPlaceholder('Ibm/granite-3.2-8b') .setValue(this.plugin.settings.defaultLMStudioModel) .onChange(async (value: string) => { this.plugin.settings.defaultLMStudioModel = value; @@ -1247,22 +1237,20 @@ class PerplexedSettingTab extends PluginSettingTab { // LM Studio Request Template const lmStudioJsonSetting = new Setting(containerEl) - .setName('Request Body Template') + .setName('Request body template') .setDesc('JSON template for LM Studio API requests'); // Create a textarea element for LM Studio - const lmStudioTextArea = document.createElement('textarea'); + const lmStudioTextArea = activeDocument.createEl('textarea'); lmStudioTextArea.rows = 10; lmStudioTextArea.cols = 50; - lmStudioTextArea.style.width = '100%'; - lmStudioTextArea.style.minHeight = '300px'; - lmStudioTextArea.style.fontFamily = 'monospace'; + lmStudioTextArea.addClass('perplexed-json-textarea'); lmStudioTextArea.placeholder = 'Enter LM Studio JSON request template...'; // Set initial value if it exists if (this.plugin.settings.lmStudioRequestTemplate) { try { - const config = JSON.parse(this.plugin.settings.lmStudioRequestTemplate); + const config: unknown = JSON.parse(this.plugin.settings.lmStudioRequestTemplate); lmStudioTextArea.value = JSON.stringify(config, null, 2); } catch (e) { // If not valid JSON, use as is @@ -1271,27 +1259,26 @@ class PerplexedSettingTab extends PluginSettingTab { } // Add input event listener for LM Studio - lmStudioTextArea.addEventListener('input', async () => { + lmStudioTextArea.addEventListener('input', () => void (async () => { this.plugin.settings.lmStudioRequestTemplate = lmStudioTextArea.value; await this.plugin.saveSettings(); - }); + })()); // Add the textarea to the setting lmStudioJsonSetting.settingEl.appendChild(lmStudioTextArea); // Prompts Section - const promptsHeader = containerEl.createEl('h3', { text: 'Prompts & Text Configuration' }); - promptsHeader.style.color = 'var(--text-accent)'; + new Setting(containerEl).setName("Prompts & text configuration").setHeading(); containerEl.createEl('p', { text: 'Customize all prompts, placeholders, descriptions, and messages used throughout the plugin', cls: 'setting-item-description' }); // System Prompts - containerEl.createEl('h4', { text: 'System Prompts' }); + new Setting(containerEl).setName("System prompts").setHeading(); new Setting(containerEl) - .setName('Perplexity System Prompt') + .setName('Perplexity system prompt') .setDesc('System prompt used for Perplexity AI requests') .addTextArea(text => text .setPlaceholder('Enter system prompt for Perplexity...') @@ -1307,7 +1294,7 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('Perplexica / Vane System Prompt') + .setName('Perplexica / Vane system prompt') .setDesc('System prompt used for Perplexica / Vane requests') .addTextArea(text => text .setPlaceholder('Enter system prompt for Perplexica / Vane...') @@ -1323,7 +1310,7 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('LM Studio Default System Prompt') + .setName('LM Studio default system prompt') .setDesc('Default system prompt used for LM Studio requests') .addTextArea(text => text .setPlaceholder('Enter default system prompt for LM Studio...') @@ -1339,10 +1326,10 @@ class PerplexedSettingTab extends PluginSettingTab { ); // Placeholder Text - containerEl.createEl('h4', { text: 'Placeholder Text' }); + new Setting(containerEl).setName("Placeholder text").setHeading(); new Setting(containerEl) - .setName('Perplexity Query Placeholder') + .setName('Perplexity query placeholder') .setDesc('Placeholder text for Perplexity query input') .addText(text => text .setPlaceholder('Enter placeholder text...') @@ -1358,7 +1345,7 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('Perplexica / Vane Query Placeholder') + .setName('Perplexica / Vane query placeholder') .setDesc('Placeholder text for Perplexica / Vane query input') .addText(text => text .setPlaceholder('Enter placeholder text...') @@ -1374,7 +1361,7 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('LM Studio Query Placeholder') + .setName('LM Studio query placeholder') .setDesc('Placeholder text for LM Studio query input') .addText(text => text .setPlaceholder('Enter placeholder text...') @@ -1390,7 +1377,7 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('LM Studio System Prompt Placeholder') + .setName('LM Studio system prompt placeholder') .setDesc('Placeholder text for LM Studio system prompt input') .addText(text => text .setPlaceholder('Enter placeholder text...') @@ -1406,7 +1393,7 @@ class PerplexedSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setName('Article Term Placeholder') + .setName('Article term placeholder') .setDesc('Placeholder text for article generator term input') .addText(text => text .setPlaceholder('Enter placeholder text...') @@ -1419,118 +1406,108 @@ class PerplexedSettingTab extends PluginSettingTab { ); // Article Generator Template - containerEl.createEl('h4', { text: 'Article Generator Template' }); + new Setting(containerEl).setName("Article generator template").setHeading(); const articleTemplateSetting = new Setting(containerEl) - .setName('Article Generator Template') + .setName('Article generator template') .setDesc('Template for generating articles. Use {TERM} as placeholder for the term.'); - const articleTemplateTextArea = document.createElement('textarea'); + const articleTemplateTextArea = activeDocument.createEl('textarea'); articleTemplateTextArea.rows = 15; articleTemplateTextArea.cols = 50; - articleTemplateTextArea.style.width = '100%'; - articleTemplateTextArea.style.minHeight = '400px'; - articleTemplateTextArea.style.fontFamily = 'monospace'; + articleTemplateTextArea.addClass('perplexed-json-textarea is-tall'); articleTemplateTextArea.placeholder = 'Enter article generator template...'; articleTemplateTextArea.value = this.plugin.settings.prompts.articleGeneratorTemplate; - articleTemplateTextArea.addEventListener('input', async () => { + articleTemplateTextArea.addEventListener('input', () => void (async () => { this.plugin.settings.prompts.articleGeneratorTemplate = articleTemplateTextArea.value; this.updatePromptsService(); await this.plugin.saveSettings(); - }); + })()); articleTemplateSetting.settingEl.appendChild(articleTemplateTextArea); // Deep Research Article Generator Template const deepResearchTemplateSetting = new Setting(containerEl) - .setName('Deep Research Article Generator Template') + .setName('Deep research article generator template') .setDesc('Template for generating articles with Deep Research model. Use {TERM} as placeholder for the term.'); - const deepResearchTemplateTextArea = document.createElement('textarea'); + const deepResearchTemplateTextArea = activeDocument.createEl('textarea'); deepResearchTemplateTextArea.rows = 20; deepResearchTemplateTextArea.cols = 50; - deepResearchTemplateTextArea.style.width = '100%'; - deepResearchTemplateTextArea.style.minHeight = '500px'; - deepResearchTemplateTextArea.style.fontFamily = 'monospace'; - deepResearchTemplateTextArea.placeholder = 'Enter Deep Research article generator template...'; + deepResearchTemplateTextArea.addClass('perplexed-json-textarea is-tall'); + deepResearchTemplateTextArea.placeholder = 'Enter deep research article generator template...'; deepResearchTemplateTextArea.value = this.plugin.settings.prompts.deepResearchArticleTemplate; - deepResearchTemplateTextArea.addEventListener('input', async () => { + deepResearchTemplateTextArea.addEventListener('input', () => void (async () => { this.plugin.settings.prompts.deepResearchArticleTemplate = deepResearchTemplateTextArea.value; this.updatePromptsService(); await this.plugin.saveSettings(); - }); + })()); deepResearchTemplateSetting.settingEl.appendChild(deepResearchTemplateTextArea); // Image Prompts - containerEl.createEl('h4', { text: 'Image Prompts' }); + new Setting(containerEl).setName("Image prompts").setHeading(); const imagePromptsSetting = new Setting(containerEl) - .setName('Image References Prompt') + .setName('Image references prompt') .setDesc('Prompt added to queries when images are enabled'); - const imagePromptsTextArea = document.createElement('textarea'); + const imagePromptsTextArea = activeDocument.createEl('textarea'); imagePromptsTextArea.rows = 8; imagePromptsTextArea.cols = 50; - imagePromptsTextArea.style.width = '100%'; - imagePromptsTextArea.style.minHeight = '200px'; - imagePromptsTextArea.style.fontFamily = 'monospace'; + imagePromptsTextArea.addClass('perplexed-json-textarea'); imagePromptsTextArea.placeholder = 'Enter image references prompt...'; imagePromptsTextArea.value = this.plugin.settings.prompts.imageReferencesPrompt; - imagePromptsTextArea.addEventListener('input', async () => { + imagePromptsTextArea.addEventListener('input', () => void (async () => { this.plugin.settings.prompts.imageReferencesPrompt = imagePromptsTextArea.value; this.updatePromptsService(); await this.plugin.saveSettings(); - }); + })()); imagePromptsSetting.settingEl.appendChild(imagePromptsTextArea); // Text Enhancement Prompt - containerEl.createEl('h4', { text: 'Text Enhancement' }); + new Setting(containerEl).setName("Text enhancement").setHeading(); const enhancePromptSetting = new Setting(containerEl) - .setName('Text Enhancement Prompt') + .setName('Text enhancement prompt') .setDesc('Template for enhancing selected text. Use {TEXT} as placeholder for the selected text.'); - const enhancePromptTextArea = document.createElement('textarea'); + const enhancePromptTextArea = activeDocument.createEl('textarea'); enhancePromptTextArea.rows = 10; enhancePromptTextArea.cols = 50; - enhancePromptTextArea.style.width = '100%'; - enhancePromptTextArea.style.minHeight = '250px'; - enhancePromptTextArea.style.fontFamily = 'monospace'; + enhancePromptTextArea.addClass('perplexed-json-textarea'); enhancePromptTextArea.placeholder = 'Enter text enhancement prompt template...'; enhancePromptTextArea.value = this.plugin.settings.prompts.enhancePrompt; - enhancePromptTextArea.addEventListener('input', async () => { + enhancePromptTextArea.addEventListener('input', () => void (async () => { this.plugin.settings.prompts.enhancePrompt = enhancePromptTextArea.value; this.updatePromptsService(); await this.plugin.saveSettings(); - }); + })()); enhancePromptSetting.settingEl.appendChild(enhancePromptTextArea); // Text Enhancement with Images Prompt const enhanceWithImagesPromptSetting = new Setting(containerEl) - .setName('Related Images Prompt') + .setName('Related images prompt') .setDesc('Template for requesting related images for selected text. Use {TEXT} as placeholder for the selected text.'); - const enhanceWithImagesPromptTextArea = document.createElement('textarea'); + const enhanceWithImagesPromptTextArea = activeDocument.createEl('textarea'); enhanceWithImagesPromptTextArea.rows = 10; enhanceWithImagesPromptTextArea.cols = 50; - enhanceWithImagesPromptTextArea.style.width = '100%'; - enhanceWithImagesPromptTextArea.style.minHeight = '250px'; - enhanceWithImagesPromptTextArea.style.fontFamily = 'monospace'; + enhanceWithImagesPromptTextArea.addClass('perplexed-json-textarea'); enhanceWithImagesPromptTextArea.placeholder = 'Enter related images prompt template...'; enhanceWithImagesPromptTextArea.value = this.plugin.settings.prompts.enhanceWithImagesPrompt; - enhanceWithImagesPromptTextArea.addEventListener('input', async () => { + enhanceWithImagesPromptTextArea.addEventListener('input', () => void (async () => { this.plugin.settings.prompts.enhanceWithImagesPrompt = enhanceWithImagesPromptTextArea.value; this.updatePromptsService(); await this.plugin.saveSettings(); - }); + })()); enhanceWithImagesPromptSetting.settingEl.appendChild(enhanceWithImagesPromptTextArea); } diff --git a/package.json b/package.json index c88adaa..f8c88f2 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,9 @@ "@types/node": "^25.6.0", "@typescript-eslint/eslint-plugin": "8.59.1", "@typescript-eslint/parser": "8.59.1", - "builtin-modules": "5.1.0", "esbuild": "0.28.0", "eslint": "^10.3.0", + "eslint-plugin-obsidianmd": "^0.2.9", "globals": "^17.6.0", "obsidian": "latest", "tslib": "2.8.1", @@ -37,7 +37,6 @@ "typescript-eslint": "^8.59.1" }, "dependencies": { - "@anthropic-ai/sdk": "^0.92.0", - "dotenv": "^17.4.2" + "@anthropic-ai/sdk": "^0.92.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92eec92..a8c658d 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ diff --git a/src/modals/ArticleGeneratorModal.ts b/src/modals/ArticleGeneratorModal.ts index 7a2f84d..7d436fe 100644 --- a/src/modals/ArticleGeneratorModal.ts +++ b/src/modals/ArticleGeneratorModal.ts @@ -67,7 +67,7 @@ export class ArticleGeneratorModal extends Modal { // ----- Header ----- const header = contentEl.createDiv({ cls: 'article-generator-modal__header' }); - header.createEl('h2', { text: 'Generate One-Page Article', cls: 'article-generator-modal__title' }); + header.createEl('h2', { text: 'Generate one-page article', cls: 'article-generator-modal__title' }); header.createEl('p', { cls: 'article-generator-modal__subtitle', text: 'Generates a structured article from a single vocabulary term. Streams into the active note.', @@ -76,7 +76,7 @@ export class ArticleGeneratorModal extends Modal { // ----- Term ----- const termSection = contentEl.createDiv({ cls: 'article-generator-modal__section' }); termSection.createEl('label', { - text: 'Vocabulary Term', + text: 'Vocabulary term', cls: 'article-generator-modal__label', attr: { for: 'article-generator-modal-term' }, }); @@ -111,7 +111,7 @@ export class ArticleGeneratorModal extends Modal { .setName('Model') .setDesc(this.modelTagline(this.model)) .addDropdown(dd => { - PERPLEXITY_MODELS.forEach(({ value, label }) => dd.addOption(value, label)); + PERPLEXITY_MODELS.forEach(({ value, label }) => { dd.addOption(value, label); }); dd.setValue(this.model); dd.onChange((value) => { this.model = value; @@ -124,10 +124,10 @@ export class ArticleGeneratorModal extends Modal { this.applyModelChange(this.model); new Setting(modelSection) - .setName('Recency Filter') + .setName('Recency filter') .setDesc('Restrict search to recent content. Multi-year options fall back to "year" (the API ceiling).') .addDropdown(dd => { - RECENCY_OPTIONS.forEach(({ value, label }) => dd.addOption(value, label)); + RECENCY_OPTIONS.forEach(({ value, label }) => { dd.addOption(value, label); }); dd.setValue(this.recencyFilter); dd.onChange((value) => { this.recencyFilter = value; @@ -140,7 +140,7 @@ export class ArticleGeneratorModal extends Modal { new Setting(returnsSection) .setName('Citations') - .setDesc('Append a Citations section with source links — recommended for research articles.') + .setDesc('Append a citations section with source links — recommended for research articles.') .addToggle(t => t .setValue(this.citations) .onChange(v => { this.citations = v; })); @@ -161,7 +161,7 @@ export class ArticleGeneratorModal extends Modal { this.updateCompatibilityWarning(); new Setting(returnsSection) - .setName('Related Questions') + .setName('Related questions') .setDesc('Surface follow-up questions Perplexity suggests at the end of the response.') .addToggle(t => t .setValue(this.relatedQuestions) @@ -172,8 +172,8 @@ export class ArticleGeneratorModal extends Modal { behaviorSection.createEl('h3', { text: 'Behavior', cls: 'article-generator-modal__section-title' }); new Setting(behaviorSection) - .setName('Stream Response') - .setDesc('Recommended for articles — see content as it generates. Note: Deep Research with streaming may not support images.') + .setName('Stream response') + .setDesc('Recommended for articles — see content as it generates. Note: Deep research with streaming may not support images.') .addToggle(t => t .setValue(this.stream) .onChange(v => { this.stream = v; })); @@ -187,12 +187,12 @@ export class ArticleGeneratorModal extends Modal { cancelBtn.addEventListener('click', () => this.close()); const generateBtn = footer.createEl('button', { - text: 'Generate Article', + text: 'Generate article', cls: 'article-generator-modal__button mod-cta', }); generateBtn.addEventListener('click', () => void this.onSubmit()); - setTimeout(() => termInput.focus(), 50); + activeWindow.setTimeout(() => termInput.focus(), 50); } private modelTagline(value: string): string { @@ -222,7 +222,7 @@ export class ArticleGeneratorModal extends Modal { const incompatible = this.images && this.model === 'sonar-deep-research'; if (incompatible) { this.compatibilityWarningEl.textContent = - '⚠ Images are unstable in Deep Research mode. Consider a different model for reliable image support.'; + '⚠ Images are unstable in deep research mode. Consider a different model for reliable image support.'; this.compatibilityWarningEl.addClass('is-active'); } else { this.compatibilityWarningEl.textContent = ''; @@ -261,7 +261,7 @@ export class ArticleGeneratorModal extends Modal { // Resolve quickly so the API request can start; PerplexityService clears // the text + interval when the first content chunk arrives. return new Promise((resolve) => { - setTimeout(() => resolve(), 100); + activeWindow.setTimeout(() => resolve(), 100); }); } diff --git a/src/modals/ClaudeModal.ts b/src/modals/ClaudeModal.ts index b21efa1..285cd30 100644 --- a/src/modals/ClaudeModal.ts +++ b/src/modals/ClaudeModal.ts @@ -71,7 +71,7 @@ export class ClaudeModal extends Modal { attr: { id: 'claude-modal-query', rows: '6', - placeholder: 'What would you like to research? Multi-line OK.', + placeholder: 'What would you like to research? Multi-line ok.', }, }); queryTextarea.value = this.query; @@ -87,7 +87,7 @@ export class ClaudeModal extends Modal { .setName('Model') .setDesc(this.modelTagline(this.model)) .addDropdown(dd => { - CLAUDE_MODELS.forEach(({ value, label }) => dd.addOption(value, label)); + CLAUDE_MODELS.forEach(({ value, label }) => { dd.addOption(value, label); }); dd.setValue(this.model); dd.onChange((value) => { this.model = value; @@ -103,7 +103,7 @@ export class ClaudeModal extends Modal { .setName('Effort') .setDesc('Controls thinking depth and overall token spend. Higher = more thorough; more tokens.') .addDropdown(dd => { - EFFORT_OPTIONS.forEach(({ value, label }) => dd.addOption(value, label)); + EFFORT_OPTIONS.forEach(({ value, label }) => { dd.addOption(value, label); }); dd.setValue(this.effort); dd.onChange((value) => { this.effort = value as NonNullable; @@ -115,21 +115,21 @@ export class ClaudeModal extends Modal { togglesSection.createEl('h3', { text: 'Behavior', cls: 'claude-modal__section-title' }); new Setting(togglesSection) - .setName('Enable Web Search') + .setName('Enable web search') .setDesc('Server-side web_search_20250305 tool. Returns per-claim citations attached to text blocks.') .addToggle(t => t .setValue(this.webSearch) .onChange(v => { this.webSearch = v; })); new Setting(togglesSection) - .setName('Adaptive Thinking') + .setName('Adaptive thinking') .setDesc('Lets Claude reason before answering. Higher quality on complex questions; adds latency and tokens.') .addToggle(t => t .setValue(this.thinking) .onChange(v => { this.thinking = v; })); new Setting(togglesSection) - .setName('Stream Response') + .setName('Stream response') .setDesc('Recommended for long answers — avoids HTTP timeouts and writes incrementally to the note.') .addToggle(t => t .setValue(this.stream) @@ -158,7 +158,7 @@ export class ClaudeModal extends Modal { }); // Focus the question after the DOM has settled - setTimeout(() => queryTextarea.focus(), 50); + activeWindow.setTimeout(() => queryTextarea.focus(), 50); } private modelTagline(value: string): string { diff --git a/src/modals/LMStudioModal.ts b/src/modals/LMStudioModal.ts index 0235452..5b46246 100644 --- a/src/modals/LMStudioModal.ts +++ b/src/modals/LMStudioModal.ts @@ -89,7 +89,7 @@ export class LMStudioModal extends Modal { .setName('Model') .setDesc(this.modelTagline(this.model)) .addDropdown(dd => { - LMSTUDIO_MODELS.forEach(({ value, label }) => dd.addOption(value, label)); + LMSTUDIO_MODELS.forEach(({ value, label }) => { dd.addOption(value, label); }); dd.setValue(this.model); dd.onChange((value) => { this.model = value; @@ -99,7 +99,7 @@ export class LMStudioModal extends Modal { this.modelDescEl = modelSetting.descEl; new Setting(modelSection) - .setName('Max Tokens') + .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)) @@ -119,7 +119,7 @@ export class LMStudioModal extends Modal { // ----- 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('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.', @@ -152,7 +152,7 @@ export class LMStudioModal extends Modal { behaviorSection.createEl('h3', { text: 'Behavior', cls: 'lmstudio-modal__section-title' }); new Setting(behaviorSection) - .setName('Stream Response') + .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) @@ -172,7 +172,7 @@ export class LMStudioModal extends Modal { }); askBtn.addEventListener('click', () => void this.onSubmit()); - setTimeout(() => queryTextarea.focus(), 50); + activeWindow.setTimeout(() => queryTextarea.focus(), 50); } private modelTagline(value: string): string { diff --git a/src/modals/PerplexicaModal.ts b/src/modals/PerplexicaModal.ts index bb863bf..4b4acdb 100644 --- a/src/modals/PerplexicaModal.ts +++ b/src/modals/PerplexicaModal.ts @@ -95,10 +95,10 @@ export class PerplexicaModal extends Modal { searchSection.createEl('h3', { text: 'Search', cls: 'perplexica-modal__section-title' }); const focusSetting = new Setting(searchSection) - .setName('Focus Mode') + .setName('Focus mode') .setDesc(this.focusTagline(this.focusMode)) .addDropdown(dd => { - FOCUS_MODES.forEach(({ value, label }) => dd.addOption(value, label)); + FOCUS_MODES.forEach(({ value, label }) => { dd.addOption(value, label); }); dd.setValue(this.focusMode); dd.onChange((value) => { this.focusMode = value; @@ -111,7 +111,7 @@ export class PerplexicaModal extends Modal { .setName('Optimization') .setDesc(this.optimizationTagline(this.optimization)) .addDropdown(dd => { - OPTIMIZATION_MODES.forEach(({ value, label }) => dd.addOption(value, label)); + OPTIMIZATION_MODES.forEach(({ value, label }) => { dd.addOption(value, label); }); dd.setValue(this.optimization); dd.onChange((value) => { this.optimization = value; @@ -136,7 +136,7 @@ export class PerplexicaModal extends Modal { behaviorSection.createEl('h3', { text: 'Behavior', cls: 'perplexica-modal__section-title' }); new Setting(behaviorSection) - .setName('Stream Response') + .setName('Stream response') .setDesc('Write tokens into the note as they arrive — useful for long answers and slow local models.') .addToggle(t => t .setValue(this.stream) @@ -157,7 +157,7 @@ export class PerplexicaModal extends Modal { askBtn.addEventListener('click', () => void this.onSubmit()); // Focus the question after the DOM has settled - setTimeout(() => queryTextarea.focus(), 50); + activeWindow.setTimeout(() => queryTextarea.focus(), 50); } private focusTagline(value: string): string { diff --git a/src/modals/PerplexityModal.ts b/src/modals/PerplexityModal.ts index 7165906..d95a237 100644 --- a/src/modals/PerplexityModal.ts +++ b/src/modals/PerplexityModal.ts @@ -103,7 +103,7 @@ export class PerplexityModal extends Modal { .setName('Model') .setDesc(this.modelTagline(this.model)) .addDropdown(dd => { - PERPLEXITY_MODELS.forEach(({ value, label }) => dd.addOption(value, label)); + PERPLEXITY_MODELS.forEach(({ value, label }) => { dd.addOption(value, label); }); dd.setValue(this.model); dd.onChange((value) => { this.model = value; @@ -115,10 +115,10 @@ export class PerplexityModal extends Modal { this.modelDescEl = modelSection.querySelector('.setting-item-description'); new Setting(modelSection) - .setName('Recency Filter') + .setName('Recency filter') .setDesc('Restrict search to recent content. Multi-year options fall back to "year" (the API ceiling).') .addDropdown(dd => { - RECENCY_OPTIONS.forEach(({ value, label }) => dd.addOption(value, label)); + RECENCY_OPTIONS.forEach(({ value, label }) => { dd.addOption(value, label); }); dd.setValue(this.recencyFilter); dd.onChange((value) => { this.recencyFilter = value; @@ -131,7 +131,7 @@ export class PerplexityModal extends Modal { new Setting(returnsSection) .setName('Citations') - .setDesc('Append a Citations section with source links — recommended for research notes.') + .setDesc('Append a citations section with source links — recommended for research notes.') .addToggle(t => t .setValue(this.citations) .onChange(v => { this.citations = v; })); @@ -144,7 +144,7 @@ export class PerplexityModal extends Modal { .onChange(v => { this.images = v; })); new Setting(returnsSection) - .setName('Related Questions') + .setName('Related questions') .setDesc('Surface follow-up questions Perplexity suggests at the end of the response.') .addToggle(t => t .setValue(this.relatedQuestions) @@ -155,8 +155,8 @@ export class PerplexityModal extends Modal { behaviorSection.createEl('h3', { text: 'Behavior', cls: 'perplexity-modal__section-title' }); new Setting(behaviorSection) - .setName('Stream Response') - .setDesc('Recommended for long answers. Deep Research model can take 30–60s — streaming makes progress visible.') + .setName('Stream response') + .setDesc('Recommended for long answers. Deep research model can take 30–60s — streaming makes progress visible.') .addToggle(t => t .setValue(this.stream) .onChange(v => { this.stream = v; })); @@ -176,7 +176,7 @@ export class PerplexityModal extends Modal { askBtn.addEventListener('click', () => void this.onSubmit()); // Focus the question after the DOM has settled - setTimeout(() => queryTextarea.focus(), 50); + activeWindow.setTimeout(() => queryTextarea.focus(), 50); } private modelTagline(value: string): string { diff --git a/src/modals/TextEnhancementModal.ts b/src/modals/TextEnhancementModal.ts index d2d0682..7e6bece 100644 --- a/src/modals/TextEnhancementModal.ts +++ b/src/modals/TextEnhancementModal.ts @@ -34,16 +34,16 @@ export class TextEnhancementModal extends Modal { // ----- 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('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.', + 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', + text: 'Selected text', cls: 'text-enhancement-modal__label', }); const selectedTextarea = selectedSection.createEl('textarea', { @@ -58,7 +58,7 @@ export class TextEnhancementModal extends Modal { // ----- Enhancement Prompt (editable) ----- const promptSection = contentEl.createDiv({ cls: 'text-enhancement-modal__section' }); promptSection.createEl('label', { - text: 'Enhancement Prompt', + text: 'Enhancement prompt', cls: 'text-enhancement-modal__label', attr: { for: 'text-enhancement-modal-prompt' }, }); @@ -94,12 +94,12 @@ export class TextEnhancementModal extends Modal { cancelBtn.addEventListener('click', () => this.close()); this.enhanceBtn = footer.createEl('button', { - text: 'Enhance Text', + text: 'Enhance text', cls: 'text-enhancement-modal__button mod-cta', }); this.enhanceBtn.addEventListener('click', () => void this.onSubmit()); - setTimeout(() => promptTextarea.focus(), 50); + activeWindow.setTimeout(() => promptTextarea.focus(), 50); } private async onSubmit(): Promise { @@ -137,7 +137,7 @@ export class TextEnhancementModal extends Modal { } finally { if (this.enhanceBtn) { this.enhanceBtn.disabled = false; - this.enhanceBtn.textContent = 'Enhance Text'; + this.enhanceBtn.textContent = 'Enhance text'; } } } diff --git a/src/modals/TextEnhancementWithImagesModal.ts b/src/modals/TextEnhancementWithImagesModal.ts index d3a4fbf..9d286cf 100644 --- a/src/modals/TextEnhancementWithImagesModal.ts +++ b/src/modals/TextEnhancementWithImagesModal.ts @@ -35,18 +35,18 @@ export class TextEnhancementWithImagesModal extends Modal { // ----- Header ----- const header = contentEl.createDiv({ cls: 'text-enhancement-with-images-modal__header' }); header.createEl('h2', { - text: 'Get Related Images', + text: 'Get related images', cls: 'text-enhancement-with-images-modal__title', }); 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.', + 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', + text: 'Selected text', cls: 'text-enhancement-with-images-modal__label', }); const selectedTextarea = selectedSection.createEl('textarea', { @@ -61,7 +61,7 @@ export class TextEnhancementWithImagesModal extends Modal { // ----- Image Request Prompt (editable) ----- const promptSection = contentEl.createDiv({ cls: 'text-enhancement-with-images-modal__section' }); promptSection.createEl('label', { - text: 'Image Request Prompt', + text: 'Image request prompt', cls: 'text-enhancement-with-images-modal__label', attr: { for: 'text-enhancement-with-images-modal-prompt' }, }); @@ -97,12 +97,12 @@ export class TextEnhancementWithImagesModal extends Modal { cancelBtn.addEventListener('click', () => this.close()); this.fetchBtn = footer.createEl('button', { - text: 'Get Related Images', + text: 'Get related images', cls: 'text-enhancement-with-images-modal__button mod-cta', }); this.fetchBtn.addEventListener('click', () => void this.onSubmit()); - setTimeout(() => promptTextarea.focus(), 50); + activeWindow.setTimeout(() => promptTextarea.focus(), 50); } private async onSubmit(): Promise { @@ -114,7 +114,7 @@ export class TextEnhancementWithImagesModal extends Modal { try { this.fetchBtn.disabled = true; - this.fetchBtn.textContent = 'Getting Related Images…'; + this.fetchBtn.textContent = 'Getting related images…'; this.close(); @@ -140,7 +140,7 @@ export class TextEnhancementWithImagesModal extends Modal { } finally { if (this.fetchBtn) { this.fetchBtn.disabled = false; - this.fetchBtn.textContent = 'Get Related Images'; + this.fetchBtn.textContent = 'Get related images'; } } } diff --git a/src/modals/URLUpdateModal.ts b/src/modals/URLUpdateModal.ts index 0f3f2af..8a02941 100644 --- a/src/modals/URLUpdateModal.ts +++ b/src/modals/URLUpdateModal.ts @@ -79,7 +79,8 @@ export class URLUpdateModal extends Modal { new Notice(`URL updated to: ${newUrl}`); this.close(); } catch (error) { - new Notice(`Failed to save URL: ${error}`); + const msg = error instanceof Error ? error.message : String(error); + new Notice(`Failed to save URL: ${msg}`); } } } diff --git a/src/services/claudeService.ts b/src/services/claudeService.ts index 946600a..77a78e4 100644 --- a/src/services/claudeService.ts +++ b/src/services/claudeService.ts @@ -240,7 +240,7 @@ export class ClaudeService { const textBlocksWithCitations = message.content .filter((b): b is Anthropic.Messages.TextBlock => b.type === 'text') .filter(b => b.citations && b.citations.length > 0).length; - console.log( + console.debug( `[ClaudeService] extractWebCitations — block types: ${JSON.stringify(blockTypes)}; ` + `text blocks with citations: ${textBlocksWithCitations}; ` + `extracted: ${out.length}` diff --git a/src/services/lmStudioService.ts b/src/services/lmStudioService.ts index d5e35ff..4fab5ff 100644 --- a/src/services/lmStudioService.ts +++ b/src/services/lmStudioService.ts @@ -65,7 +65,7 @@ export class LMStudioService { } if (imageIndex > 0) { - console.log(`🔄 Processed ${imageIndex} image markers in LM Studio content`); + console.debug(`🔄 Processed ${imageIndex} image markers in LM Studio content`); } return content; @@ -82,7 +82,7 @@ export class LMStudioService { // Insert query header at the current cursor position const cursor = editor.getCursor(); - console.log('Initial cursor position:', cursor); + console.debug('Initial cursor position:', cursor); // Process query to handle multi-line content in callout const processedQuery = query.split('\n').map(line => `> ${line}`).join('\n'); @@ -100,7 +100,7 @@ export class LMStudioService { ch: lastLine.length }; - console.log('Response cursor position:', responseCursor); + console.debug('Response cursor position:', responseCursor); try { const messages: ChatMessage[] = []; @@ -153,6 +153,9 @@ export class LMStudioService { }; } + // Streaming uses fetch because Obsidian's requestUrl does not + // support SSE / chunked bodies. Marketplace `/skip` justification. + // eslint-disable-next-line no-restricted-globals const response = await fetch(this.settings.lmStudioEndpoint, { method: 'POST', headers: { @@ -236,7 +239,7 @@ export class LMStudioService { // Scroll to follow the new content editor.scrollIntoView({ from: currentPos, to: currentPos }, true); // Small delay to make scrolling smoother - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise(resolve => activeWindow.setTimeout(resolve, 10)); } } catch (e) { // Ignore JSON parse errors for partial chunks diff --git a/src/services/perplexicaService.ts b/src/services/perplexicaService.ts index 0720f5a..790d141 100644 --- a/src/services/perplexicaService.ts +++ b/src/services/perplexicaService.ts @@ -69,7 +69,7 @@ export class PerplexicaService { } if (imageIndex > 0) { - console.log(`🔄 Processed ${imageIndex} image markers in Perplexica content`); + console.debug(`🔄 Processed ${imageIndex} image markers in Perplexica content`); } return content; @@ -177,6 +177,9 @@ export class PerplexicaService { }; } + // Streaming uses fetch because Obsidian's requestUrl does not + // support SSE / chunked bodies. Marketplace `/skip` justification. + // eslint-disable-next-line no-restricted-globals const response = await fetch(endpoint, { method: 'POST', headers: { @@ -206,7 +209,7 @@ export class PerplexicaService { } // If we get here, all endpoints failed - throw lastError || new Error('All endpoints failed'); + throw lastError instanceof Error ? lastError : new Error('All endpoints failed'); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); @@ -248,7 +251,7 @@ export class PerplexicaService { }; } editor.scrollIntoView({ from: currentPos, to: currentPos }, true); - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise(resolve => activeWindow.setTimeout(resolve, 10)); } } catch (e) { // Ignore JSON parse errors diff --git a/src/services/perplexityService.ts b/src/services/perplexityService.ts index 97b9da2..e24f931 100644 --- a/src/services/perplexityService.ts +++ b/src/services/perplexityService.ts @@ -68,7 +68,7 @@ interface PerplexityStreamChunk { export class PerplexityService { private settings: PerplexitySettings; private promptsService: PromptsService | undefined; - private loadingInterval: ReturnType | null = null; + private loadingInterval: number | null = null; constructor(settings: PerplexitySettings) { this.settings = settings; @@ -101,7 +101,7 @@ export class PerplexityService { private processContentWithImages(content: string, images: PerplexityImage[]): string { if (!images || images.length === 0) return content; - console.log(`🖼️ Processing ${images.length} images for content replacement`); + console.debug(`🖼️ Processing ${images.length} images for content replacement`); let processedContent = content; const imageRegex = /\[IMAGE\s+(\d+):\s*(.*?)\]/gi; @@ -111,7 +111,7 @@ export class PerplexityService { let match; while ((match = imageRegex.exec(content)) !== null) { const [fullMatch, number, description] = match; - console.log(`🔍 Regex match found: "${fullMatch}" with number="${number}" description="${description}"`); + console.debug(`🔍 Regex match found: "${fullMatch}" with number="${number}" description="${description}"`); if (number && description) { matches.push({ fullMatch, @@ -120,16 +120,16 @@ export class PerplexityService { index: match.index }); } else { - console.log(`⚠️ Invalid match - number: "${number}", description: "${description}"`); + console.debug(`⚠️ Invalid match - number: "${number}", description: "${description}"`); } } if (matches.length === 0) { - console.log('🔍 No image markers found in content'); + console.debug('🔍 No image markers found in content'); return content; } - console.log(`🔍 Found ${matches.length} image markers in content`); + console.debug(`🔍 Found ${matches.length} image markers in content`); // Sort matches by their position in the content (descending) to avoid index shifting issues matches.sort((a, b) => b.index - a.index); @@ -138,7 +138,7 @@ export class PerplexityService { // Replace matches from end to beginning to avoid index shifting matches.forEach((matchInfo) => { - console.log(`🔍 Processing image reference: "${matchInfo.fullMatch}" at index ${matchInfo.index}`); + console.debug(`🔍 Processing image reference: "${matchInfo.fullMatch}" at index ${matchInfo.index}`); const imageIndex = parseInt(matchInfo.number) - 1; // Convert 1-based to 0-based const image = images[imageIndex]; @@ -147,13 +147,13 @@ export class PerplexityService { const imageMarkdown = `![${matchInfo.description || 'Image'}](${image.image_url})`; processedContent = processedContent.replace(matchInfo.fullMatch, imageMarkdown); replacedCount++; - console.log(`🔄 Replaced "${matchInfo.fullMatch}" with image markdown`); + console.debug(`🔄 Replaced "${matchInfo.fullMatch}" with image markdown`); } else { - console.log(`⚠️ No image found for index ${imageIndex} or no image_url`); + console.debug(`⚠️ No image found for index ${imageIndex} or no image_url`); } }); - console.log(`✅ Replaced ${replacedCount} image markers with actual images`); + console.debug(`✅ Replaced ${replacedCount} image markers with actual images`); return processedContent; } @@ -194,7 +194,7 @@ export class PerplexityService { // If we found a callout end, return it if (calloutEndIndex > 0) { - console.log(`🔍 Query header ends at position ${calloutEndIndex}`); + console.debug(`🔍 Query header ends at position ${calloutEndIndex}`); return calloutEndIndex; } @@ -210,7 +210,7 @@ export class PerplexityService { const match = content.match(pattern); if (match) { const endIndex = match.index || 0; - console.log(`🔍 Found header end pattern at position ${endIndex}`); + console.debug(`🔍 Found header end pattern at position ${endIndex}`); return endIndex; } } @@ -221,7 +221,7 @@ export class PerplexityService { private processThinkBlocks(content: string): string { if (!content.includes('')) return content; - console.log('🧠 Processing think blocks in content'); + console.debug('🧠 Processing think blocks in content'); // Replace blocks with markdown callouts let processedContent = content; @@ -229,7 +229,7 @@ export class PerplexityService { // Match ... blocks, including nested content const thinkRegex = /([\s\S]*?)<\/think>/gi; - processedContent = processedContent.replace(thinkRegex, (_match, thinkContent) => { + processedContent = processedContent.replace(thinkRegex, (_match: string, thinkContent: string) => { // Clean up the think content - remove extra whitespace and format const cleanedContent = thinkContent .trim() @@ -246,7 +246,7 @@ export class PerplexityService { > --- > *This shows the AI's internal reasoning before generating the response.*`; - console.log('🧠 Converted think block to callout'); + console.debug('🧠 Converted think block to callout'); return callout; }); @@ -265,15 +265,15 @@ export class PerplexityService { if (updatedLines.length !== lines.length) { editor.setValue(updatedLines.join('\n')); - console.log('🧹 Cleared loading text from document'); + console.debug('🧹 Cleared loading text from document'); } } } private clearLoadingAnimation(): void { if (this.loadingInterval) { - console.log('🛑 Clearing loading animation interval'); - clearInterval(this.loadingInterval); + console.debug('🛑 Clearing loading animation interval'); + activeWindow.clearInterval(this.loadingInterval); this.loadingInterval = null; } } @@ -281,7 +281,7 @@ export class PerplexityService { private addCitations(editor: Editor, sources: (string | PerplexitySource)[]): void { if (!sources || sources.length === 0) return; - console.log(`📚 Processing ${sources.length} sources for citations`); + console.debug(`📚 Processing ${sources.length} sources for citations`); // Check if there's already a citations section const content = editor.getValue(); @@ -292,7 +292,7 @@ export class PerplexityService { if (existingCitationsMatch && existingCitationsMatch[1]) { // Found existing citations section existingCitations = existingCitationsMatch[1]; - console.log('📚 Found existing citations section, will append to it'); + console.debug('📚 Found existing citations section, will append to it'); } // Generate new citations @@ -369,14 +369,14 @@ export class PerplexityService { const updatedContent = beforeCitations + updatedCitations + afterCitations; editor.setValue(updatedContent); - console.log('✅ Citations appended to existing section'); + console.debug('✅ Citations appended to existing section'); } else { // Create new citations section at the end const citationsText = '\n\n### Citations\n\n' + newCitationsText; const endOfDoc = editor.lastLine(); const endPos = { line: endOfDoc, ch: editor.getLine(endOfDoc).length }; editor.replaceRange(citationsText, endPos); - console.log('✅ New citations section created'); + console.debug('✅ New citations section created'); } } @@ -387,8 +387,8 @@ export class PerplexityService { editor: Editor, options?: PerplexityOptions ): Promise { - console.log('🚀 PerplexityService.queryPerplexity called'); - console.log('📊 Parameters:', { model, stream, options, queryLength: query.length }); + console.debug('🚀 PerplexityService.queryPerplexity called'); + console.debug('📊 Parameters:', { model, stream, options, queryLength: query.length }); const timestamp = new Date().toISOString(); @@ -396,11 +396,11 @@ export class PerplexityService { const isDeepResearch = model === 'sonar-deep-research'; const useStreaming = stream; // Allow streaming for all models including deep research - console.log('🔍 Model analysis:', { isDeepResearch, useStreaming }); + console.debug('🔍 Model analysis:', { isDeepResearch, useStreaming }); // Insert query header based on headerPosition setting const cursor = editor.getCursor(); - console.log('Initial cursor position:', cursor); + console.debug('Initial cursor position:', cursor); // Process query to handle multi-line content in callout const processedQuery = query.split('\n').map(line => `> ${line}`).join('\n'); @@ -429,9 +429,9 @@ export class PerplexityService { }; } - console.log('Response cursor position:', responseCursor); - console.log('Header position setting:', this.settings.headerPosition); - console.log('Header text preview:', headerText ? headerText.substring(0, 100) + '...' : 'No header text'); + console.debug('Response cursor position:', responseCursor); + console.debug('Header position setting:', this.settings.headerPosition); + console.debug('Header text preview:', headerText ? headerText.substring(0, 100) + '...' : 'No header text'); // Show loading notice for deep research let loadingNotice: Notice | null = null; @@ -465,7 +465,7 @@ export class PerplexityService { const payloadMatch = cleanedTemplate.match(/payload\s*=\s*({[\s\S]*?});/); if (payloadMatch) { let jsObject = payloadMatch[1]; - console.log('🔍 Extracted payload from JavaScript:', jsObject); + console.debug('🔍 Extracted payload from JavaScript:', jsObject); jsObject = jsObject?.replace(/(\w+):/g, '"$1":').replace(/'/g, '"'); @@ -475,7 +475,7 @@ export class PerplexityService { } } - console.log('🧹 Final cleaned template:', cleanedTemplate); + console.debug('🧹 Final cleaned template:', cleanedTemplate); JSON.parse(cleanedTemplate); payload = { model, @@ -515,12 +515,15 @@ export class PerplexityService { const requestId = Date.now() + Math.random(); // Debug: Log the API payload being sent - console.log(`🚀 Perplexity API Payload [${requestId}]:`, JSON.stringify(payload, null, 2)); + console.debug(`🚀 Perplexity API Payload [${requestId}]:`, JSON.stringify(payload, null, 2)); try { if (useStreaming) { - console.log('🔄 Making streaming API request...'); - // Use fetch for streaming responses with cache busting headers + console.debug('🔄 Making streaming API request...'); + // Streaming uses fetch because Obsidian's requestUrl does not + // support reading SSE / chunked response bodies — it buffers + // the whole response. Marketplace `/skip` justification. + // eslint-disable-next-line no-restricted-globals const response = await fetch(this.settings.perplexityEndpoint, { method: 'POST', headers: { @@ -540,10 +543,10 @@ export class PerplexityService { throw new Error('No response body'); } - console.log('✅ Streaming response received, starting to handle...'); + console.debug('✅ Streaming response received, starting to handle...'); await this.handleStreamingResponse(response, editor, responseCursor, requestId, headerText); } else { - console.log('🔄 Making non-streaming API request...'); + console.debug('🔄 Making non-streaming API request...'); // Use Obsidian's request method for non-streaming with cache busting const response = await request({ url: this.settings.perplexityEndpoint, @@ -556,17 +559,17 @@ export class PerplexityService { body: JSON.stringify(payload) }); - console.log('✅ Non-streaming response received'); + console.debug('✅ Non-streaming response received'); const data = JSON.parse(response) as PerplexityResponse; - console.log('📊 Response data structure:', Object.keys(data)); + console.debug('📊 Response data structure:', Object.keys(data)); if (data.choices && data.choices.length > 0) { let content = data.choices[0]?.message?.content ?? ''; content = this.processThinkBlocks(content); if (options?.return_images && data.images && data.images.length > 0) { - console.log('🖼️ Processing images in non-streaming response:', data.images.length, 'images found'); + console.debug('🖼️ Processing images in non-streaming response:', data.images.length, 'images found'); content = this.processContentWithImages(content, data.images); } @@ -631,11 +634,11 @@ export class PerplexityService { requestId?: number, headerText?: string ): Promise { - console.log('🔄 handleStreamingResponse called'); + console.debug('🔄 handleStreamingResponse called'); const reader = response.body?.getReader(); if (!reader) throw new Error('No response body'); - console.log(`🔄 Starting streaming response handler [${requestId || 'unknown'}]`); + console.debug(`🔄 Starting streaming response handler [${requestId || 'unknown'}]`); // Hoist decoder out of the loop so the {stream: true} flag carries // partial multi-byte UTF-8 state across chunk boundaries. @@ -661,7 +664,7 @@ export class PerplexityService { }); }; - console.log(`📍 Initial currentPos:`, currentPos); + console.debug(`📍 Initial currentPos:`, currentPos); try { while (true) { @@ -694,14 +697,14 @@ export class PerplexityService { if (parsed.choices?.[0]?.delta?.content) { const content = parsed.choices[0].delta.content; if (content) { - // console.log('🎉 First content received! Clearing loading text...'); + // console.debug('🎉 First content received! Clearing loading text...'); // Clear any loading text before inserting content this.clearLoadingText(editor); // Clear the animation interval if it exists this.clearLoadingAnimation(); - // console.log(`📝 Inserting content at position:`, currentPos, `Content:`, content.substring(0, 50) + '...'); + // console.debug(`📝 Inserting content at position:`, currentPos, `Content:`, content.substring(0, 50) + '...'); editor.replaceRange(content, currentPos); const contentLines = content.split('\n'); if (contentLines.length === 1) { @@ -722,12 +725,12 @@ export class PerplexityService { } // Small delay to prevent UI blocking - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise(resolve => activeWindow.setTimeout(resolve, 10)); } // Process final metadata (citations, images) after streaming is complete if (finalResponseData) { - console.log(`📝 Processing final response data [${requestId || 'unknown'}]:`, finalResponseData); + console.debug(`📝 Processing final response data [${requestId || 'unknown'}]:`, finalResponseData); await this.processStreamingMetadata(finalResponseData, editor, headerText); } @@ -756,13 +759,13 @@ export class PerplexityService { editor: Editor, headerText?: string ): Promise { - console.log('🔍 Processing streaming metadata'); - console.log('📊 Response data keys:', Object.keys(finalResponseData || {})); + console.debug('🔍 Processing streaming metadata'); + console.debug('📊 Response data keys:', Object.keys(finalResponseData || {})); if (finalResponseData.images) { - console.log(`🖼️ Images array length: ${finalResponseData.images.length}`); + console.debug(`🖼️ Images array length: ${finalResponseData.images.length}`); } if (finalResponseData.search_results) { - console.log(`📚 Search results array length: ${finalResponseData.search_results.length}`); + console.debug(`📚 Search results array length: ${finalResponseData.search_results.length}`); } // Get current content for processing @@ -774,35 +777,35 @@ export class PerplexityService { if (processedThinkContent !== content) { content = processedThinkContent; contentUpdated = true; - console.log('🧠 Think blocks processed in streaming response'); + console.debug('🧠 Think blocks processed in streaming response'); } // Process images with intelligent placement if (finalResponseData.images && finalResponseData.images.length > 0) { - console.log('🖼️ Processing images in streaming response:', finalResponseData.images.length, 'images found'); + console.debug('🖼️ Processing images in streaming response:', finalResponseData.images.length, 'images found'); // Debug: Let's see what image references are in the content const imageRegex = /\[IMAGE\s+(\d+):\s*(.*?)\]/gi; const allMatches = content.match(imageRegex); - console.log('🔍 All image references found in content:', allMatches); + console.debug('🔍 All image references found in content:', allMatches); // Also log a sample of the content to see the format const sampleContent = content.substring(0, Math.min(500, content.length)); - console.log('🔍 Sample content:', sampleContent); + console.debug('🔍 Sample content:', sampleContent); const processedContent = this.processContentWithImages(content, finalResponseData.images); if (processedContent !== content) { content = processedContent; contentUpdated = true; - console.log('🔄 Content updated with inline images (streaming)'); + console.debug('🔄 Content updated with inline images (streaming)'); } else { - console.log('⚠️ No image markers were replaced, falling back to Images section'); + console.debug('⚠️ No image markers were replaced, falling back to Images section'); // Try to find where the response content starts (after the query header) const queryHeaderEnd = this.findQueryHeaderEnd(content); if (queryHeaderEnd > 0) { - console.log(`📍 Found query header end at position ${queryHeaderEnd}, inserting images inline`); + console.debug(`📍 Found query header end at position ${queryHeaderEnd}, inserting images inline`); // Insert images inline after the query header let imagesSection = '\n\n'; @@ -818,7 +821,7 @@ export class PerplexityService { const newContent = beforeHeader + imagesSection + afterHeader; editor.setValue(newContent); contentUpdated = true; - console.log('🔄 Images inserted inline after query header'); + console.debug('🔄 Images inserted inline after query header'); } else { // Fallback: add images at the end if no markers found let imagesSection = '\n\n## Images\n\n'; @@ -840,14 +843,14 @@ export class PerplexityService { } } } else { - console.log('🖼️ No images found in streaming response data'); - console.log('💡 Note: Deep Research with streaming may not support images. Consider using non-streaming mode for image support.'); + console.debug('🖼️ No images found in streaming response data'); + console.debug('💡 Note: Deep Research with streaming may not support images. Consider using non-streaming mode for image support.'); } // Update editor with processed content if any changes were made if (contentUpdated) { editor.setValue(content); - console.log('🔄 Editor updated with processed content (think blocks and/or images)'); + console.debug('🔄 Editor updated with processed content (think blocks and/or images)'); } // Process sources/citations if available @@ -858,19 +861,19 @@ export class PerplexityService { // Fallback to citations array (could be URLs or other format) this.addCitations(editor, finalResponseData.citations); } else { - console.log('📚 No sources/citations found in Perplexity streaming response'); + console.debug('📚 No sources/citations found in Perplexity streaming response'); } // Add header at bottom if that setting is enabled if (this.settings.headerPosition === 'bottom' && headerText) { - console.log('📄 Adding header at bottom of document'); + console.debug('📄 Adding header at bottom of document'); const endOfDoc = editor.lastLine(); const endPos = { line: endOfDoc, ch: editor.getLine(endOfDoc).length }; - console.log('📍 End position for header:', endPos); + console.debug('📍 End position for header:', endPos); editor.replaceRange('\n\n' + headerText, endPos); - console.log('✅ Header added at bottom'); + console.debug('✅ Header added at bottom'); } else { - console.log('📄 Header position setting:', this.settings.headerPosition, 'Header text available:', !!headerText); + console.debug('📄 Header position setting:', this.settings.headerPosition, 'Header text available:', !!headerText); } } } diff --git a/src/styles/main.css b/src/styles/main.css index fd1bf22..d705e9e 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -9,6 +9,7 @@ @import './text-enhancement-modal.css'; @import './text-enhancement-with-images-modal.css'; @import './claude-modal.css'; +@import './settings-tab.css'; /* Custom callout styling for AI reasoning process */ .callout[data-callout="brain"] { diff --git a/src/styles/settings-tab.css b/src/styles/settings-tab.css new file mode 100644 index 0000000..3561630 --- /dev/null +++ b/src/styles/settings-tab.css @@ -0,0 +1,22 @@ +/* Perplexed settings-tab styles + * + * Replaces inline element.style.* assignments that ObsidianReviewBot rejects. + * The textareas used for JSON request templates need a fixed minimum height, + * full container width, and monospace font; that's what these classes do. + */ + +.perplexed-json-textarea { + width: 100%; + min-height: 300px; + font-family: var(--font-monospace); +} + +/* Modifier classes for taller variants — used by the article-generator + * and deep-research template editors which want more vertical room. */ +.perplexed-json-textarea.is-tall { + min-height: 500px; +} + +.perplexed-json-textarea.is-extra-tall { + min-height: 600px; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 329a074..4e5f736 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -41,7 +41,8 @@ export class FileLogger { const file = this.vault.getAbstractFileByPath(this.logFile); if (file instanceof TFile) { const content = await this.vault.read(file); - this.logEntries = JSON.parse(content); + const parsed: unknown = JSON.parse(content); + this.logEntries = Array.isArray(parsed) ? (parsed as LogEntry[]) : []; } } catch (error) { // File doesn't exist or is corrupted, start with empty logs @@ -102,9 +103,19 @@ export class FileLogger { console.error('Failed to save log entry:', error); }); - // Also log to console - const logMethod = console[level] || console.log; - logMethod(`[${entry.timestamp}] [${level.toUpperCase()}] ${message}`, details || ''); + // Also log to console — only allow warn / error / debug per Obsidian + // marketplace rules; map info → debug, default → debug. + const formatted = `[${entry.timestamp}] [${level.toUpperCase()}] ${message}`; + switch (level) { + case 'error': + console.error(formatted, details ?? ''); + break; + case 'warn': + console.warn(formatted, details ?? ''); + break; + default: + console.debug(formatted, details ?? ''); + } } error(message: string, details?: unknown): void { diff --git a/styles.css b/styles.css index 49f2bfd..7a9029f 100644 --- a/styles.css +++ b/styles.css @@ -1 +1 @@ -.perplexity-modal{width:90vw;max-width:640px}.perplexity-modal .modal-content{padding:0}.perplexity-modal__header{padding:24px 28px 16px;border-bottom:1px solid var(--background-modifier-border)}.perplexity-modal__title{margin:0 0 6px;font-size:1.5em;font-weight:600;color:var(--text-normal);letter-spacing:-.01em}.perplexity-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.perplexity-modal__section{padding:18px 28px 4px}.perplexity-modal__section+.perplexity-modal__section{border-top:1px solid var(--background-modifier-border-hover);margin-top:4px}.perplexity-modal__section-title{margin:0 0 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.perplexity-modal__section .setting-item{padding:10px 0;border-top:none}.perplexity-modal__section .setting-item+.setting-item{border-top:1px solid var(--background-modifier-border-hover)}.perplexity-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.perplexity-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 .15s ease,box-shadow .15s ease;box-sizing:border-box}.perplexity-modal__textarea:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.perplexity-modal__textarea::placeholder{color:var(--text-faint)}.perplexity-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)}.perplexity-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 .15s ease,border-color .15s ease,transform .05s ease}.perplexity-modal__button:hover{background-color:var(--background-modifier-hover)}.perplexity-modal__button:active{transform:translateY(1px)}.perplexity-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.perplexity-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.perplexity-modal__button:disabled{opacity:.5;cursor:not-allowed}@media(max-width:600px){.perplexity-modal{width:95vw;max-width:none}.perplexity-modal__header,.perplexity-modal__section,.perplexity-modal__footer{padding-left:18px;padding-right:18px}.perplexity-modal__footer{flex-direction:column-reverse}.perplexity-modal__button{width:100%}}.article-generator-modal{width:90vw;max-width:640px}.article-generator-modal .modal-content{padding:0}.article-generator-modal__header{padding:24px 28px 16px;border-bottom:1px solid var(--background-modifier-border)}.article-generator-modal__title{margin:0 0 6px;font-size:1.5em;font-weight:600;color:var(--text-normal);letter-spacing:-.01em}.article-generator-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.article-generator-modal__section{padding:18px 28px 4px}.article-generator-modal__section+.article-generator-modal__section{border-top:1px solid var(--background-modifier-border-hover);margin-top:4px}.article-generator-modal__section-title{margin:0 0 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.article-generator-modal__section .setting-item{padding:10px 0;border-top:none}.article-generator-modal__section .setting-item+.setting-item{border-top:1px solid var(--background-modifier-border-hover)}.article-generator-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.article-generator-modal__input{width:100%;padding:10px 14px;font-family:var(--font-text);font-size:15px;line-height:1.4;color:var(--text-normal);background-color:var(--background-primary);border:1px solid var(--background-modifier-border);border-radius:8px;transition:border-color .15s ease,box-shadow .15s ease;box-sizing:border-box}.article-generator-modal__input:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.article-generator-modal__input::placeholder{color:var(--text-faint)}.article-generator-modal__hint{margin:8px 0 0;font-size:12px;line-height:1.45;color:var(--text-muted);font-style:italic}.article-generator-modal__warning{margin-top:6px;font-size:12px;line-height:1.4;color:var(--text-error);font-weight:500;display:none}.article-generator-modal__warning.is-active{display:block}.article-generator-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)}.article-generator-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 .15s ease,border-color .15s ease,transform .05s ease}.article-generator-modal__button:hover{background-color:var(--background-modifier-hover)}.article-generator-modal__button:active{transform:translateY(1px)}.article-generator-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.article-generator-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.article-generator-modal__button:disabled{opacity:.5;cursor:not-allowed}@media(max-width:600px){.article-generator-modal{width:95vw;max-width:none}.article-generator-modal__header,.article-generator-modal__section,.article-generator-modal__footer{padding-left:18px;padding-right:18px}.article-generator-modal__footer{flex-direction:column-reverse}.article-generator-modal__button{width:100%}}.perplexica-modal{width:90vw;max-width:640px}.perplexica-modal .modal-content{padding:0}.perplexica-modal__header{padding:24px 28px 16px;border-bottom:1px solid var(--background-modifier-border)}.perplexica-modal__title{margin:0 0 6px;font-size:1.5em;font-weight:600;color:var(--text-normal);letter-spacing:-.01em}.perplexica-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.perplexica-modal__section{padding:18px 28px 4px}.perplexica-modal__section+.perplexica-modal__section{border-top:1px solid var(--background-modifier-border-hover);margin-top:4px}.perplexica-modal__section-title{margin:0 0 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.perplexica-modal__section .setting-item{padding:10px 0;border-top:none}.perplexica-modal__section .setting-item+.setting-item{border-top:1px solid var(--background-modifier-border-hover)}.perplexica-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.perplexica-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 .15s ease,box-shadow .15s ease;box-sizing:border-box}.perplexica-modal__textarea:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.perplexica-modal__textarea::placeholder{color:var(--text-faint)}.perplexica-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)}.perplexica-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 .15s ease,border-color .15s ease,transform .05s ease}.perplexica-modal__button:hover{background-color:var(--background-modifier-hover)}.perplexica-modal__button:active{transform:translateY(1px)}.perplexica-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.perplexica-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.perplexica-modal__button:disabled{opacity:.5;cursor:not-allowed}@media(max-width:600px){.perplexica-modal{width:95vw;max-width:none}.perplexica-modal__header,.perplexica-modal__section,.perplexica-modal__footer{padding-left:18px;padding-right:18px}.perplexica-modal__footer{flex-direction:column-reverse}.perplexica-modal__button{width:100%}}.lmstudio-modal{width:90vw;max-width:640px}.lmstudio-modal .modal-content{padding:0}.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:-.01em}.lmstudio-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.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:.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)}.lmstudio-modal__hint{margin:-4px 0 8px;font-size:12px;line-height:1.45;color:var(--text-muted)}.lmstudio-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.lmstudio-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 .15s ease,box-shadow .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)}.lmstudio-modal__textarea--system{min-height:72px;font-family:var(--font-monospace);font-size:13px}.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 .15s ease,border-color .15s ease,transform .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:.5;cursor:not-allowed}@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%}}.url-update-modal{width:90vw;max-width:500px}.url-update-modal .modal-content{padding:0}.url-update-modal__header{padding:20px 24px 12px;border-bottom:1px solid var(--background-modifier-border)}.url-update-modal__title{margin:0;font-size:1.3em;font-weight:600;color:var(--text-normal)}.url-update-modal__section{padding:18px 24px}.url-update-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.url-update-modal__input{width:100%;padding:10px 12px;font-family:var(--font-text);font-size:14px;color:var(--text-normal);background-color:var(--background-primary);border:1px solid var(--background-modifier-border);border-radius:6px;box-sizing:border-box}.url-update-modal__input:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.url-update-modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:16px 24px;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)}.url-update-modal__button{padding:6px 14px;font-size:13px;font-weight:500;border:1px solid var(--background-modifier-border);border-radius:4px;background-color:var(--background-primary);color:var(--text-normal);cursor:pointer;transition:background-color .15s ease,border-color .15s ease,transform .05s ease}.url-update-modal__button:hover{background-color:var(--background-modifier-hover)}.url-update-modal__button:active{transform:translateY(1px)}.url-update-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.url-update-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}@media(max-width:500px){.url-update-modal{width:95vw;max-width:none}.url-update-modal__header,.url-update-modal__section,.url-update-modal__footer{padding-left:16px;padding-right:16px}.url-update-modal__footer{flex-direction:column-reverse}.url-update-modal__button{width:100%}}.text-enhancement-modal{width:90vw;max-width:720px}.text-enhancement-modal .modal-content{padding:0}.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:-.01em}.text-enhancement-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.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}.text-enhancement-modal__hint{margin:-4px 0 8px;font-size:12px;line-height:1.45;color:var(--text-muted)}.text-enhancement-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.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 .15s ease,box-shadow .15s ease;box-sizing:border-box}.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)}.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}.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:6px;background-color:var(--background-primary);color:var(--text-normal);cursor:pointer;transition:background-color .15s ease,border-color .15s ease,transform .05s ease}.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:.5;cursor:not-allowed}@media(max-width:600px){.text-enhancement-modal{width:95vw;max-width:none}.text-enhancement-modal__header,.text-enhancement-modal__section,.text-enhancement-modal__footer{padding-left:18px;padding-right:18px}.text-enhancement-modal__footer{flex-direction:column-reverse}.text-enhancement-modal__button{width:100%}}.text-enhancement-with-images-modal{width:90vw;max-width:720px}.text-enhancement-with-images-modal .modal-content{padding:0}.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:-.01em}.text-enhancement-with-images-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.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}.text-enhancement-with-images-modal__hint{margin:-4px 0 8px;font-size:12px;line-height:1.45;color:var(--text-muted)}.text-enhancement-with-images-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.text-enhancement-with-images-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 .15s ease,box-shadow .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}.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);cursor:pointer;transition:background-color .15s ease,border-color .15s ease,transform .05s ease}.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)}.text-enhancement-with-images-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.text-enhancement-with-images-modal__button:disabled{opacity:.5;cursor:not-allowed}@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%}}.claude-modal{width:90vw;max-width:640px}.claude-modal .modal-content{padding:0}.claude-modal__header{padding:24px 28px 16px;border-bottom:1px solid var(--background-modifier-border)}.claude-modal__title{margin:0 0 6px;font-size:1.5em;font-weight:600;color:var(--text-normal);letter-spacing:-.01em}.claude-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.claude-modal__section{padding:18px 28px 4px}.claude-modal__section+.claude-modal__section{border-top:1px solid var(--background-modifier-border-hover);margin-top:4px}.claude-modal__section-title{margin:0 0 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.claude-modal__section .setting-item{padding:10px 0;border-top:none}.claude-modal__section .setting-item+.setting-item{border-top:1px solid var(--background-modifier-border-hover)}.claude-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.claude-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 .15s ease,box-shadow .15s ease;box-sizing:border-box}.claude-modal__textarea:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.claude-modal__textarea::placeholder{color:var(--text-faint)}.claude-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)}.claude-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 .15s ease,border-color .15s ease,transform .05s ease}.claude-modal__button:hover{background-color:var(--background-modifier-hover)}.claude-modal__button:active{transform:translateY(1px)}.claude-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.claude-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.claude-modal__button:disabled{opacity:.5;cursor:not-allowed}@media(max-width:600px){.claude-modal{width:95vw;max-width:none}.claude-modal__header,.claude-modal__section,.claude-modal__footer{padding-left:18px;padding-right:18px}.claude-modal__footer{flex-direction:column-reverse}.claude-modal__button{width:100%}}.callout[data-callout=brain]{background-color:#9333ea1a;border-left:4px solid #9333ea}.callout[data-callout=brain] .callout-title{color:#9333ea;font-weight:600}.callout[data-callout=brain] .callout-content{color:var(--text-normal);font-style:normal}.callout[data-callout=brain] .callout-icon{color:#9333ea} +.perplexity-modal{width:90vw;max-width:640px}.perplexity-modal .modal-content{padding:0}.perplexity-modal__header{padding:24px 28px 16px;border-bottom:1px solid var(--background-modifier-border)}.perplexity-modal__title{margin:0 0 6px;font-size:1.5em;font-weight:600;color:var(--text-normal);letter-spacing:-.01em}.perplexity-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.perplexity-modal__section{padding:18px 28px 4px}.perplexity-modal__section+.perplexity-modal__section{border-top:1px solid var(--background-modifier-border-hover);margin-top:4px}.perplexity-modal__section-title{margin:0 0 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.perplexity-modal__section .setting-item{padding:10px 0;border-top:none}.perplexity-modal__section .setting-item+.setting-item{border-top:1px solid var(--background-modifier-border-hover)}.perplexity-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.perplexity-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 .15s ease,box-shadow .15s ease;box-sizing:border-box}.perplexity-modal__textarea:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.perplexity-modal__textarea::placeholder{color:var(--text-faint)}.perplexity-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)}.perplexity-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 .15s ease,border-color .15s ease,transform .05s ease}.perplexity-modal__button:hover{background-color:var(--background-modifier-hover)}.perplexity-modal__button:active{transform:translateY(1px)}.perplexity-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.perplexity-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.perplexity-modal__button:disabled{opacity:.5;cursor:not-allowed}@media(max-width:600px){.perplexity-modal{width:95vw;max-width:none}.perplexity-modal__header,.perplexity-modal__section,.perplexity-modal__footer{padding-left:18px;padding-right:18px}.perplexity-modal__footer{flex-direction:column-reverse}.perplexity-modal__button{width:100%}}.article-generator-modal{width:90vw;max-width:640px}.article-generator-modal .modal-content{padding:0}.article-generator-modal__header{padding:24px 28px 16px;border-bottom:1px solid var(--background-modifier-border)}.article-generator-modal__title{margin:0 0 6px;font-size:1.5em;font-weight:600;color:var(--text-normal);letter-spacing:-.01em}.article-generator-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.article-generator-modal__section{padding:18px 28px 4px}.article-generator-modal__section+.article-generator-modal__section{border-top:1px solid var(--background-modifier-border-hover);margin-top:4px}.article-generator-modal__section-title{margin:0 0 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.article-generator-modal__section .setting-item{padding:10px 0;border-top:none}.article-generator-modal__section .setting-item+.setting-item{border-top:1px solid var(--background-modifier-border-hover)}.article-generator-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.article-generator-modal__input{width:100%;padding:10px 14px;font-family:var(--font-text);font-size:15px;line-height:1.4;color:var(--text-normal);background-color:var(--background-primary);border:1px solid var(--background-modifier-border);border-radius:8px;transition:border-color .15s ease,box-shadow .15s ease;box-sizing:border-box}.article-generator-modal__input:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.article-generator-modal__input::placeholder{color:var(--text-faint)}.article-generator-modal__hint{margin:8px 0 0;font-size:12px;line-height:1.45;color:var(--text-muted);font-style:italic}.article-generator-modal__warning{margin-top:6px;font-size:12px;line-height:1.4;color:var(--text-error);font-weight:500;display:none}.article-generator-modal__warning.is-active{display:block}.article-generator-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)}.article-generator-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 .15s ease,border-color .15s ease,transform .05s ease}.article-generator-modal__button:hover{background-color:var(--background-modifier-hover)}.article-generator-modal__button:active{transform:translateY(1px)}.article-generator-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.article-generator-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.article-generator-modal__button:disabled{opacity:.5;cursor:not-allowed}@media(max-width:600px){.article-generator-modal{width:95vw;max-width:none}.article-generator-modal__header,.article-generator-modal__section,.article-generator-modal__footer{padding-left:18px;padding-right:18px}.article-generator-modal__footer{flex-direction:column-reverse}.article-generator-modal__button{width:100%}}.perplexica-modal{width:90vw;max-width:640px}.perplexica-modal .modal-content{padding:0}.perplexica-modal__header{padding:24px 28px 16px;border-bottom:1px solid var(--background-modifier-border)}.perplexica-modal__title{margin:0 0 6px;font-size:1.5em;font-weight:600;color:var(--text-normal);letter-spacing:-.01em}.perplexica-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.perplexica-modal__section{padding:18px 28px 4px}.perplexica-modal__section+.perplexica-modal__section{border-top:1px solid var(--background-modifier-border-hover);margin-top:4px}.perplexica-modal__section-title{margin:0 0 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.perplexica-modal__section .setting-item{padding:10px 0;border-top:none}.perplexica-modal__section .setting-item+.setting-item{border-top:1px solid var(--background-modifier-border-hover)}.perplexica-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.perplexica-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 .15s ease,box-shadow .15s ease;box-sizing:border-box}.perplexica-modal__textarea:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.perplexica-modal__textarea::placeholder{color:var(--text-faint)}.perplexica-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)}.perplexica-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 .15s ease,border-color .15s ease,transform .05s ease}.perplexica-modal__button:hover{background-color:var(--background-modifier-hover)}.perplexica-modal__button:active{transform:translateY(1px)}.perplexica-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.perplexica-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.perplexica-modal__button:disabled{opacity:.5;cursor:not-allowed}@media(max-width:600px){.perplexica-modal{width:95vw;max-width:none}.perplexica-modal__header,.perplexica-modal__section,.perplexica-modal__footer{padding-left:18px;padding-right:18px}.perplexica-modal__footer{flex-direction:column-reverse}.perplexica-modal__button{width:100%}}.lmstudio-modal{width:90vw;max-width:640px}.lmstudio-modal .modal-content{padding:0}.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:-.01em}.lmstudio-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.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:.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)}.lmstudio-modal__hint{margin:-4px 0 8px;font-size:12px;line-height:1.45;color:var(--text-muted)}.lmstudio-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.lmstudio-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 .15s ease,box-shadow .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)}.lmstudio-modal__textarea--system{min-height:72px;font-family:var(--font-monospace);font-size:13px}.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 .15s ease,border-color .15s ease,transform .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:.5;cursor:not-allowed}@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%}}.url-update-modal{width:90vw;max-width:500px}.url-update-modal .modal-content{padding:0}.url-update-modal__header{padding:20px 24px 12px;border-bottom:1px solid var(--background-modifier-border)}.url-update-modal__title{margin:0;font-size:1.3em;font-weight:600;color:var(--text-normal)}.url-update-modal__section{padding:18px 24px}.url-update-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.url-update-modal__input{width:100%;padding:10px 12px;font-family:var(--font-text);font-size:14px;color:var(--text-normal);background-color:var(--background-primary);border:1px solid var(--background-modifier-border);border-radius:6px;box-sizing:border-box}.url-update-modal__input:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.url-update-modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:16px 24px;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)}.url-update-modal__button{padding:6px 14px;font-size:13px;font-weight:500;border:1px solid var(--background-modifier-border);border-radius:4px;background-color:var(--background-primary);color:var(--text-normal);cursor:pointer;transition:background-color .15s ease,border-color .15s ease,transform .05s ease}.url-update-modal__button:hover{background-color:var(--background-modifier-hover)}.url-update-modal__button:active{transform:translateY(1px)}.url-update-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.url-update-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}@media(max-width:500px){.url-update-modal{width:95vw;max-width:none}.url-update-modal__header,.url-update-modal__section,.url-update-modal__footer{padding-left:16px;padding-right:16px}.url-update-modal__footer{flex-direction:column-reverse}.url-update-modal__button{width:100%}}.text-enhancement-modal{width:90vw;max-width:720px}.text-enhancement-modal .modal-content{padding:0}.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:-.01em}.text-enhancement-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.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}.text-enhancement-modal__hint{margin:-4px 0 8px;font-size:12px;line-height:1.45;color:var(--text-muted)}.text-enhancement-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.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 .15s ease,box-shadow .15s ease;box-sizing:border-box}.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)}.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}.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:6px;background-color:var(--background-primary);color:var(--text-normal);cursor:pointer;transition:background-color .15s ease,border-color .15s ease,transform .05s ease}.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:.5;cursor:not-allowed}@media(max-width:600px){.text-enhancement-modal{width:95vw;max-width:none}.text-enhancement-modal__header,.text-enhancement-modal__section,.text-enhancement-modal__footer{padding-left:18px;padding-right:18px}.text-enhancement-modal__footer{flex-direction:column-reverse}.text-enhancement-modal__button{width:100%}}.text-enhancement-with-images-modal{width:90vw;max-width:720px}.text-enhancement-with-images-modal .modal-content{padding:0}.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:-.01em}.text-enhancement-with-images-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.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}.text-enhancement-with-images-modal__hint{margin:-4px 0 8px;font-size:12px;line-height:1.45;color:var(--text-muted)}.text-enhancement-with-images-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.text-enhancement-with-images-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 .15s ease,box-shadow .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}.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);cursor:pointer;transition:background-color .15s ease,border-color .15s ease,transform .05s ease}.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)}.text-enhancement-with-images-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.text-enhancement-with-images-modal__button:disabled{opacity:.5;cursor:not-allowed}@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%}}.claude-modal{width:90vw;max-width:640px}.claude-modal .modal-content{padding:0}.claude-modal__header{padding:24px 28px 16px;border-bottom:1px solid var(--background-modifier-border)}.claude-modal__title{margin:0 0 6px;font-size:1.5em;font-weight:600;color:var(--text-normal);letter-spacing:-.01em}.claude-modal__subtitle{margin:0;font-size:13px;line-height:1.45;color:var(--text-muted)}.claude-modal__section{padding:18px 28px 4px}.claude-modal__section+.claude-modal__section{border-top:1px solid var(--background-modifier-border-hover);margin-top:4px}.claude-modal__section-title{margin:0 0 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.claude-modal__section .setting-item{padding:10px 0;border-top:none}.claude-modal__section .setting-item+.setting-item{border-top:1px solid var(--background-modifier-border-hover)}.claude-modal__label{display:block;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}.claude-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 .15s ease,box-shadow .15s ease;box-sizing:border-box}.claude-modal__textarea:focus{outline:none;border-color:var(--interactive-accent);box-shadow:0 0 0 3px var(--interactive-accent-hover)}.claude-modal__textarea::placeholder{color:var(--text-faint)}.claude-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)}.claude-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 .15s ease,border-color .15s ease,transform .05s ease}.claude-modal__button:hover{background-color:var(--background-modifier-hover)}.claude-modal__button:active{transform:translateY(1px)}.claude-modal__button.mod-cta{background-color:var(--interactive-accent);color:var(--text-on-accent);border-color:var(--interactive-accent)}.claude-modal__button.mod-cta:hover{background-color:var(--interactive-accent-hover);border-color:var(--interactive-accent-hover)}.claude-modal__button:disabled{opacity:.5;cursor:not-allowed}@media(max-width:600px){.claude-modal{width:95vw;max-width:none}.claude-modal__header,.claude-modal__section,.claude-modal__footer{padding-left:18px;padding-right:18px}.claude-modal__footer{flex-direction:column-reverse}.claude-modal__button{width:100%}}.perplexed-json-textarea{width:100%;min-height:300px;font-family:var(--font-monospace)}.perplexed-json-textarea.is-tall{min-height:500px}.perplexed-json-textarea.is-extra-tall{min-height:600px}.callout[data-callout=brain]{background-color:#9333ea1a;border-left:4px solid #9333ea}.callout[data-callout=brain] .callout-title{color:#9333ea;font-weight:600}.callout[data-callout=brain] .callout-content{color:var(--text-normal);font-style:normal}.callout[data-callout=brain] .callout-icon{color:#9333ea}