lossless-group_perplexed-pl.../main.ts
mpstaton 76046359a9 feat(find-images): selection-anchored image discovery with paragraph-level placement
New command "Find images for selection". Highlight a passage in any open file,
invoke the command, and the plugin:

1. Splits the selection into paragraphs and numbers them.
2. Reads the active file's frontmatter for url / site_name (entity context).
3. Sends a Perplexity request with return_images=true asking the model to:
   - Find N screenshot/product/feature images that illustrate the specific
     passage content.
   - Prefer images hosted on the entity's domain; fall back to other web
     sources only when on-domain images aren't available.
   - For each chosen image, output one line of the form
     `[AFTER {paragraph_number}] [IMAGE {n}: <description>]` so the model
     itself decides which paragraph each image belongs after.
4. Parses the placement markers, maps each image number to the corresponding
   entry in response.images, and rebuilds the selection with `\n\n`
   separators around each embed so images sit between paragraphs, never
   inside one.
5. Falls back to even-distribution domain-preferred ordering if the model
   emitted no placement markers.

Result is `![description](image_url)` embeds in standard CommonMark/GFM form,
inserted via editor.replaceSelection. Frontmatter and surrounding content
are untouched.

New setting `findImagesMaxImages` (default 3) caps the per-invocation count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:46:24 -05:00

1821 lines
80 KiB
TypeScript

