diff --git a/main.ts b/main.ts index 77486f4..fb16796 100644 --- a/main.ts +++ b/main.ts @@ -179,8 +179,10 @@ export default class PerplexedPlugin extends Plugin { await this.perplexityService.queryPerplexity(query, model, stream, editor, options); } - public async queryPerplexica(query: string, focusMode: string, optimizationMode: string, stream: boolean, editor: Editor): Promise { - await this.perplexicaService.queryPerplexica(query, focusMode, optimizationMode, stream, editor); + public async queryPerplexica(query: string, focusMode: string, optimizationMode: string, stream: boolean, editor: Editor, options?: { + return_images?: boolean; + }): Promise { + await this.perplexicaService.queryPerplexica(query, focusMode, optimizationMode, stream, editor, options); } public async queryLMStudio(query: string, model: string, stream: boolean, editor: Editor, options?: { @@ -188,6 +190,7 @@ export default class PerplexedPlugin extends Plugin { temperature?: number; top_p?: number; system_prompt?: string; + return_images?: boolean; }): Promise { await this.lmStudioService.queryLMStudio(query, model, stream, editor, options); } diff --git a/src/modals/LMStudioModal.ts b/src/modals/LMStudioModal.ts index a1c8bf3..eaaac8a 100644 --- a/src/modals/LMStudioModal.ts +++ b/src/modals/LMStudioModal.ts @@ -10,6 +10,7 @@ export class LMStudioModal extends Modal { private maxTokensInput!: HTMLInputElement; private temperatureInput!: HTMLInputElement; private systemPromptInput!: HTMLTextAreaElement; + private imagesToggle!: HTMLInputElement; constructor(app: App, editor: Editor, lmStudioService: LMStudioService) { super(app); @@ -79,6 +80,17 @@ export class LMStudioModal extends Modal { cls: 'text-input' }); + // Images toggle + const imagesDiv = form.createDiv({cls: 'setting-item'}); + const imagesLabel = imagesDiv.createEl('label'); + this.imagesToggle = imagesLabel.createEl('input', {type: 'checkbox'}); + this.imagesToggle.checked = false; + imagesLabel.createSpan({text: ' Include Images'}); + + // Add description for images toggle + const imagesDesc = imagesDiv.createDiv({cls: 'setting-item-description images-description'}); + imagesDesc.textContent = 'Include image references throughout the response where appropriate'; + // Stream toggle const streamDiv = form.createDiv({cls: 'setting-item'}); const streamLabel = streamDiv.createEl('label'); @@ -110,9 +122,22 @@ export class LMStudioModal extends Modal { return; } + // If images are enabled, add image markers to the query + let processedQuery = query; + if (this.imagesToggle.checked) { + processedQuery = `${query} + +**Image References:** +Please include the following image references throughout your response where appropriate: +- [IMAGE 1: Relevant diagram or illustration related to the topic] +- [IMAGE 2: Practical example or use case visualization] +- [IMAGE 3: Additional supporting visual content]`; + } + const options: LMStudioOptions = { max_tokens: parseInt(this.maxTokensInput.value) || 2048, - temperature: parseFloat(this.temperatureInput.value) || 0.7 + temperature: parseFloat(this.temperatureInput.value) || 0.7, + return_images: this.imagesToggle.checked }; const systemPrompt = this.systemPromptInput.value.trim(); @@ -122,7 +147,7 @@ export class LMStudioModal extends Modal { this.close(); await this.lmStudioService.queryLMStudio( - query, + processedQuery, this.modelSelect.value, this.streamToggle.checked, this.editor, diff --git a/src/modals/PerplexicaModal.ts b/src/modals/PerplexicaModal.ts index f818bee..5812e66 100644 --- a/src/modals/PerplexicaModal.ts +++ b/src/modals/PerplexicaModal.ts @@ -1,5 +1,5 @@ import { App, Modal, Notice, Editor } from 'obsidian'; -import { PerplexicaService } from '../services/perplexicaService'; +import { PerplexicaService, PerplexicaOptions } from '../services/perplexicaService'; export class PerplexicaModal extends Modal { private editor: Editor; @@ -8,6 +8,7 @@ export class PerplexicaModal extends Modal { private focusModeSelect!: HTMLSelectElement; private optimizationSelect!: HTMLSelectElement; private streamToggle!: HTMLInputElement; + private imagesToggle!: HTMLInputElement; constructor(app: App, editor: Editor, perplexicaService: PerplexicaService) { super(app); @@ -51,6 +52,17 @@ export class PerplexicaModal extends Modal { if (mode === 'balanced') option.selected = true; }); + // Images toggle + const imagesDiv = form.createDiv({cls: 'setting-item'}); + const imagesLabel = imagesDiv.createEl('label'); + this.imagesToggle = imagesLabel.createEl('input', {type: 'checkbox'}); + this.imagesToggle.checked = false; + imagesLabel.createSpan({text: ' Include Images'}); + + // Add description for images toggle + const imagesDesc = imagesDiv.createDiv({cls: 'setting-item-description images-description'}); + imagesDesc.textContent = 'Include image references throughout the response where appropriate'; + // Stream toggle const streamDiv = form.createDiv({cls: 'setting-item'}); const streamLabel = streamDiv.createEl('label'); @@ -82,13 +94,30 @@ export class PerplexicaModal extends Modal { return; } + // If images are enabled, add image markers to the query + let processedQuery = query; + if (this.imagesToggle.checked) { + processedQuery = `${query} + +**Image References:** +Please include the following image references throughout your response where appropriate: +- [IMAGE 1: Relevant diagram or illustration related to the topic] +- [IMAGE 2: Practical example or use case visualization] +- [IMAGE 3: Additional supporting visual content]`; + } + + const options: PerplexicaOptions = { + return_images: this.imagesToggle.checked + }; + this.close(); await this.perplexicaService.queryPerplexica( - query, + processedQuery, this.focusModeSelect.value, this.optimizationSelect.value, this.streamToggle.checked, - this.editor + this.editor, + options ); } diff --git a/src/modals/PerplexityModal.ts b/src/modals/PerplexityModal.ts index 8b8186c..b6a3bca 100644 --- a/src/modals/PerplexityModal.ts +++ b/src/modals/PerplexityModal.ts @@ -75,7 +75,7 @@ export class PerplexityModal extends Modal { // Add description for images toggle const imagesDesc = imagesDiv.createDiv({cls: 'setting-item-description images-description'}); - imagesDesc.textContent = 'Include image results from search - images will appear in an Images section'; + imagesDesc.textContent = 'Include image results from search - images will be integrated throughout the response where appropriate'; // Related questions toggle const relatedQuestionsDiv = form.createDiv({cls: 'setting-item'}); @@ -133,6 +133,18 @@ export class PerplexityModal extends Modal { return; } + // If images are enabled, add image markers to the query + let processedQuery = query; + if (this.imagesToggle.checked) { + processedQuery = `${query} + +**Image References:** +Please include the following image references throughout your response where appropriate: +- [IMAGE 1: Relevant diagram or illustration related to the topic] +- [IMAGE 2: Practical example or use case visualization] +- [IMAGE 3: Additional supporting visual content]`; + } + const options: PerplexityOptions = { return_citations: this.citationsToggle.checked, return_images: this.imagesToggle.checked, @@ -142,7 +154,7 @@ export class PerplexityModal extends Modal { this.close(); await this.perplexityService.queryPerplexity( - query, + processedQuery, this.modelSelect.value, this.streamToggle.checked, this.editor, diff --git a/src/services/lmStudioService.ts b/src/services/lmStudioService.ts index 0826e6c..f4c41e6 100644 --- a/src/services/lmStudioService.ts +++ b/src/services/lmStudioService.ts @@ -5,6 +5,7 @@ export interface LMStudioOptions { temperature?: number; top_p?: number; system_prompt?: string; + return_images?: boolean; } export interface LMStudioSettings { @@ -18,6 +19,30 @@ export class LMStudioService { this.settings = settings; } + private processContentWithImages(content: string): string { + // Find image markers like [IMAGE 1: description] or [IMAGE 1: description] + const imageRegex = /\[IMAGE\s+(\d+):\s*(.*?)\]/gi; + let match; + let imageIndex = 0; + + while ((match = imageRegex.exec(content)) !== null) { + const description = match[2]?.trim() || ''; + + // Create a placeholder image markdown with description + const imageMarkdown = `\n\n![${description}](https://via.placeholder.com/600x400/cccccc/666666?text=${encodeURIComponent(description)})\n*${description}*\n`; + + // Replace the marker with the placeholder image + content = content.replace(match[0], imageMarkdown); + imageIndex++; + } + + if (imageIndex > 0) { + console.log(`🔄 Processed ${imageIndex} image markers in LM Studio content`); + } + + return content; + } + public async queryLMStudio( query: string, model: string, @@ -85,9 +110,9 @@ export class LMStudioService { let finalCursor = responseCursor; if (stream) { - await this.handleStreamingResponse(response, editor, responseCursor); + await this.handleStreamingResponse(response, editor, responseCursor, options); } else { - await this.handleNonStreamingResponse(response, editor, responseCursor); + await this.handleNonStreamingResponse(response, editor, responseCursor, options); } // Add separator at the final cursor position @@ -103,7 +128,8 @@ export class LMStudioService { private async handleStreamingResponse( response: Response, editor: Editor, - responseCursor: { line: number; ch: number } + responseCursor: { line: number; ch: number }, + options?: LMStudioOptions ): Promise { const reader = response.body?.getReader(); if (!reader) throw new Error('No response body'); @@ -130,7 +156,13 @@ export class LMStudioService { try { const parsed = JSON.parse(data); if (parsed.choices?.[0]?.delta?.content) { - const content = parsed.choices[0].delta.content; + let content = parsed.choices[0].delta.content; + + // Process images if enabled + if (options?.return_images) { + content = this.processContentWithImages(content); + } + editor.replaceRange(content, currentPos); // Update cursor position after insertion const lines = content.split('\n'); @@ -158,11 +190,18 @@ export class LMStudioService { private async handleNonStreamingResponse( response: Response, editor: Editor, - responseCursor: { line: number; ch: number } + responseCursor: { line: number; ch: number }, + options?: LMStudioOptions ): Promise { const data = await response.json(); const content = data.choices?.[0]?.message?.content || 'No response received'; - editor.replaceRange(content, responseCursor); + // Process images if enabled + let processedContent = content; + if (options?.return_images) { + processedContent = this.processContentWithImages(content); + } + + editor.replaceRange(processedContent, responseCursor); } } \ No newline at end of file diff --git a/src/services/perplexicaService.ts b/src/services/perplexicaService.ts index 3c69c31..abc2e84 100644 --- a/src/services/perplexicaService.ts +++ b/src/services/perplexicaService.ts @@ -6,6 +6,10 @@ export interface PerplexicaSettings { defaultModel: string; } +export interface PerplexicaOptions { + return_images?: boolean; +} + export class PerplexicaService { private settings: PerplexicaSettings; @@ -13,12 +17,37 @@ export class PerplexicaService { this.settings = settings; } + private processContentWithImages(content: string): string { + // Find image markers like [IMAGE 1: description] or [IMAGE 1: description] + const imageRegex = /\[IMAGE\s+(\d+):\s*(.*?)\]/gi; + let match; + let imageIndex = 0; + + while ((match = imageRegex.exec(content)) !== null) { + const description = match[2]?.trim() || ''; + + // Create a placeholder image markdown with description + const imageMarkdown = `\n\n![${description}](https://via.placeholder.com/600x400/cccccc/666666?text=${encodeURIComponent(description)})\n*${description}*\n`; + + // Replace the marker with the placeholder image + content = content.replace(match[0], imageMarkdown); + imageIndex++; + } + + if (imageIndex > 0) { + console.log(`🔄 Processed ${imageIndex} image markers in Perplexica content`); + } + + return content; + } + public async queryPerplexica( query: string, focusMode: string, optimizationMode: string, stream: boolean, - editor: Editor + editor: Editor, + options?: PerplexicaOptions ): Promise { const timestamp = new Date().toISOString(); @@ -77,9 +106,9 @@ export class PerplexicaService { } if (stream) { - await this.handleStreamingResponse(response, editor, responseCursor); + await this.handleStreamingResponse(response, editor, responseCursor, options); } else { - await this.handleNonStreamingResponse(response, editor, responseCursor); + await this.handleNonStreamingResponse(response, editor, responseCursor, options); } // Add separator @@ -102,7 +131,7 @@ export class PerplexicaService { } } - private async handleStreamingResponse(response: Response, editor: Editor, responseCursor: { line: number; ch: number }): Promise { + private async handleStreamingResponse(response: Response, editor: Editor, responseCursor: { line: number; ch: number }, options?: PerplexicaOptions): Promise { const reader = response.body?.getReader(); if (!reader) throw new Error('No response body'); @@ -117,23 +146,30 @@ export class PerplexicaService { for (const line of lines) { try { const parsed = JSON.parse(line); - if (parsed.type === 'response' && parsed.data) { - editor.replaceRange(parsed.data, currentPos); - // Update cursor position - const lines = parsed.data.split('\n'); - if (lines.length === 1) { - currentPos = { line: currentPos.line, ch: currentPos.ch + parsed.data.length }; - } else { - currentPos = { - line: currentPos.line + lines.length - 1, - ch: lines[lines.length - 1]?.length || 0 - }; + if (parsed.type === 'response' && parsed.data) { + let content = parsed.data; + + // Process images if enabled + if (options?.return_images) { + content = this.processContentWithImages(content); + } + + editor.replaceRange(content, currentPos); + // Update cursor position + const lines = content.split('\n'); + if (lines.length === 1) { + currentPos = { line: currentPos.line, ch: currentPos.ch + content.length }; + } else { + currentPos = { + line: currentPos.line + lines.length - 1, + ch: lines[lines.length - 1]?.length || 0 + }; + } + // 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)); } - // 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)); - } } catch (e) { // Ignore JSON parse errors } @@ -141,8 +177,15 @@ export class PerplexicaService { } } - private async handleNonStreamingResponse(response: Response, editor: Editor, responseCursor: { line: number; ch: number }): Promise { + private async handleNonStreamingResponse(response: Response, editor: Editor, responseCursor: { line: number; ch: number }, options?: PerplexicaOptions): Promise { const text = await response.text(); - editor.replaceRange(text, responseCursor); + + // Process images if enabled + let processedText = text; + if (options?.return_images) { + processedText = this.processContentWithImages(text); + } + + editor.replaceRange(processedText, responseCursor); } } \ No newline at end of file