diff --git a/main.ts b/main.ts index 4707a7b..bad13ab 100644 --- a/main.ts +++ b/main.ts @@ -1,155 +1,456 @@ -import { Notice, Plugin, Editor } from 'obsidian'; -import { citationService } from './src/services/citationService'; -import { CitationModal } from './src/modals/CitationModal'; -import { cleanReferencesSectionService } from './src/services/cleanReferencesSectionService'; +import { App, Editor, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import * as dotenv from 'dotenv'; + +// Load environment variables +dotenv.config({ path: `${process.cwd()}/.env` }); + +interface PerplexedPluginSettings { + mySetting: string; + localLLMPath: string; + requestBodyTemplate: string; + perplexityRequestTemplate: string; + perplexityApiKey: string; + perplexicaEndpoint: string; + perplexityEndpoint: string; + defaultModel: string; + defaultOptimizationMode: string; + defaultFocusMode: string; +} + +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', + 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": "You are a helpful AI assistant. Provide clear, concise, and accurate information.", + "stream": false, + "maxTokens": 2048, + "temperature": 0.7 +}`, + perplexityApiKey: process.env.PERPLEXITY_API_KEY || '', + perplexityRequestTemplate: `{ + "model": "llama-3.1-sonar-small-128k-online", + "messages": [ + { + "role": "system", + "content": "You are a helpful AI assistant. Provide clear, concise, and accurate information with proper citations." + }, + { + "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 +}`, + defaultModel: 'llama3.2:latest', + defaultOptimizationMode: 'speed', + defaultFocusMode: 'webSearch' +}; + +export default class PerplexedPlugin extends Plugin { + public settings: PerplexedPluginSettings = DEFAULT_SETTINGS; + private statusBarItemEl: HTMLElement | null = null; + private ribbonIconEl: HTMLElement | null = null; -export default class CiteWidePlugin extends Plugin { async onload(): Promise { - // Load CSS - this.loadStyles(); + await this.loadSettings(); + + // Debug: Log current settings + console.log('Current Perplexica Path:', this.settings.perplexicaEndpoint); + console.log('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)); // Register commands - this.registerCitationCommands(); - this.registerReferenceCleanupCommands(); - this.registerCitationFormattingCommands(); + this.registerPerplexicaCommands(); + this.registerPerplexityCommands(); } - - private async loadStyles() { + + onunload(): void { + this.statusBarItemEl?.remove(); + this.ribbonIconEl?.remove(); + } + + private async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + public async saveSettings(): Promise { try { - const cssPath = this.manifest.dir + '/styles.css'; - const response = await fetch(cssPath); - if (!response.ok) throw new Error('Failed to load CSS'); - - const css = await response.text(); - const styleEl = document.createElement('style'); - styleEl.id = 'cite-wide-styles'; - styleEl.textContent = css; - document.head.appendChild(styleEl); + await this.saveData(this.settings); } catch (error) { - console.error('Error loading styles:', error); + console.error('Failed to save settings:', error); + new Notice('Failed to save settings'); } } - private registerCitationCommands(): void { - // Command to show citations in current file + private registerPerplexicaCommands(): void { + // Command to update Perplexica URL this.addCommand({ - id: 'show-citations', - name: 'Show Citations in Current File', - editorCallback: (editor: Editor) => { - new CitationModal(this.app, editor).open(); - } - }); + id: 'update-perplexica-url', + name: 'Update Perplexica URL', + callback: () => { + const modal = new (class extends Modal { + private urlInput!: HTMLInputElement; - // Command to convert all citations to hex format - this.addCommand({ - id: 'convert-all-citations', - name: 'Convert All Citations to Hex Format', - editorCallback: async (editor: Editor) => { - try { - const content = editor.getValue(); - // Get all citation groups - const groups = citationService.findCitations(content); - let totalConverted = 0; - let updatedContent = content; + constructor(app: App, private plugin: PerplexedPlugin) { + super(app); + } - // Convert each citation group - for (const group of groups) { - const result = citationService.convertCitation(updatedContent, group.number); - if (result.changed) { - updatedContent = result.content; - totalConverted += result.stats.citationsConverted; + onOpen() { + const {contentEl} = this; + contentEl.createEl('h2', {text: 'Update Perplexica API URL'}); + + const form = contentEl.createEl('form'); + const div = form.createDiv({cls: 'setting-item'}); + + div.createEl('label', { + text: 'Perplexica API URL', + attr: {for: 'perplexica-url-input'} + }); + + this.urlInput = div.createEl('input', { + type: 'text', + value: this.plugin.settings.perplexicaEndpoint, + cls: 'text-input', + attr: {id: 'perplexica-url-input'} + }); + + const buttonDiv = contentEl.createDiv({cls: 'setting-item'}); + const saveButton = buttonDiv.createEl('button', { + text: 'Save', + cls: 'mod-cta' + }); + + form.onsubmit = (e) => { + e.preventDefault(); + this.onSubmit(); + }; + + saveButton.onclick = () => this.onSubmit(); + } + + onSubmit() { + const newUrl = this.urlInput.value.trim(); + if (newUrl) { + this.plugin.settings.perplexicaEndpoint = newUrl; + this.plugin.saveSettings(); + new Notice(`Perplexica URL updated to: ${newUrl}`); + this.close(); } } - if (totalConverted > 0) { - editor.setValue(updatedContent); - new Notice(`Updated ${totalConverted} citations`); - } else { - new Notice('No citations needed conversion'); + onClose() { + const {contentEl} = this; + contentEl.empty(); } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - new Notice('Error processing citations: ' + errorMsg); - } + })(this.app, this); + + modal.open(); + } + }); + + // Command to show current settings + this.addCommand({ + id: 'show-perplexica-settings', + name: 'Show Perplexica Settings', + callback: () => { + new Notice(`Current Perplexica URL: ${this.settings.perplexicaEndpoint}`); + console.log('Perplexica Settings:', this.settings); } }); - // Command to insert a new citation + // Command to insert Perplexica request template this.addCommand({ - id: 'insert-hex-citation', - name: 'Insert Hex Citation', + id: 'insert-perplexica-template', + name: 'Insert Perplexica Request Template', editorCallback: (editor: Editor) => { - try { - const cursor = editor.getCursor(); - const hexId = citationService.getNewHexId(); - - // Insert the citation reference at cursor - editor.replaceRange(`[^${hexId}]`, cursor); - - // Add the footnote definition at the end - const content = editor.getValue(); - const footnotePosition = { - line: content.split('\n').length, - ch: 0 - }; - - editor.replaceRange(`\n\n[^${hexId}]: `, footnotePosition); - - // Position cursor after the inserted citation - const newPos = { - line: footnotePosition.line + 2, // +2 for the two newlines - ch: `[^${hexId}]: `.length - }; - editor.setCursor(newPos); - - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - new Notice('Error inserting citation: ' + errorMsg); - } + const template = this.settings.requestBodyTemplate; + editor.replaceSelection( + '```json\n' + + template + '\n' + + '```' + ); } }); } - private registerReferenceCleanupCommands(): void { - // Command to clean up references section + private registerPerplexityCommands(): void { + // Command to update Perplexity URL this.addCommand({ - id: 'clean-references-section', - name: 'Add Colon to Footnote References in Selection', - editorCallback: (editor: Editor) => { - const selection = editor.getSelection(); - if (!selection) { - new Notice('Please select some text first'); - return; - } - - const processed = cleanReferencesSectionService.addColonSyntaxWhereNone(selection); - editor.replaceSelection(processed); - new Notice('References cleaned up successfully'); + id: 'update-perplexity-url', + name: 'Update Perplexity URL', + callback: () => { + const modal = new (class extends Modal { + private urlInput!: HTMLInputElement; + + constructor(app: App, private plugin: PerplexedPlugin) { + super(app); + } + + onOpen() { + const {contentEl} = this; + contentEl.createEl('h2', {text: 'Update Perplexity API URL'}); + const form = contentEl.createEl('form'); + const div = form.createDiv({cls: 'setting-item'}); + + div.createEl('label', { + text: 'Perplexity API URL', + attr: {for: 'perplexity-url-input'} + }); + + this.urlInput = div.createEl('input', { + type: 'text', + value: this.plugin.settings.perplexityEndpoint, + cls: 'text-input', + attr: {id: 'perplexity-url-input'} + }); + + const buttonDiv = contentEl.createDiv({cls: 'setting-item'}); + const saveButton = buttonDiv.createEl('button', { + text: 'Save', + cls: 'mod-cta' + }); + + form.onsubmit = (e) => { + e.preventDefault(); + this.onSubmit(); + }; + + saveButton.onclick = () => this.onSubmit(); + } + + onSubmit() { + const newUrl = this.urlInput.value.trim(); + if (newUrl) { + this.plugin.settings.perplexityEndpoint = newUrl; + this.plugin.saveSettings(); + new Notice(`Perplexity URL updated to: ${newUrl}`); + this.close(); + } + } + + onClose() { + const {contentEl} = this; + contentEl.empty(); + } + })(this.app, this); + + 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.log('Perplexity Settings:', this.settings); + } + }); + + // Command to insert Perplexity request template + this.addCommand({ + id: 'insert-perplexity-template', + name: 'Insert Perplexity Request Template', + editorCallback: (editor: Editor) => { + const template = this.settings.perplexityRequestTemplate; + editor.replaceSelection( + '```json\n' + + template + '\n' + + '```' + ); + } + }); + } +} + +class PerplexedSettingTab extends PluginSettingTab { + plugin: PerplexedPlugin; + + constructor(app: App, plugin: PerplexedPlugin) { + super(app, plugin); + this.plugin = plugin; } - private registerCitationFormattingCommands(): void { - // Command to format citations by moving them after punctuation and ensuring proper spacing - this.addCommand({ - id: 'format-citations-punctuation', - name: 'Move Citations after Punctuation', - editorCallback: (editor: Editor) => { - // Process the entire document content - const content = editor.getValue(); - - // First move citations after punctuation, then ensure proper spacing - let processed = citationService.moveCitationsBehindPunctuation(content); - processed = citationService.assureSpacingBetweenCitations(processed); - - // Only update if there were changes - if (processed !== content) { - editor.setValue(processed); - new Notice('Formatted citations in document'); - } else { - new Notice('No citations needed formatting'); - } - } + display(): void { + const { containerEl } = this; + containerEl.empty(); + + containerEl.createEl('h2', { text: 'Perplexed Plugin Settings' }); + + // Perplexity Section + containerEl.createEl('h3', { text: 'Perplexity (Remote Service)' }); + 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(); + }) + ); + + // Perplexica Section + containerEl.createEl('h3', { text: 'Perplexica (Self-Hosted)' }); + containerEl.createEl('p', { + text: 'Configure settings for your local Perplexica installation', + cls: 'setting-item-description' + }); + + new Setting(containerEl) + .setName('Endpoint') + .setDesc('API endpoint for your local Perplexica 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 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(); + }) + ); + + // 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 = document.createElement('textarea'); + perplexityTextArea.rows = 10; + perplexityTextArea.cols = 50; + perplexityTextArea.style.width = '100%'; + perplexityTextArea.style.minHeight = '300px'; + perplexityTextArea.style.fontFamily = 'monospace'; + 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); + 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', async () => { + this.plugin.settings.perplexityRequestTemplate = perplexityTextArea.value; + await this.plugin.saveSettings(); + }); + + // Add the textarea to the setting + perplexityJsonSetting.settingEl.appendChild(perplexityTextArea); + + // Perplexica Request Template + const perplexicaJsonSetting = new Setting(containerEl) + .setName('Request Body Template') + .setDesc('JSON template for Perplexica API requests'); + + // Create a textarea element for Perplexica + const perplexicaTextArea = document.createElement('textarea'); + perplexicaTextArea.rows = 10; + perplexicaTextArea.cols = 50; + perplexicaTextArea.style.width = '100%'; + perplexicaTextArea.style.minHeight = '300px'; + perplexicaTextArea.style.fontFamily = 'monospace'; + 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); + 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', async () => { + this.plugin.settings.requestBodyTemplate = perplexicaTextArea.value; + await this.plugin.saveSettings(); + }); + + // Add the textarea to the setting + perplexicaJsonSetting.settingEl.appendChild(perplexicaTextArea); } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c0cdbae Binary files /dev/null and b/package-lock.json differ diff --git a/package.json b/package.json index 66852d5..b6b1a50 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.15.0", + "dotenv": "^17.2.0", "fastify": "^5.4.0", "zod": "^4.0.0" } diff --git a/styles.css b/styles.css index 0cfbc59..18c12e7 100644 --- a/styles.css +++ b/styles.css @@ -1,90 +1 @@ -/* src/styles/citations.css */ -.cite-wide-modal { - padding: 1.5rem; - max-width: 800px; - margin: 0 auto; -} -.cite-wide-title { - margin-top: 0; - padding-bottom: 0.75rem; - border-bottom: 1px solid var(--background-modifier-border); -} -.cite-wide-container { - margin-top: 1rem; -} -.cite-wide-group { - margin-bottom: 1.5rem; - border: 1px solid var(--background-modifier-border); - border-radius: 6px; - overflow: hidden; -} -.cite-wide-group-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - background-color: var(--background-secondary); - cursor: pointer; - transition: background-color 0.2s; -} -.cite-wide-group-header:hover { - background-color: var(--background-modifier-hover); -} -.cite-wide-group-header-content { - display: flex; - align-items: center; - gap: 0.5rem; -} -.cite-wide-group-title { - margin: 0; - font-size: 1rem; - font-weight: 600; -} -.cite-wide-source-link { - font-size: 0.85rem; - opacity: 0.8; - text-decoration: none; -} -.cite-wide-group-content { - padding: 0.75rem 1rem; - background-color: var(--background-primary); - border-top: 1px solid var(--background-modifier-border); -} -.cite-wide-instance { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.5rem 0; - border-bottom: 1px solid var(--background-modifier-border); -} -.cite-wide-instance:last-child { - border-bottom: none; -} -.cite-wide-line-info { - flex: 1; - font-family: var(--font-monospace); - font-size: 0.9em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.cite-wide-line-number { - color: var(--text-muted); - margin-right: 0.5rem; -} -.cite-wide-line-preview { - opacity: 0.9; -} -.cite-wide-convert-btn { - margin-left: 0.5rem; -} -.cite-wide-view-btn { - margin-left: 0.5rem; -} -.cite-wide-footer { - margin-top: 1.5rem; - padding-top: 1rem; - border-top: 1px solid var(--background-modifier-border); - display: flex; - justify-content: flex-end; -} +.cite-wide-modal{padding:1.5rem;max-width:800px;margin:0 auto}.cite-wide-title{margin-top:0;padding-bottom:.75rem;border-bottom:1px solid var(--background-modifier-border)}.cite-wide-container{margin-top:1rem}.cite-wide-group{margin-bottom:1.5rem;border:1px solid var(--background-modifier-border);border-radius:6px;overflow:hidden}.cite-wide-group-header{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background-color:var(--background-secondary);cursor:pointer;transition:background-color .2s}.cite-wide-group-header:hover{background-color:var(--background-modifier-hover)}.cite-wide-group-header-content{display:flex;align-items:center;gap:.5rem}.cite-wide-group-title{margin:0;font-size:1rem;font-weight:600}.cite-wide-source-link{font-size:.85rem;opacity:.8;text-decoration:none}.cite-wide-group-content{padding:.75rem 1rem;background-color:var(--background-primary);border-top:1px solid var(--background-modifier-border)}.cite-wide-instance{display:flex;justify-content:space-between;align-items:center;padding:.5rem 0;border-bottom:1px solid var(--background-modifier-border)}.cite-wide-instance.cite-wide-reference-source{background-color:var(--background-secondary-alt);margin:0 -1rem;padding:.75rem 1rem;border-radius:4px;border:1px solid var(--background-modifier-border);border-left:3px solid var(--interactive-accent)}.cite-wide-instance:last-child{border-bottom:none}.cite-wide-line-info{flex:1;font-family:var(--font-monospace);font-size:.9em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cite-wide-line-number{color:var(--text-muted);margin-right:.5rem}.cite-wide-line-preview{opacity:.9}.cite-wide-badge{display:inline-block;padding:.15em .5em;font-size:.75em;font-weight:600;line-height:1.2;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:3px;text-transform:uppercase;letter-spacing:.5px}.cite-wide-badge-reference{color:var(--text-normal);background-color:var(--background-modifier-border)}.cite-wide-line-info>.cite-wide-badge+span{margin-left:.5rem}.cite-wide-instance-actions{display:flex;gap:.5rem;margin-left:.75rem}.cite-wide-convert-btn,.cite-wide-view-btn{white-space:nowrap;padding:.25rem .75rem;font-size:.85em;line-height:1.4;border-radius:4px;transition:all .1s ease}.cite-wide-convert-btn{background-color:var(--interactive-accent);color:var(--text-on-accent);border:1px solid var(--interactive-accent-hover)}.cite-wide-convert-btn:hover{background-color:var(--interactive-accent-hover)}.cite-wide-view-btn{background-color:var(--background-secondary);border:1px solid var(--background-modifier-border)}.cite-wide-view-btn:hover{background-color:var(--background-modifier-hover)}.cite-wide-footer{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--background-modifier-border);display:flex;justify-content:flex-end}