import type { App, Editor} from 'obsidian';
import { Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
// Import services
import { PerplexityService } from './src/services/perplexityService';
import { PerplexicaService } from './src/services/perplexicaService';
import { LMStudioService } from './src/services/lmStudioService';
import { ClaudeService } from './src/services/claudeService';
import { PromptsService } from './src/services/promptsService';
// Import modals
import { PerplexityModal } from './src/modals/PerplexityModal';
import { PerplexicaModal } from './src/modals/PerplexicaModal';
import { LMStudioModal } from './src/modals/LMStudioModal';
import { ClaudeModal } from './src/modals/ClaudeModal';
import { URLUpdateModal } from './src/modals/URLUpdateModal';
import { ArticleGeneratorModal } from './src/modals/ArticleGeneratorModal';
import { TextEnhancementModal } from './src/modals/TextEnhancementModal';
import { TextEnhancementWithImagesModal } from './src/modals/TextEnhancementWithImagesModal';
import { DirectoryTemplatePickerModal } from './src/modals/DirectoryTemplatePickerModal';
import { FolderPickerModal } from './src/modals/FolderPickerModal';
import { BatchConfirmModal } from './src/modals/BatchConfirmModal';
import {
applyTemplate as applyDirectoryTemplate,
applyTemplateBatch as applyDirectoryTemplateBatch,
listMarkdownFilesInFolder,
listTemplates as listDirectoryTemplates,
loadTemplate as loadDirectoryTemplate,
pathMatchesGlobs,
} from './src/services/directoryTemplateService';
import type { DirectoryTemplateSettings, ParsedTemplate } from './src/services/directoryTemplateService';
import type { TFile } from 'obsidian';
import { findImagesForSelection } from './src/services/findImagesService';
import type { FindImagesSettings } from './src/services/findImagesService';
interface PerplexedPluginSettings {
mySetting: string;
localLLMPath: string;
requestBodyTemplate: string;
perplexityRequestTemplate: string;
perplexityApiKey: string;
perplexicaEndpoint: string;
perplexityEndpoint: string;
lmStudioEndpoint: string;
lmStudioRequestTemplate: string;
anthropicApiKey: string;
claudeDefaultModel: string;
defaultModel: string;
defaultOptimizationMode: string;
defaultFocusMode: string;
defaultLMStudioModel: string;
// Display Settings
headerPosition: 'top' | 'bottom';
// Prompt Settings
prompts: {
// System prompts
perplexitySystemPrompt: string;
perplexicaSystemPrompt: string;
lmStudioDefaultSystemPrompt: string;
// Placeholder text
perplexityQueryPlaceholder: string;
perplexicaQueryPlaceholder: string;
lmStudioQueryPlaceholder: string;
lmStudioSystemPromptPlaceholder: string;
articleTermPlaceholder: string;
// Article generator template
articleGeneratorTemplate: string;
// Deep Research article generator template
deepResearchArticleTemplate: string;
// Image prompts
imageReferencesPrompt: string;
// Text enhancement prompt
enhancePrompt: string;
// Text enhancement with images prompt
enhanceWithImagesPrompt: string;
};
// Directory templates (v0.1 spike — see context-v/specs/Per-Directory-Profile-Templates.md)
directoryTemplatesRoot: string;
directoryTemplatesFrontmatterWhitelist: string[];
directoryTemplatesRequestTimeoutMs: number;
// Find images for selection
findImagesMaxImages: number;
}
const DEFAULT_SETTINGS: PerplexedPluginSettings = {
mySetting: 'default',
// Use host.docker.internal to connect to the host machine from Docker containers
localLLMPath: 'http://host.docker.internal:3030/api/search',
perplexicaEndpoint: 'http://localhost:3030/api/search',
perplexityEndpoint: 'https://api.perplexity.ai/chat/completions',
lmStudioEndpoint: 'http://localhost:1234/v1/chat/completions',
headerPosition: 'top',
requestBodyTemplate: `{
"chatModel": {
"provider": "ollama",
"name": "llama3.2:latest"
},
"embeddingModel": {
"provider": "ollama",
"name": "llama3.2:latest"
},
"optimizationMode": "speed",
"focusMode": "webSearch",
"query": "What is Perplexica's architecture?",
"history": [
{
"role": "user",
"content": "What is Perplexica's architecture?"
}
],
"systemInstructions": "{{PERPLEXICA_SYSTEM_PROMPT}}",
"stream": false,
"maxTokens": 2048,
"temperature": 0.7
}`,
perplexityApiKey: '',
anthropicApiKey: '',
claudeDefaultModel: 'claude-opus-4-7',
perplexityRequestTemplate: `{
"model": "llama-3.1-sonar-small-128k-online",
"messages": [
{
"role": "system",
"content": "{{PERPLEXITY_SYSTEM_PROMPT}}"
},
{
"role": "user",
"content": "What is Perplexity AI's approach to search?"
}
],
"max_tokens": 2048,
"temperature": 0.7,
"top_p": 0.9,
"return_citations": true,
"search_domain_filter": [],
"return_images": false,
"return_related_questions": false,
"search_recency_filter": "month",
"top_k": 0,
"stream": false,
"presence_penalty": 0,
"frequency_penalty": 1
}`,
lmStudioRequestTemplate: `{
"model": "ibm/granite-3.2-8b",
"messages": [
{
"role": "user",
"content": "Hello, can you help me with this question?"
}
],
"max_tokens": 2048,
"temperature": 0.7,
"stream": false
}`,
defaultModel: 'llama3.2:latest',
defaultOptimizationMode: 'speed',
defaultFocusMode: 'webSearch',
defaultLMStudioModel: 'ibm/granite-3.2-8b',
// Prompt Settings
prompts: {
// System prompts
perplexitySystemPrompt: "You are a helpful AI assistant. Provide clear, concise, and accurate information with proper citations.",
perplexicaSystemPrompt: "You are a helpful AI assistant. Provide clear, concise, and accurate information.",
lmStudioDefaultSystemPrompt: "You are a helpful AI assistant. Provide clear, concise, and accurate information.",
// Placeholder text
perplexityQueryPlaceholder: "What would you like to ask Perplexity?",
perplexicaQueryPlaceholder: "What would you like to ask Perplexica / Vane?",
lmStudioQueryPlaceholder: "What would you like to ask?",
lmStudioSystemPromptPlaceholder: "You are a helpful AI assistant...",
articleTermPlaceholder: "e.g., AI Copilots, AI Studios, Machine Learning, etc.",
// Article generator template
articleGeneratorTemplate: `Write a comprehensive one-page article about "{TERM}".
Structure the article as follows:
1. **Introduction** (2-3 sentences)
- Define the term and its significance
- Provide context for why it matters
2. **Main Content** (3-4 paragraphs)
- Explain the concept in detail
- Include practical examples and use cases
- Discuss benefits and potential applications
- Address any challenges or considerations
3. **Current State and Trends** (1-2 paragraphs)
- Discuss current adoption and market status
- Mention key players or technologies
- Highlight recent developments
4. **Future Outlook** (1 paragraph)
- Predict future developments
- Discuss potential impact
5. **Conclusion** (1-2 sentences)
- Summarize key points
- End with a forward-looking statement
**Important Guidelines:**
- Keep the total length to approximately one page (500-800 words)
- Use clear, accessible language
- Include specific examples and real-world applications
- Make it engaging and informative for a general audience
- Use markdown formatting for structure`,
// Deep Research article generator template
deepResearchArticleTemplate: `Conduct comprehensive research and write an in-depth article about "{TERM}".
**Research Requirements:**
- Conduct exhaustive research across hundreds of sources
- Analyze multiple perspectives and viewpoints
- Include academic, industry, and expert sources
- Provide detailed citations and references
- Examine historical context and evolution
- Consider global implications and regional variations
**Article Structure:**
1. **Executive Summary** (1 paragraph)
- Concise overview of key findings
- Main conclusions and implications
2. **Introduction and Definition** (2-3 paragraphs)
- Comprehensive definition and scope
- Historical context and evolution
- Current significance and relevance
3. **Comprehensive Analysis** (6-8 paragraphs)
- Detailed examination of core concepts
- Multiple perspectives and approaches
- Industry applications and use cases
- Technical implementation details
- Market analysis and competitive landscape
- Regulatory and ethical considerations
4. **Current State and Market Dynamics** (3-4 paragraphs)
- Global adoption patterns and trends
- Key players, technologies, and platforms
- Regional variations and cultural factors
- Economic impact and market size
- Recent developments and breakthroughs
5. **Challenges and Opportunities** (2-3 paragraphs)
- Technical challenges and limitations
- Implementation barriers and solutions
- Future opportunities and potential
- Risk factors and mitigation strategies
6. **Future Outlook and Predictions** (2-3 paragraphs)
- Short-term developments (1-2 years)
- Medium-term trends (3-5 years)
- Long-term implications (5+ years)
- Strategic recommendations
7. **Conclusion** (1-2 paragraphs)
- Synthesis of key findings
- Strategic implications
- Call to action or forward-looking statement
**Research Guidelines:**
- Include diverse source types (academic, industry, news, expert opinions)
- Provide detailed citations for all claims
- Analyze conflicting viewpoints and evidence
- Consider global and regional perspectives
- Include quantitative data where available
- Examine both benefits and risks
- Address ethical and societal implications
**Quality Standards:**
- Academic rigor with practical relevance
- Balanced analysis of multiple perspectives
- Evidence-based conclusions
- Clear, professional writing style
- Comprehensive bibliography`,
// Image prompts
imageReferencesPrompt: "**Image References:**\nPlease include the following image references throughout your response where appropriate:\n- [IMAGE 1: Relevant diagram or illustration related to the topic]\n- [IMAGE 2: Practical example or use case visualization]\n- [IMAGE 3: Additional supporting visual content]",
// Text enhancement prompt
enhancePrompt: "Please enhance the following text by improving clarity, adding relevant details, expanding on key points, and making it more comprehensive and engaging. Maintain the original meaning and tone while making it more informative and well-structured:\n\n{TEXT}",
// Text enhancement with images prompt
enhanceWithImagesPrompt: "Please provide 1-3 relevant images for the following text. Return ONLY the image markers in the format [IMAGE 1: description], [IMAGE 2: description], etc. Each image should illustrate a key concept, example, or visual representation related to the text. Do not include any other text or explanation:\n\n{TEXT}"
},
// Directory templates (v0.1 spike defaults)
directoryTemplatesRoot: 'zz-cf-lib/templates',
directoryTemplatesFrontmatterWhitelist: ['title', 'og_description', 'tags', 'og_image'],
directoryTemplatesRequestTimeoutMs: 300000,
// Find images for selection
findImagesMaxImages: 3
};
export default class PerplexedPlugin extends Plugin {
public settings: PerplexedPluginSettings = DEFAULT_SETTINGS;
private statusBarItemEl: HTMLElement | null = null;
private ribbonIconEl: HTMLElement | null = null;
private batchCancelled = false;
// Service instances
private perplexityService!: PerplexityService | null;
private perplexicaService!: PerplexicaService | null;
private lmStudioService!: LMStudioService | null;
private claudeService!: ClaudeService | null;
private promptsService!: PromptsService | null;
async onload(): Promise<void> {
try {
console.debug('Perplexed Plugin: Starting initialization...');
await this.loadSettings();
console.debug('Perplexed Plugin: Settings loaded successfully');
// Initialize prompts service first
try {
this.promptsService = new PromptsService(this.settings.prompts);
console.debug('Perplexed Plugin: PromptsService initialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to initialize PromptsService:', error);
new Notice('Failed to initialize PromptsService');
this.promptsService = null;
}
// Initialize services with error handling - only if promptsService is available
if (this.promptsService) {
try {
this.perplexityService = new PerplexityService({
perplexityApiKey: this.settings.perplexityApiKey,
perplexityEndpoint: this.settings.perplexityEndpoint,
promptsService: this.promptsService,
requestTemplate: this.settings.perplexityRequestTemplate,
headerPosition: this.settings.headerPosition
});
console.debug('Perplexed Plugin: PerplexityService initialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to initialize PerplexityService:', error);
new Notice('Failed to initialize PerplexityService');
this.perplexityService = null;
}
try {
this.perplexicaService = new PerplexicaService({
perplexicaEndpoint: this.settings.perplexicaEndpoint,
localLLMPath: this.settings.localLLMPath,
defaultModel: this.settings.defaultModel,
promptsService: this.promptsService,
requestTemplate: this.settings.requestBodyTemplate
});
console.debug('Perplexed Plugin: PerplexicaService initialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to initialize PerplexicaService:', error);
new Notice('Failed to initialize PerplexicaService');
this.perplexicaService = null;
}
try {
this.lmStudioService = new LMStudioService({
lmStudioEndpoint: this.settings.lmStudioEndpoint,
promptsService: this.promptsService,
requestTemplate: this.settings.lmStudioRequestTemplate
});
console.debug('Perplexed Plugin: LMStudioService initialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to initialize LMStudioService:', error);
new Notice('Failed to initialize LMStudioService');
this.lmStudioService = null;
}
try {
this.claudeService = new ClaudeService({
anthropicApiKey: this.settings.anthropicApiKey,
promptsService: this.promptsService,
headerPosition: this.settings.headerPosition,
});
console.debug('Perplexed Plugin: ClaudeService initialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to initialize ClaudeService:', error);
new Notice('Failed to initialize ClaudeService');
this.claudeService = null;
}
} else {
// If promptsService failed, set all other services to null
this.perplexityService = null;
this.perplexicaService = null;
this.lmStudioService = null;
this.claudeService = null;
console.debug('Perplexed Plugin: Skipping service initialization due to PromptsService failure');
}
// Debug: Log current settings
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.debug('Perplexed Plugin: Settings tab added successfully');
// Register commands with error handling
try {
this.registerPerplexicaCommands();
console.debug('Perplexed Plugin: Perplexica commands registered successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to register Perplexica commands:', error);
}
try {
this.registerPerplexityCommands();
console.debug('Perplexed Plugin: Perplexity commands registered successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to register Perplexity commands:', error);
}
try {
this.registerLMStudioCommands();
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.debug('Perplexed Plugin: Claude commands registered successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to register Claude commands:', error);
}
try {
this.registerArticleGeneratorCommands();
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.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.debug('Perplexed Plugin: Get related images commands registered successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to register get related images commands:', error);
}
// Diagnostic action: log registered commands to the console.
this.addCommand({
id: 'debug-status',
name: 'Debug: log registered actions',
callback: () => {
this.debugCommands();
}
});
// Add command to reset prompts to defaults
this.addCommand({
id: 'reset-prompts',
name: 'Reset prompts to default',
callback: async () => {
await this.resetPromptsToDefault();
}
});
// Reinitialize all provider services (Perplexity / Perplexica /
// LM Studio / Claude). Useful after editing settings.
this.addCommand({
id: 'reinitialize-services',
name: 'Reinitialize provider services',
callback: async () => {
await this.reinitializeServices();
}
});
// Directory templates (v0.1 spike). See context-v/specs/Per-Directory-Profile-Templates.md
this.addCommand({
id: 'apply-directory-template-to-current-file',
name: 'Apply directory template to current file',
callback: async () => {
await this.runApplyDirectoryTemplate();
}
});
// Batch run a directory template across every file in a folder (v0.2).
this.addCommand({
id: 'apply-directory-template-to-folder',
name: 'Apply directory template to all files in folder',
callback: () => {
this.runApplyDirectoryTemplateBatch();
}
});
// Cancel an in-flight batch run.
this.addCommand({
id: 'stop-directory-template-batch',
name: 'Stop directory template batch',
callback: () => {
this.batchCancelled = true;
new Notice('Stop requested — finishing current file then halting.');
}
});
// Find images for the current selection — anchors search on the
// selection's content + the active file's url/site_name frontmatter
// and distributes returned images between paragraphs.
this.addCommand({
id: 'find-images-for-selection',
name: 'Find images for selection',
editorCallback: (editor: Editor) => {
const file = this.app.workspace.getActiveFile();
if (!file) {
new Notice('No active file.');
return;
}
const findSettings: FindImagesSettings = {
perplexityApiKey: this.settings.perplexityApiKey,
perplexityEndpoint: this.settings.perplexityEndpoint,
maxImages: this.settings.findImagesMaxImages,
};
void findImagesForSelection(this.app, findSettings, file, editor);
}
});
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');
}
}
onunload(): void {
this.statusBarItemEl?.remove();
this.ribbonIconEl?.remove();
}
private async loadSettings() {
const savedData: Partial<PerplexedPluginSettings> = (await this.loadData()) as Partial<PerplexedPluginSettings> ?? {};
this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData);
// Ensure new fields are always present (migration for existing users)
if (!this.settings.prompts.deepResearchArticleTemplate) {
this.settings.prompts.deepResearchArticleTemplate = DEFAULT_SETTINGS.prompts.deepResearchArticleTemplate;
await this.saveSettings();
}
if (!this.settings.prompts.enhancePrompt) {
this.settings.prompts.enhancePrompt = DEFAULT_SETTINGS.prompts.enhancePrompt;
await this.saveSettings();
}
if (!this.settings.prompts.enhanceWithImagesPrompt) {
this.settings.prompts.enhanceWithImagesPrompt = DEFAULT_SETTINGS.prompts.enhanceWithImagesPrompt;
await this.saveSettings();
}
}
public async saveSettings(): Promise<void> {
try {
await this.saveData(this.settings);
} catch (error) {
console.error('Failed to save settings:', error);
new Notice('Failed to save settings');
}
}
// Delegate methods to services
public async queryPerplexity(query: string, model: string, stream: boolean, editor: Editor, options?: {
return_citations?: boolean;
return_images?: boolean;
return_related_questions?: boolean;
search_recency_filter?: string;
}): Promise<void> {
if (!this.perplexityService) {
throw new Error('Perplexity service not initialized');
}
await this.perplexityService.queryPerplexity(query, model, stream, editor, options);
}
public async queryPerplexica(query: string, focusMode: string, optimizationMode: string, stream: boolean, editor: Editor, options?: {
return_images?: boolean;
}): Promise<void> {
if (!this.perplexicaService) {
throw new Error('Perplexica service not initialized');
}
await this.perplexicaService.queryPerplexica(query, focusMode, optimizationMode, stream, editor, options);
}
public async queryLMStudio(query: string, model: string, stream: boolean, editor: Editor, options?: {
max_tokens?: number;
temperature?: number;
top_p?: number;
system_prompt?: string;
return_images?: boolean;
}): Promise<void> {
if (!this.lmStudioService) {
throw new Error('LM Studio service not initialized');
}
await this.lmStudioService.queryLMStudio(query, model, stream, editor, options);
}
// Getter for prompts service
public getPromptsService(): PromptsService | null {
return this.promptsService;
}
private registerPerplexicaCommands(): void {
// Command to update Perplexica URL
this.addCommand({
id: 'update-perplexica-url',
name: 'Update Perplexica / Vane URL',
callback: () => {
const modal = new URLUpdateModal(this.app, {
title: 'Update Perplexica / Vane API URL',
label: 'Perplexica / Vane API URL',
placeholder: 'http://localhost:3030/api/search',
currentValue: this.settings.perplexicaEndpoint,
onSave: async (newUrl: string) => {
this.settings.perplexicaEndpoint = newUrl;
await this.saveSettings();
}
});
modal.open();
}
});
// Command to show current settings
this.addCommand({
id: 'show-perplexica-settings',
name: 'Show Perplexica / Vane settings',
callback: () => {
new Notice(`Current Perplexica / Vane URL: ${this.settings.perplexicaEndpoint}`);
console.debug('Perplexica Settings:', this.settings);
}
});
// Command to ask Perplexica
this.addCommand({
id: 'ask-perplexica',
name: 'Ask Perplexica / Vane',
editorCallback: (editor: Editor) => {
try {
if (!this.perplexicaService) {
new Notice('Perplexica / Vane service not initialized. Please check console for errors and try the debug command.');
console.error('Perplexica service is not initialized');
return;
}
if (!this.promptsService) {
new Notice('Prompts service not initialized. Please check console for errors and try the debug command.');
console.error('Prompts service is not initialized');
return;
}
const modal = new PerplexicaModal(this.app, editor, this.perplexicaService, this.promptsService);
modal.open();
} catch (error) {
console.error('Error opening Perplexica modal:', error);
new Notice('Failed to open Perplexica / Vane modal. Check console for details.');
}
}
});
}
private registerPerplexityCommands(): void {
try {
// Command to update Perplexity URL
this.addCommand({
id: 'update-perplexity-url',
name: 'Update Perplexity URL',
callback: () => {
const modal = new URLUpdateModal(this.app, {
title: 'Update Perplexity API URL',
label: 'Perplexity API URL',
placeholder: 'https://api.perplexity.ai/chat/completions',
currentValue: this.settings.perplexityEndpoint,
onSave: async (newUrl: string) => {
this.settings.perplexityEndpoint = newUrl;
await this.saveSettings();
}
});
modal.open();
}
});
// Command to show current Perplexity settings
this.addCommand({
id: 'show-perplexity-settings',
name: 'Show Perplexity settings',
callback: () => {
new Notice(`Current Perplexity URL: ${this.settings.perplexityEndpoint}`);
console.debug('Perplexity Settings:', this.settings);
}
});
// Command to ask Perplexity
this.addCommand({
id: 'ask-perplexity',
name: 'Ask Perplexity',
editorCallback: (editor: Editor) => {
try {
if (!this.perplexityService) {
new Notice('Perplexity service not initialized. Please check console for errors and try the debug command.');
console.error('Perplexity service is not initialized');
return;
}
if (!this.promptsService) {
new Notice('Prompts service not initialized. Please check console for errors and try the debug command.');
console.error('Prompts service is not initialized');
return;
}
const modal = new PerplexityModal(this.app, editor, this.perplexityService, this.promptsService);
modal.open();
} catch (error) {
console.error('Error opening Perplexity modal:', error);
new Notice('Failed to open Perplexity modal. Check console for details.');
}
}
});
// Add a fallback command that shows service status
this.addCommand({
id: 'perplexity-service-status',
name: 'Check Perplexity service status',
callback: () => {
if (this.perplexityService) {
new Notice('Perplexity service is initialized and ready');
console.debug('Perplexity service status: OK');
} else {
new Notice('Perplexity service is not initialized. Check console for errors.');
console.error('Perplexity service status: FAILED');
}
}
});
console.debug('Perplexed Plugin: Perplexity commands registered successfully');
} catch (error) {
console.error('Perplexed Plugin: Error registering Perplexity commands:', error);
throw error;
}
}
private registerClaudeCommands(): void {
this.addCommand({
id: 'ask-claude',
name: 'Ask Claude',
editorCallback: (editor: Editor) => {
if (!this.claudeService) {
new Notice('Claude service not initialized. Set ANTHROPIC_API_KEY in .env or settings, then reinitialize services.');
return;
}
if (!this.promptsService) {
new Notice('Prompts service not initialized.');
return;
}
new ClaudeModal(this.app, editor, this.claudeService, this.promptsService).open();
},
});
this.addCommand({
id: '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.');
}
},
});
}
private registerLMStudioCommands(): void {
// Command to update LM Studio URL
this.addCommand({
id: 'update-lmstudio-url',
name: 'Update LM Studio URL',
callback: () => {
const modal = new URLUpdateModal(this.app, {
title: 'Update LM Studio API URL',
label: 'LM Studio API URL',
placeholder: 'http://localhost:1234/v1/chat/completions',
currentValue: this.settings.lmStudioEndpoint,
onSave: async (newUrl: string) => {
this.settings.lmStudioEndpoint = newUrl;
await this.saveSettings();
}
});
modal.open();
}
});
// Command to show current LM Studio settings
this.addCommand({
id: 'show-lmstudio-settings',
name: 'Show LM Studio settings',
callback: () => {
new Notice(`Current LM Studio URL: ${this.settings.lmStudioEndpoint}`);
console.debug('LM Studio Settings:', this.settings);
}
});
// Command to ask LM Studio
this.addCommand({
id: 'ask-lmstudio',
name: 'Ask LM Studio',
editorCallback: (editor: Editor) => {
try {
if (!this.lmStudioService) {
new Notice('LM Studio service not initialized. Please check console for errors and try the debug command.');
console.error('LM Studio service is not initialized');
return;
}
if (!this.promptsService) {
new Notice('Prompts service not initialized. Please check console for errors and try the debug command.');
console.error('Prompts service is not initialized');
return;
}
const modal = new LMStudioModal(this.app, editor, this.lmStudioService, this.promptsService);
modal.open();
} catch (error) {
console.error('Error opening LM Studio modal:', error);
new Notice('Failed to open LM Studio modal. Check console for details.');
}
}
});
}
private registerArticleGeneratorCommands(): void {
// Register Article Generator command
this.addCommand({
id: 'generate-article',
name: 'Generate one-page article',
editorCallback: (editor: Editor) => {
try {
if (!this.perplexityService) {
new Notice('Perplexity service not initialized. Please check console for errors and try the debug command.');
console.error('Perplexity service is not initialized');
return;
}
if (!this.promptsService) {
new Notice('Prompts service not initialized. Please check console for errors and try the debug command.');
console.error('Prompts service is not initialized');
return;
}
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.');
}
}
});
}
private registerTextEnhancementCommands(): void {
// Register Text Enhancement command
this.addCommand({
id: 'enhance-text',
name: 'Enhance selected text with Perplexity',
editorCallback: (editor: Editor) => {
try {
const selectedText = editor.getSelection();
if (!selectedText || selectedText.trim() === '') {
new Notice('Please select some text to enhance');
return;
}
if (!this.perplexityService) {
new Notice('Perplexity service not initialized. Please check console for errors and try the debug command.');
console.error('Perplexity service is not initialized');
return;
}
if (!this.promptsService) {
new Notice('Prompts service not initialized. Please check console for errors and try the debug command.');
console.error('Prompts service is not initialized');
return;
}
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.');
}
}
});
}
private registerTextEnhancementWithImagesCommands(): void {
// Register Get Related Images command
this.addCommand({
id: 'enhance-text-with-images',
name: 'Get related images for selected text',
editorCallback: (editor: Editor) => {
try {
const selectedText = editor.getSelection();
if (!selectedText || selectedText.trim() === '') {
new Notice('Please select some text to get related images for');
return;
}
if (!this.perplexityService) {
new Notice('Perplexity service not initialized. Please check console for errors and try the debug command.');
console.error('Perplexity service is not initialized');
return;
}
if (!this.promptsService) {
new Notice('Prompts service not initialized. Please check console for errors and try the debug command.');
console.error('Prompts service is not initialized');
return;
}
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.');
}
}
});
}
private debugCommands(): void {
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;
const perplexedCommands = Object.keys(registeredCommands).filter(cmd =>
cmd.startsWith('perplexed') ||
cmd.includes('perplexity') ||
cmd.includes('perplexica') ||
cmd.includes('lmstudio') ||
cmd.includes('generate-article') ||
cmd.includes('enhance-text')
);
console.debug('Registered Perplexed commands:', perplexedCommands);
if (perplexedCommands.length === 0) {
new Notice('No perplexed commands found! Check console for details.');
} else {
new Notice(`Found ${perplexedCommands.length} Perplexed commands. Check console for details.`);
}
console.debug('=== End Debug Information ===');
}
private async resetPromptsToDefault(): Promise<void> {
try {
console.debug('Perplexed Plugin: Resetting prompts to default...');
new Notice('Resetting prompts to default values...');
// Reset all prompt settings to default values
this.settings.prompts = { ...DEFAULT_SETTINGS.prompts };
// Save the updated settings
await this.saveSettings();
// Reinitialize the prompts service with new settings
if (this.promptsService) {
this.promptsService.updateSettings(this.settings.prompts);
console.debug('Perplexed Plugin: PromptsService updated with default settings');
}
new Notice('✅ Prompts reset to default values successfully');
console.debug('Perplexed Plugin: Prompts reset to default successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to reset prompts to default:', error);
new Notice('❌ Failed to reset prompts to default. Check console for details.');
}
}
private async reinitializeServices(): Promise<void> {
try {
console.debug('Perplexed Plugin: Reinitializing services...');
new Notice('Reinitializing perplexed services...');
// Reinitialize prompts service first
try {
this.promptsService = new PromptsService(this.settings.prompts);
console.debug('Perplexed Plugin: PromptsService reinitialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to reinitialize PromptsService:', error);
this.promptsService = null;
}
// Reinitialize other services only if promptsService is available
if (this.promptsService) {
try {
this.perplexityService = new PerplexityService({
perplexityApiKey: this.settings.perplexityApiKey,
perplexityEndpoint: this.settings.perplexityEndpoint,
promptsService: this.promptsService,
requestTemplate: this.settings.perplexityRequestTemplate,
headerPosition: this.settings.headerPosition
});
console.debug('Perplexed Plugin: PerplexityService reinitialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to reinitialize PerplexityService:', error);
this.perplexityService = null;
}
try {
this.perplexicaService = new PerplexicaService({
perplexicaEndpoint: this.settings.perplexicaEndpoint,
localLLMPath: this.settings.localLLMPath,
defaultModel: this.settings.defaultModel,
promptsService: this.promptsService,
requestTemplate: this.settings.requestBodyTemplate
});
console.debug('Perplexed Plugin: PerplexicaService reinitialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to reinitialize PerplexicaService:', error);
this.perplexicaService = null;
}
try {
this.lmStudioService = new LMStudioService({
lmStudioEndpoint: this.settings.lmStudioEndpoint,
promptsService: this.promptsService,
requestTemplate: this.settings.lmStudioRequestTemplate
});
console.debug('Perplexed Plugin: LMStudioService reinitialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to reinitialize LMStudioService:', error);
this.lmStudioService = null;
}
try {
this.claudeService = new ClaudeService({
anthropicApiKey: this.settings.anthropicApiKey,
promptsService: this.promptsService,
headerPosition: this.settings.headerPosition,
});
console.debug('Perplexed Plugin: ClaudeService reinitialized successfully');
} catch (error) {
console.error('Perplexed Plugin: Failed to reinitialize ClaudeService:', error);
this.claudeService = null;
}
} else {
// If promptsService failed, set all other services to null
this.perplexityService = null;
this.perplexicaService = null;
this.lmStudioService = null;
this.claudeService = null;
console.debug('Perplexed Plugin: Skipping service reinitialization due to PromptsService failure');
}
new Notice('Services reinitialization completed. Check console for details.');
console.debug('Perplexed Plugin: Services reinitialization completed');
} catch (error) {
console.error('Perplexed Plugin: Error during services reinitialization:', error);
new Notice('Failed to reinitialize services. Check console for details.');
}
}
private async runApplyDirectoryTemplate(): Promise<void> {
const target = this.app.workspace.getActiveFile();
if (!target) {
new Notice('No active file.');
return;
}
if (!this.settings.perplexityApiKey) {
new Notice('Perplexity API key is not set. Configure it in perplexed settings.');
return;
}
const root = this.settings.directoryTemplatesRoot;
const all = await listDirectoryTemplates(this.app, root);
if (all.length === 0) {
new Notice(`No templates found under "${root}".`);
return;
}
const matching = all.filter(t => pathMatchesGlobs(target.path, t.appliesToPaths));
if (matching.length === 0) {
new Notice("No template's applies-to-paths matches this file.");
return;
}
const dirSettings: DirectoryTemplateSettings = {
perplexityApiKey: this.settings.perplexityApiKey,
perplexityEndpoint: this.settings.perplexityEndpoint,
templatesRoot: this.settings.directoryTemplatesRoot,
frontmatterWhitelist: this.settings.directoryTemplatesFrontmatterWhitelist,
requestTimeoutMs: this.settings.directoryTemplatesRequestTimeoutMs,
};
new DirectoryTemplatePickerModal(this.app, matching, (chosen) => {
void (async () => {
const parsed = await loadDirectoryTemplate(this.app, chosen.file);
if (!parsed) {
new Notice('Template parse error: missing or malformed cft block.');
return;
}
await applyDirectoryTemplate(this.app, dirSettings, target, parsed);
})();
}).open();
}
private runApplyDirectoryTemplateBatch(): void {
if (!this.settings.perplexityApiKey) {
new Notice('Perplexity API key is not set. Configure it in perplexed settings.');
return;
}
new FolderPickerModal(this.app, (folder) => {
void (async () => {
const folderPath = folder.path;
const filesInFolder = listMarkdownFilesInFolder(this.app, folderPath);
if (filesInFolder.length === 0) {
new Notice(`No markdown files in "${folderPath || '/'}".`);
return;
}
const all = await listDirectoryTemplates(this.app, this.settings.directoryTemplatesRoot);
const matchingTemplates = all.filter(t =>
filesInFolder.some(f => pathMatchesGlobs(f.path, t.appliesToPaths))
);
if (matchingTemplates.length === 0) {
new Notice('No template matches any file in this folder.');
return;
}
new DirectoryTemplatePickerModal(this.app, matchingTemplates, (chosen) => {
void (async () => {
const parsed = await loadDirectoryTemplate(this.app, chosen.file);
if (!parsed) {
new Notice('Template parse error: missing or malformed cft block.');
return;
}
const filesForThisTemplate = filesInFolder.filter(f =>
pathMatchesGlobs(f.path, chosen.appliesToPaths)
);
let fillCount = 0;
let appendCount = 0;
for (const f of filesForThisTemplate) {
const content = await this.app.vault.cachedRead(f);
const afterFm = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
if (afterFm.trim().length === 0) fillCount++;
else appendCount++;
}
const dirSettings: DirectoryTemplateSettings = {
perplexityApiKey: this.settings.perplexityApiKey,
perplexityEndpoint: this.settings.perplexityEndpoint,
templatesRoot: this.settings.directoryTemplatesRoot,
frontmatterWhitelist: this.settings.directoryTemplatesFrontmatterWhitelist,
requestTimeoutMs: this.settings.directoryTemplatesRequestTimeoutMs,
};
new BatchConfirmModal(this.app, {
folderPath,
templateTitle: chosen.title,
fileCount: filesForThisTemplate.length,
fillCount,
appendCount,
}, () => {
void this.executeBatch(dirSettings, parsed, filesForThisTemplate);
}).open();
})();
}).open();
})();
}).open();
}
private async executeBatch(
dirSettings: DirectoryTemplateSettings,
parsed: ParsedTemplate,
files: TFile[],
): Promise<void> {
this.batchCancelled = false;
const progressNotice = new Notice(`Batch starting on ${files.length.toString()} files…`, 0);
try {
const result = await applyDirectoryTemplateBatch(
this.app,
dirSettings,
files,
parsed,
(p) => {
progressNotice.setMessage(
`Applying ${p.current.toString()}/${p.total.toString()}: ${p.file.basename}`
);
},
() => this.batchCancelled,
);
const summary = [
`Batch ${result.cancelled ? 'cancelled' : 'complete'}.`,
`Filled: ${result.appliedFill.toString()}`,
`Appended: ${result.appliedAppend.toString()}`,
`Errored: ${result.errored.toString()}`,
].join(' ');
new Notice(summary, 8000);
if (result.errors.length > 0) {
console.warn('Directory-template batch errors:', result.errors);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
new Notice(`Batch failed: ${msg}`);
} finally {
progressNotice.hide();
this.batchCancelled = false;
}
}
}
class PerplexedSettingTab extends PluginSettingTab {
plugin: PerplexedPlugin;
constructor(app: App, plugin: PerplexedPlugin) {
super(app, plugin);
this.plugin = plugin;
}
// Helper method to safely update prompts service
private updatePromptsService(): void {
const promptsService = this.plugin.getPromptsService();
if (promptsService) {
promptsService.updateSettings(this.plugin.settings.prompts);
}
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// Perplexity Section
new Setting(containerEl).setName("Perplexity (remote service)").setHeading();
containerEl.createEl('p', {
text: 'Configure settings for the hosted Perplexity AI service',
cls: 'setting-item-description'
});
new Setting(containerEl)
.setName('Endpoint')
.setDesc('API endpoint for Perplexity service')
.addText(text => text
.setPlaceholder('HTTPS://API.Perplexity.ai/chat/completions')
.setValue(this.plugin.settings.perplexityEndpoint)
.onChange(async (value: string) => {
this.plugin.settings.perplexityEndpoint = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('API key')
.setDesc('Your Perplexity API key (required for remote service)')
.addText(text => text
.setPlaceholder('Pplx-xxxxxxxxxxxxxxxxxxxxx')
.setValue(this.plugin.settings.perplexityApiKey)
.onChange(async (value: string) => {
this.plugin.settings.perplexityApiKey = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Header position')
.setDesc('Where to place the query header in generated articles')
.addDropdown(dropdown => dropdown
.addOption('top', 'Top of article')
.addOption('bottom', 'Bottom of article')
.setValue(this.plugin.settings.headerPosition)
.onChange(async (value: string) => {
this.plugin.settings.headerPosition = value as 'top' | 'bottom';
await this.plugin.saveSettings();
})
);
// Perplexity Request Template
const perplexityJsonSetting = new Setting(containerEl)
.setName('Request body template')
.setDesc('JSON template for Perplexity API requests');
// Create a textarea element for Perplexity
const perplexityTextArea = activeDocument.createEl('textarea');
perplexityTextArea.rows = 10;
perplexityTextArea.cols = 50;
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: unknown = JSON.parse(this.plugin.settings.perplexityRequestTemplate);
perplexityTextArea.value = JSON.stringify(config, null, 2);
} catch (e) {
// If not valid JSON, use as is
perplexityTextArea.value = this.plugin.settings.perplexityRequestTemplate;
}
}
// Add input event listener for Perplexity
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
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')
.setDesc('Your Anthropic API key. Read from ANTHROPIC_API_KEY in .env if set; can be overridden here.')
.addText(text => text
.setPlaceholder('Sk-ant-...')
.setValue(this.plugin.settings.anthropicApiKey)
.onChange(async (value) => {
this.plugin.settings.anthropicApiKey = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.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')
.setValue(this.plugin.settings.claudeDefaultModel)
.onChange(async (value) => {
this.plugin.settings.claudeDefaultModel = value;
await this.plugin.saveSettings();
}));
// Perplexica / Vane Section
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'
});
new Setting(containerEl)
.setName('Endpoint')
.setDesc('API endpoint for your local Perplexica / Vane instance')
.addText(text => text
.setPlaceholder('HTTP://localhost:3030/API/search')
.setValue(this.plugin.settings.perplexicaEndpoint)
.onChange(async (value: string) => {
this.plugin.settings.perplexicaEndpoint = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Fallback container path')
.setDesc('Alternative endpoint for docker container setups')
.addText(text => text
.setPlaceholder('HTTP://host.docker.internal:3030/API/search')
.setValue(this.plugin.settings.localLLMPath)
.onChange(async (value: string) => {
this.plugin.settings.localLLMPath = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Default model')
.setDesc('Default AI model for Perplexica / Vane to use')
.addText(text => text
.setPlaceholder('Llama3.2:latest')
.setValue(this.plugin.settings.defaultModel)
.onChange(async (value: string) => {
this.plugin.settings.defaultModel = value;
await this.plugin.saveSettings();
})
);
// Perplexica Request Template
const perplexicaJsonSetting = new Setting(containerEl)
.setName('Request body template')
.setDesc('JSON template for Perplexica / Vane API requests');
// Create a textarea element for Perplexica
const perplexicaTextArea = activeDocument.createEl('textarea');
perplexicaTextArea.rows = 10;
perplexicaTextArea.cols = 50;
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: unknown = JSON.parse(this.plugin.settings.requestBodyTemplate);
perplexicaTextArea.value = JSON.stringify(config, null, 2);
} catch (e) {
// If not valid JSON, use as is
perplexicaTextArea.value = this.plugin.settings.requestBodyTemplate;
}
}
// Add input event listener for Perplexica
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
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'
});
new Setting(containerEl)
.setName('Endpoint')
.setDesc('API endpoint for your local LM Studio instance')
.addText(text => text
.setPlaceholder('HTTP://localhost:1234/v1/chat/completions')
.setValue(this.plugin.settings.lmStudioEndpoint)
.onChange(async (value: string) => {
this.plugin.settings.lmStudioEndpoint = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Default model')
.setDesc('Default model name for LM Studio to use')
.addText(text => text
.setPlaceholder('Ibm/granite-3.2-8b')
.setValue(this.plugin.settings.defaultLMStudioModel)
.onChange(async (value: string) => {
this.plugin.settings.defaultLMStudioModel = value;
await this.plugin.saveSettings();
})
);
// LM Studio Request Template
const lmStudioJsonSetting = new Setting(containerEl)
.setName('Request body template')
.setDesc('JSON template for LM Studio API requests');
// Create a textarea element for LM Studio
const lmStudioTextArea = activeDocument.createEl('textarea');
lmStudioTextArea.rows = 10;
lmStudioTextArea.cols = 50;
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: unknown = JSON.parse(this.plugin.settings.lmStudioRequestTemplate);
lmStudioTextArea.value = JSON.stringify(config, null, 2);
} catch (e) {
// If not valid JSON, use as is
lmStudioTextArea.value = this.plugin.settings.lmStudioRequestTemplate;
}
}
// Add input event listener for LM Studio
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
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
new Setting(containerEl).setName("System prompts").setHeading();
new Setting(containerEl)
.setName('Perplexity system prompt')
.setDesc('System prompt used for Perplexity AI requests')
.addTextArea(text => text
.setPlaceholder('Enter system prompt for Perplexity...')
.setValue(this.plugin.settings.prompts.perplexitySystemPrompt)
.onChange(async (value: string) => {
this.plugin.settings.prompts.perplexitySystemPrompt = value;
const promptsService = this.plugin.getPromptsService();
if (promptsService) {
promptsService.updateSettings(this.plugin.settings.prompts);
}
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Perplexica / Vane system prompt')
.setDesc('System prompt used for Perplexica / Vane requests')
.addTextArea(text => text
.setPlaceholder('Enter system prompt for Perplexica / Vane...')
.setValue(this.plugin.settings.prompts.perplexicaSystemPrompt)
.onChange(async (value: string) => {
this.plugin.settings.prompts.perplexicaSystemPrompt = value;
const promptsService = this.plugin.getPromptsService();
if (promptsService) {
promptsService.updateSettings(this.plugin.settings.prompts);
}
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.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...')
.setValue(this.plugin.settings.prompts.lmStudioDefaultSystemPrompt)
.onChange(async (value: string) => {
this.plugin.settings.prompts.lmStudioDefaultSystemPrompt = value;
const promptsService = this.plugin.getPromptsService();
if (promptsService) {
promptsService.updateSettings(this.plugin.settings.prompts);
}
await this.plugin.saveSettings();
})
);
// Placeholder Text
new Setting(containerEl).setName("Placeholder text").setHeading();
new Setting(containerEl)
.setName('Perplexity query placeholder')
.setDesc('Placeholder text for Perplexity query input')
.addText(text => text
.setPlaceholder('Enter placeholder text...')
.setValue(this.plugin.settings.prompts.perplexityQueryPlaceholder)
.onChange(async (value: string) => {
this.plugin.settings.prompts.perplexityQueryPlaceholder = value;
const promptsService = this.plugin.getPromptsService();
if (promptsService) {
promptsService.updateSettings(this.plugin.settings.prompts);
}
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Perplexica / Vane query placeholder')
.setDesc('Placeholder text for Perplexica / Vane query input')
.addText(text => text
.setPlaceholder('Enter placeholder text...')
.setValue(this.plugin.settings.prompts.perplexicaQueryPlaceholder)
.onChange(async (value: string) => {
this.plugin.settings.prompts.perplexicaQueryPlaceholder = value;
const promptsService = this.plugin.getPromptsService();
if (promptsService) {
promptsService.updateSettings(this.plugin.settings.prompts);
}
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('LM Studio query placeholder')
.setDesc('Placeholder text for LM Studio query input')
.addText(text => text
.setPlaceholder('Enter placeholder text...')
.setValue(this.plugin.settings.prompts.lmStudioQueryPlaceholder)
.onChange(async (value: string) => {
this.plugin.settings.prompts.lmStudioQueryPlaceholder = value;
const promptsService = this.plugin.getPromptsService();
if (promptsService) {
promptsService.updateSettings(this.plugin.settings.prompts);
}
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('LM Studio system prompt placeholder')
.setDesc('Placeholder text for LM Studio system prompt input')
.addText(text => text
.setPlaceholder('Enter placeholder text...')
.setValue(this.plugin.settings.prompts.lmStudioSystemPromptPlaceholder)
.onChange(async (value: string) => {
this.plugin.settings.prompts.lmStudioSystemPromptPlaceholder = value;
const promptsService = this.plugin.getPromptsService();
if (promptsService) {
promptsService.updateSettings(this.plugin.settings.prompts);
}
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Article term placeholder')
.setDesc('Placeholder text for article generator term input')
.addText(text => text
.setPlaceholder('Enter placeholder text...')
.setValue(this.plugin.settings.prompts.articleTermPlaceholder)
.onChange(async (value: string) => {
this.plugin.settings.prompts.articleTermPlaceholder = value;
this.updatePromptsService();
await this.plugin.saveSettings();
})
);
// Article Generator Template
new Setting(containerEl).setName("Article generator template").setHeading();
const articleTemplateSetting = new Setting(containerEl)
.setName('Article generator template')
.setDesc('Template for generating articles. Use {TERM} as placeholder for the term.');
const articleTemplateTextArea = activeDocument.createEl('textarea');
articleTemplateTextArea.rows = 15;
articleTemplateTextArea.cols = 50;
articleTemplateTextArea.addClass('perplexed-json-textarea is-tall');
articleTemplateTextArea.placeholder = 'Enter article generator template...';
articleTemplateTextArea.value = this.plugin.settings.prompts.articleGeneratorTemplate;
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')
.setDesc('Template for generating articles with Deep Research model. Use {TERM} as placeholder for the term.');
const deepResearchTemplateTextArea = activeDocument.createEl('textarea');
deepResearchTemplateTextArea.rows = 20;
deepResearchTemplateTextArea.cols = 50;
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', () => void (async () => {
this.plugin.settings.prompts.deepResearchArticleTemplate = deepResearchTemplateTextArea.value;
this.updatePromptsService();
await this.plugin.saveSettings();
})());
deepResearchTemplateSetting.settingEl.appendChild(deepResearchTemplateTextArea);
// Image Prompts
new Setting(containerEl).setName("Image prompts").setHeading();
const imagePromptsSetting = new Setting(containerEl)
.setName('Image references prompt')
.setDesc('Prompt added to queries when images are enabled');
const imagePromptsTextArea = activeDocument.createEl('textarea');
imagePromptsTextArea.rows = 8;
imagePromptsTextArea.cols = 50;
imagePromptsTextArea.addClass('perplexed-json-textarea');
imagePromptsTextArea.placeholder = 'Enter image references prompt...';
imagePromptsTextArea.value = this.plugin.settings.prompts.imageReferencesPrompt;
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
new Setting(containerEl).setName("Text enhancement").setHeading();
const enhancePromptSetting = new Setting(containerEl)
.setName('Text enhancement prompt')
.setDesc('Template for enhancing selected text. Use {TEXT} as placeholder for the selected text.');
const enhancePromptTextArea = activeDocument.createEl('textarea');
enhancePromptTextArea.rows = 10;
enhancePromptTextArea.cols = 50;
enhancePromptTextArea.addClass('perplexed-json-textarea');
enhancePromptTextArea.placeholder = 'Enter text enhancement prompt template...';
enhancePromptTextArea.value = this.plugin.settings.prompts.enhancePrompt;
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')
.setDesc('Template for requesting related images for selected text. Use {TEXT} as placeholder for the selected text.');
const enhanceWithImagesPromptTextArea = activeDocument.createEl('textarea');
enhanceWithImagesPromptTextArea.rows = 10;
enhanceWithImagesPromptTextArea.cols = 50;
enhanceWithImagesPromptTextArea.addClass('perplexed-json-textarea');
enhanceWithImagesPromptTextArea.placeholder = 'Enter related images prompt template...';
enhanceWithImagesPromptTextArea.value = this.plugin.settings.prompts.enhanceWithImagesPrompt;
enhanceWithImagesPromptTextArea.addEventListener('input', () => void (async () => {
this.plugin.settings.prompts.enhanceWithImagesPrompt = enhanceWithImagesPromptTextArea.value;
this.updatePromptsService();
await this.plugin.saveSettings();
})());
enhanceWithImagesPromptSetting.settingEl.appendChild(enhanceWithImagesPromptTextArea);
// Directory Templates Section (v0.1 spike)
new Setting(containerEl).setName('Directory templates').setHeading();
containerEl.createEl('p', {
text: 'Apply a template (heading skeleton + per-section bullets) to fill a file via Perplexity deep research. Templates live in a vault folder and are matched to files by glob.',
cls: 'setting-item-description'
});
new Setting(containerEl)
.setName('Templates root')
.setDesc('Vault-relative folder where directory templates live.')
.addText(text => text
.setPlaceholder('Zz-cf-lib/templates')
.setValue(this.plugin.settings.directoryTemplatesRoot)
.onChange(async (value: string) => {
this.plugin.settings.directoryTemplatesRoot = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Frontmatter whitelist')
.setDesc('Comma-separated list of frontmatter keys passed to the template as {{frontmatter}}. Other keys are filtered out.')
.addText(text => text
.setPlaceholder('Title, og_description, tags, og_image')
.setValue(this.plugin.settings.directoryTemplatesFrontmatterWhitelist.join(', '))
.onChange(async (value: string) => {
this.plugin.settings.directoryTemplatesFrontmatterWhitelist = value
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Request timeout (ms)')
.setDesc('Maximum time to wait for the Perplexity deep research response. Default 300000 (5 min).')
.addText(text => text
.setPlaceholder('300000')
.setValue(String(this.plugin.settings.directoryTemplatesRequestTimeoutMs))
.onChange(async (value: string) => {
const n = parseInt(value, 10);
if (!isNaN(n) && n > 0) {
this.plugin.settings.directoryTemplatesRequestTimeoutMs = n;
await this.plugin.saveSettings();
}
})
);
// Find images for selection
new Setting(containerEl).setName('Find images for selection').setHeading();
containerEl.createEl('p', {
text: 'Highlight a passage, run "find images for selection". The plugin asks Perplexity for screenshots that visually illustrate the passage, prefers images on the entity\'s domain (frontmatter URL), and embeds them between paragraphs.',
cls: 'setting-item-description'
});
new Setting(containerEl)
.setName('Max images')
.setDesc('Maximum number of images to embed per invocation.')
.addText(text => text
.setPlaceholder('3')
.setValue(String(this.plugin.settings.findImagesMaxImages))
.onChange(async (value: string) => {
const n = parseInt(value, 10);
if (!isNaN(n) && n > 0) {
this.plugin.settings.findImagesMaxImages = n;
await this.plugin.saveSettings();
}
})
);
}
}