From 7b17b3b6e765720217cbca2e2d839237da376363 Mon Sep 17 00:00:00 2001 From: mpstaton Date: Sat, 9 May 2026 02:39:06 -0500 Subject: [PATCH] chore(marketplace): pass ObsidianReviewBot lint cleanly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies all required fixes from the bot's review of PR #12513 (obsidianmd/obsidian-releases). ESLint now passes 0 errors with the Obsidian community ruleset; tsc passes; production build is clean. Lint setup: - Adopted eslint-plugin-obsidianmd@^0.2.9 in eslint.config.mjs - Mirrored the bot's rule surface locally so violations surface in `pnpm lint` instead of the marketplace PR thread - Configured the ui/sentence-case rule with a brand allowlist (Perplexity, Perplexica, Vane, Claude, Anthropic, LM Studio, Imgur, ImageKit, OpenAI, Ollama, Sonar, Llama, GPT, YAML, JSON, URL, API) so legitimate proper nouns aren't lowercased Code fixes (487 → 0): - console.log/info → console.debug across all sources (~130 sites) - UI strings normalized to sentence case (~150 sites) - Command IDs and names cleaned up: dropped "command" suffix, dropped "perplexed" plugin-name prefix - Settings tab section headers switched from createEl('h2') to new Setting().setHeading() (5 sites) - Inline element.style.color/width/minHeight/fontFamily migrated to a new CSS class (.perplexed-json-textarea + .is-tall / .is-extra-tall) in src/styles/settings-tab.css (8 textarea sites, 32 style assignments) - Async input handlers wrapped: addEventListener('input', () => void (async () => { ... })()) so the listener type matches (8 sites) - forEach((opt) => dd.addOption(...)) blocks made void-returning to satisfy no-misused-promises (9 modal sites) - JSON.parse results typed as unknown then narrowed - throw → throw new Error() - ${unknown} interpolations narrowed via instanceof Error - Removed dotenv runtime dependency: published plugins shouldn't read .env at runtime; user enters API keys via the settings tab - Replaced builtin-modules dev-dependency with node:module's builtinModules — same data, no extra package - Logger console-method dispatch rewritten as a switch instead of dynamic console[level] indexing (which the bot rejects) Streaming exceptions: - src/services/{perplexityService,lmStudioService,perplexicaService}.ts retain `fetch()` for SSE / chunked streaming because Obsidian's `requestUrl` buffers the whole body. Each site has an `eslint-disable-next-line no-restricted-globals` with the marketplace `/skip` justification inline. Plan to surface these on the PR with a `/skip` reply. Reference docs: - context-v/issues/Obsidian-Review-Bot-Feedback-on-Perplexed-Submission.md (issue log distilled into…) - context-v/reminders/Obsidian-Marketplace-Compliance.md (the rules themselves, reusable for image-gin and cite-wide submissions) Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- esbuild.config.mjs | 2 +- eslint.config.mjs | 44 ++- main.ts | 333 +++++++++---------- package.json | 5 +- pnpm-lock.yaml | Bin 40635 -> 99368 bytes src/modals/ArticleGeneratorModal.ts | 26 +- src/modals/ClaudeModal.ts | 14 +- src/modals/LMStudioModal.ts | 10 +- src/modals/PerplexicaModal.ts | 10 +- src/modals/PerplexityModal.ts | 16 +- src/modals/TextEnhancementModal.ts | 14 +- src/modals/TextEnhancementWithImagesModal.ts | 16 +- src/modals/URLUpdateModal.ts | 3 +- src/services/claudeService.ts | 2 +- src/services/lmStudioService.ts | 11 +- src/services/perplexicaService.ts | 9 +- src/services/perplexityService.ts | 135 ++++---- src/styles/main.css | 1 + src/styles/settings-tab.css | 22 ++ src/utils/logger.ts | 19 +- styles.css | 2 +- 22 files changed, 379 insertions(+), 317 deletions(-) create mode 100644 src/styles/settings-tab.css 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 92eec921f9eb5553ffb73f972c78285b2cf450d8..a8c658df83905be12b2c0ea60b1cfe9e0106a877 100644 GIT binary patch literal 99368 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 Py>l&tn2dNd;&}fr9=bU{ delta 1088 zcmZXTZA_b06vuO(z6=UNaZoIbQG!S+rL<353e1?6*8&~TLBSD%>(dr_>Dsik(6Z?i zqcWXj$uZs~8VPRE&A7RFHy;^e)0mh{6t`)@lx1|r@Wo{C!A;x(@hNd1jQ7*I=jPo1 zIrsO!Cnuz{bIj47l2j^BAnf&z;FMS?GHO{Y5N(&3wL)}mVGYu_$bc_A;4j@D@I-uG z{6MVOVbbcgI+^OS=!z*XjkGEu3-@1s5_vmftmSmZe|s3VnDE*S zwbN~Cin?k;W21WpgNCkH(@>+{+OWqIjg>cq$|D|M*gWiL<9xcx5`%lFx}-b627?Z^ zxxc2PI%IZKcx=^116SA5u(xi=81;BU^|mt3jCYcEinXk+nEvRf25Rj*3rSngW6_&Y zcT(;2HyWLWjzBovY;{{}EA94($5_LeIo2^2cIkO^IR)gK`+1riN8F|3cYp!u1H>)9o>VmamS}}51 zOdSX2M40v39G1LsoG!eb#9;Be=du0R8N4(77G5|$MORNW16izxe7yJ31o^WEy?h$yuY8X3uRp;L=P!}h{g8txxAN&13u++uB9M+( z7hl29ujlFgr6{1}_7N;vzCe@*A(P(x?J2;oexIS=tjqvoVbOJ6O(*_%259YFNJjEk zoxpoaz?_9g{?5ZewG0P06jc3SA7H%nF`Rv9ctkx7FFsJ<%NtUBb|V*O*mV4PqZKWi zQaZV*k&t~UU?Qs`C`?3B5>JI~B+fu4xgv)wA{W7S{v`?IK;pOn4^^e|x@}Oz@WM8Y zA!`Rg%BQEp5|eO}MsQFHDa(PVJ}5|V>=nSU9W-- MYwu==h0$dH08^TkXaE2J 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}