mirror of
https://github.com/travisvn/obsidian-vision-recall.git
synced 2026-07-22 05:38:13 +00:00
Enhance configuration options
- Introduced new configuration options for vision and notes LLM prompts - Added category detection feature toggle in ConfigModal - Updated ScreenshotProcessor to utilize new prompt configurations - Cleaned up HelpModal by removing outdated forum links
This commit is contained in:
parent
317d3c2f6f
commit
773cb610e6
10 changed files with 163 additions and 38 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "vision-recall",
|
||||
"name": "Vision Recall",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"minAppVersion": "1.8.3",
|
||||
"description": "Transform screenshots into searchable notes using AI vision and text analysis.",
|
||||
"author": "Travis Van Nimwegen",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-vision-recall",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Transform screenshots into searchable Obsidian notes using AI vision and text analysis.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { DocViewerModal } from '@/components/modals/DocViewerModal';
|
|||
import { ConfigModal } from '@/components/modals/ConfigModal';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useDataContext } from '@/data/DataContext';
|
||||
import { TestConfigModal } from './modals/TestConfigModal';
|
||||
import { TestSetupModal } from './modals/TestSetupModal';
|
||||
import { ProcessingQueueModal } from './modals/ProcessingQueueModal';
|
||||
import { useQueueStore } from '@/stores/queueStore';
|
||||
import { DebugOperationsModal } from './modals/DebugOperationsModal';
|
||||
|
|
@ -166,10 +166,10 @@ export const MainViewHeader = ({ metadata, refreshMetadata, viewMode = 'list', o
|
|||
</button>
|
||||
|
||||
<button
|
||||
aria-label='Test config'
|
||||
aria-label='Test setup'
|
||||
className='cursor-pointer flex flex-row items-center gap-2'
|
||||
onClick={() => {
|
||||
new TestConfigModal(
|
||||
new TestSetupModal(
|
||||
app,
|
||||
plugin,
|
||||
dataManager
|
||||
|
|
@ -178,7 +178,7 @@ export const MainViewHeader = ({ metadata, refreshMetadata, viewMode = 'list', o
|
|||
>
|
||||
<Bug className='w-4 h-4' />
|
||||
<div className='hidden @3xl/subheader:block'>
|
||||
Test config
|
||||
Test setup
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ export const MainViewHeader = ({ metadata, refreshMetadata, viewMode = 'list', o
|
|||
</div>
|
||||
|
||||
<button
|
||||
aria-label='Advanced settings'
|
||||
aria-label='Advanced configuration'
|
||||
className='cursor-pointer flex flex-row items-center gap-2'
|
||||
onClick={() => {
|
||||
new ConfigModal(app, plugin, plugin.dataManager).open();
|
||||
|
|
@ -272,7 +272,7 @@ export const MainViewHeader = ({ metadata, refreshMetadata, viewMode = 'list', o
|
|||
>
|
||||
<Settings2 className='w-4 h-4' />
|
||||
<div className='hidden @3xl/subheader:block'>
|
||||
Settings
|
||||
Config
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Modal } from 'obsidian';
|
||||
import { DataManager } from '@/data/DataManager';
|
||||
import { Plugin } from 'obsidian';
|
||||
import { Config, DefaultConfig } from '@/types/config-types';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ConfigModalContent = ({
|
||||
dataManager,
|
||||
|
|
@ -16,11 +17,17 @@ const ConfigModalContent = ({
|
|||
const [config, setConfig] = useState<Config>({});
|
||||
const [dirty, setDirty] = useState(false);
|
||||
|
||||
const [enableCategoryDetection, setEnableCategoryDetection] = useState(false);
|
||||
|
||||
const promptTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const notesPromptTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialized) return;
|
||||
const loadConfig = async () => {
|
||||
const currentConfig = dataManager.getConfig();
|
||||
setConfig(currentConfig);
|
||||
setEnableCategoryDetection(currentConfig.enableCategoryDetection);
|
||||
setInitialized(true);
|
||||
};
|
||||
loadConfig();
|
||||
|
|
@ -34,9 +41,26 @@ const ConfigModalContent = ({
|
|||
|
||||
const updateConfig = (updates: Partial<Config>) => {
|
||||
setConfig(prev => ({ ...prev, ...updates }));
|
||||
if (updates.enableCategoryDetection !== undefined) {
|
||||
setEnableCategoryDetection(updates.enableCategoryDetection);
|
||||
}
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
const resetVisionLLMPrompt = () => {
|
||||
if (promptTextareaRef.current) {
|
||||
promptTextareaRef.current.value = DefaultConfig.visionLLMPrompt;
|
||||
}
|
||||
updateConfig({ visionLLMPrompt: DefaultConfig.visionLLMPrompt });
|
||||
};
|
||||
|
||||
const resetNotesLLMPrompt = () => {
|
||||
if (notesPromptTextareaRef.current) {
|
||||
notesPromptTextareaRef.current.value = DefaultConfig.notesLLMPrompt;
|
||||
}
|
||||
updateConfig({ notesLLMPrompt: DefaultConfig.notesLLMPrompt });
|
||||
};
|
||||
|
||||
if (!initialized) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
|
@ -96,6 +120,83 @@ const ConfigModalContent = ({
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div className='config-field'>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className='flex flex-row gap-1 w-full items-center'>
|
||||
<label className='text-sm font-bold flex-1'>
|
||||
Vision LLM prompt
|
||||
</label>
|
||||
<button
|
||||
aria-label="Reset vision LLM prompt to default"
|
||||
type="button"
|
||||
className="mod-warning cursor-pointer opacity-50 hover:opacity-100 duration-300 text-xs px-2 py-1"
|
||||
onClick={resetVisionLLMPrompt}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
ref={promptTextareaRef}
|
||||
className='w-full flex-1'
|
||||
defaultValue={config.visionLLMPrompt || DefaultConfig.visionLLMPrompt}
|
||||
onChange={e => updateConfig({ visionLLMPrompt: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div className="config-field">
|
||||
<label
|
||||
className="config-field-checkbox-label"
|
||||
aria-label={`Attempts to categorize screenshots based on vision LLM response. If enabled, the notes LLM prompt will be modified to include the category.`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={config.enableCategoryDetection || DefaultConfig.enableCategoryDetection}
|
||||
onChange={e => updateConfig({ enableCategoryDetection: e.target.checked })}
|
||||
/>
|
||||
Enable category detection
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='config-field'>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className='flex flex-row gap-1 w-full items-center'>
|
||||
<label
|
||||
className={cn(
|
||||
'text-sm font-bold flex-1',
|
||||
enableCategoryDetection && 'opacity-30 hover:opacity-70 duration-300'
|
||||
)}
|
||||
aria-label={!enableCategoryDetection ? 'Notes LLM prompt' : 'Notes LLM prompt (disabled because category detection is enabled)'}
|
||||
>
|
||||
Notes LLM prompt
|
||||
</label>
|
||||
<button
|
||||
aria-label="Reset notes LLM prompt to default"
|
||||
type="button"
|
||||
className="mod-warning cursor-pointer opacity-30 hover:opacity-70 duration-300 text-xs px-2 py-1"
|
||||
onClick={resetNotesLLMPrompt}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
ref={notesPromptTextareaRef}
|
||||
aria-label={!enableCategoryDetection ? 'Notes LLM prompt' : 'Notes LLM prompt (disabled because category detection is enabled)'}
|
||||
className={cn(
|
||||
'w-full flex-1',
|
||||
enableCategoryDetection && 'opacity-30 hover:opacity-70 duration-300'
|
||||
)}
|
||||
disabled={enableCategoryDetection}
|
||||
defaultValue={config.notesLLMPrompt || DefaultConfig.notesLLMPrompt}
|
||||
onChange={e => updateConfig({ notesLLMPrompt: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* <div className="config-field">
|
||||
<label className="config-field-checkbox-label">
|
||||
|
|
|
|||
|
|
@ -41,18 +41,6 @@ const HelpView: React.FC<HelpModalProps> = ({ plugin, onClose }) => {
|
|||
>
|
||||
Join the new Discord server <ExternalLink className='w-3 h-3' />
|
||||
</a>
|
||||
|
||||
<div className='text-text-muted'>or</div>
|
||||
|
||||
<a
|
||||
href='https://forum.visionrecall.com'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='cursor-pointer inline-flex flex-row items-center gap-1'
|
||||
aria-label={'Open the forum'}
|
||||
>
|
||||
Visit & chat on the forum <ExternalLink className='w-3 h-3' />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end items-center w-full">
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import { DateTime } from 'luxon';
|
|||
import { DataManager } from '@/data/DataManager';
|
||||
import { getModels } from '@/services/llm-service';
|
||||
|
||||
interface TestConfigModalProps {
|
||||
interface TestSetupModalProps {
|
||||
dataManager: DataManager;
|
||||
plugin: VisionRecallPlugin;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TestConfigView: React.FC<TestConfigModalProps> = ({ dataManager, plugin, onClose }) => {
|
||||
const TestSetupView: React.FC<TestSetupModalProps> = ({ dataManager, plugin, onClose }) => {
|
||||
const settings = plugin.settings;
|
||||
|
||||
const [models, setModels] = useState<string[]>([]);
|
||||
|
|
@ -127,7 +127,7 @@ const TestConfigView: React.FC<TestConfigModalProps> = ({ dataManager, plugin, o
|
|||
);
|
||||
};
|
||||
|
||||
export class TestConfigModal extends Modal {
|
||||
export class TestSetupModal extends Modal {
|
||||
private metadata: any;
|
||||
private plugin: VisionRecallPlugin;
|
||||
|
||||
|
|
@ -139,11 +139,11 @@ export class TestConfigModal extends Modal {
|
|||
|
||||
onOpen() {
|
||||
const { contentEl, titleEl } = this;
|
||||
titleEl.setText('Test config');
|
||||
titleEl.setText('Test your setup');
|
||||
const root = createRoot(contentEl);
|
||||
|
||||
root.render(
|
||||
<TestConfigView
|
||||
<TestSetupView
|
||||
dataManager={this.plugin.dataManager}
|
||||
plugin={this.plugin}
|
||||
onClose={() => this.close()}
|
||||
|
|
@ -1,11 +1,30 @@
|
|||
import { OPENROUTER_HEADERS } from '@/constants';
|
||||
import { getLanguagePromptModifierIfIndicated } from '@/lib/languages';
|
||||
import { tagsJsonSchema, TagsSchema } from '@/lib/tag-utils';
|
||||
import { Config } from '@/types/config-types';
|
||||
import { VisionRecallPluginSettings } from '@/types/settings-types';
|
||||
import { requestUrl, RequestUrlParam, RequestUrlResponse } from 'obsidian';
|
||||
|
||||
export const VISION_LLM_PROMPT = "Analyze this screenshot and describe its content and identify the type of screenshot if possible.";
|
||||
|
||||
export const NOTES_LLM_PROMPT = "The following OCR text and vision analysis are from a screenshot. Summarize and synthesize the text and vision analysis and identify key information.";
|
||||
|
||||
export const getVisionLLMPrompt = (config: Config) => {
|
||||
if (config.visionLLMPrompt && config.visionLLMPrompt.trim() !== '') {
|
||||
return config.visionLLMPrompt;
|
||||
}
|
||||
|
||||
return VISION_LLM_PROMPT;
|
||||
}
|
||||
|
||||
export const getNotesLLMPrompt = (config: Config) => {
|
||||
if (config.notesLLMPrompt && config.notesLLMPrompt.trim() !== '') {
|
||||
return config.notesLLMPrompt;
|
||||
}
|
||||
|
||||
return NOTES_LLM_PROMPT;
|
||||
}
|
||||
|
||||
export function adjustEndpoint(apiEndpointUrl: string, shouldRemove: boolean): string {
|
||||
const suffix = '/v1'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { App, FileManager, Notice, TFile, TFolder, arrayBufferToBase64, normalizePath } from 'obsidian';
|
||||
import Tesseract, { createWorker, Worker } from 'tesseract.js';
|
||||
import { VisionRecallPluginSettings } from '@/types/settings-types';
|
||||
import { DEFAULT_TAGS_AND_TITLE, TagsAndTitle, VISION_LLM_PROMPT, callLLMAPI } from '@/services/llm-service';
|
||||
import { DEFAULT_TAGS_AND_TITLE, TagsAndTitle, VISION_LLM_PROMPT, callLLMAPI, getNotesLLMPrompt, getVisionLLMPrompt } from '@/services/llm-service';
|
||||
import VisionRecallPlugin from '@/main';
|
||||
import { checkOCRText } from '@/lib/ocr-validation';
|
||||
import { formatTags, sanitizeObsidianTag, tagsToCommaString } from '@/lib/tag-utils';
|
||||
|
|
@ -171,7 +171,7 @@ export class ScreenshotProcessor {
|
|||
if (!ocrText || this.progressManager.isStoppedByUser()) return null;
|
||||
|
||||
ocrText = cleanOCRResultLanguageSpecific(ocrText, currentLanguageSetting || 'eng') || '';
|
||||
console.log('ocrText (after cleaning)', ocrText);
|
||||
this.plugin.logger.debug('ocrText (after cleaning)', ocrText);
|
||||
if (!ocrText) return null;
|
||||
|
||||
let validOCRText = await checkOCRText(ocrText, currentLanguageSetting || 'eng');
|
||||
|
|
@ -363,7 +363,7 @@ export class ScreenshotProcessor {
|
|||
languagePromptModifier = getLanguagePromptModifierIfIndicated(this.settings, true);
|
||||
}
|
||||
|
||||
let textPayload = VISION_LLM_PROMPT + languagePromptModifier;
|
||||
let textPayload = getVisionLLMPrompt(this.plugin.dataManager.getConfig()) + languagePromptModifier;
|
||||
this.plugin.logger.debug('Vision LLM prompt:', textPayload);
|
||||
|
||||
const visionPayload = {
|
||||
|
|
@ -399,11 +399,12 @@ export class ScreenshotProcessor {
|
|||
|
||||
private async generateNotes(ocrText: string, visionLLMResponse: string, settings: VisionRecallPluginSettings): Promise<string | null> {
|
||||
try {
|
||||
|
||||
const config = this.plugin.dataManager.getConfig();
|
||||
const languagePromptModifier = getLanguagePromptModifierIfIndicated(settings, true);
|
||||
|
||||
let endpointPrompt = `The following text is from a screenshot. Summarize the text and identify key information.`;
|
||||
let endpointPrompt = getNotesLLMPrompt(config);
|
||||
|
||||
if (config.enableCategoryDetection) {
|
||||
const visionLLMCategories = Object.keys(visionLLMResponseCategoriesMap);
|
||||
|
||||
const visionLLMCategory = visionLLMCategories.find(category => visionLLMResponse.toLowerCase().includes(category));
|
||||
|
|
@ -411,6 +412,9 @@ export class ScreenshotProcessor {
|
|||
if (visionLLMCategory) {
|
||||
endpointPrompt = visionLLMResponseCategoriesMap[visionLLMCategory];
|
||||
}
|
||||
}
|
||||
|
||||
this.plugin.logger.debug('Generate Notes LLM prompt (without added language modifier, OCR text, or vision analysis that are added later):', endpointPrompt);
|
||||
|
||||
endpointPrompt += `${languagePromptModifier}\n\nOCR text:\n${ocrText}\n\nVision analysis:\n${visionLLMResponse}`
|
||||
|
||||
|
|
@ -430,8 +434,8 @@ export class ScreenshotProcessor {
|
|||
);
|
||||
|
||||
this.plugin.logger.debug('Generated notes:', generatedNotes ? generatedNotes.substring(0, 300) + '...' : 'API call failed or no notes generated');
|
||||
return generatedNotes;
|
||||
|
||||
return generatedNotes;
|
||||
} catch (error) {
|
||||
this.plugin.logger.error('Endpoint LLM API error:', error);
|
||||
new Notice('Endpoint LLM API call failed. See console for details.');
|
||||
|
|
@ -502,6 +506,7 @@ export class ScreenshotProcessor {
|
|||
|
||||
let counter = 1;
|
||||
while (this.app.vault.getAbstractFileByPath(notePath) && counter < 100) {
|
||||
this.plugin.logger.debug(`Note title already exists: ${noteTitle}. Incrementing counter and trying again.`);
|
||||
noteTitle = `${noteTitleBase} Notes (${counter}).md`;
|
||||
notePath = normalizePath(`${outputNotesFolder}/${noteTitle}`);
|
||||
counter++;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { VISION_LLM_PROMPT, NOTES_LLM_PROMPT } from '@/services/llm-service';
|
||||
|
||||
export interface Config {
|
||||
enableAutoIntakeFolderProcessing?: boolean;
|
||||
|
||||
|
|
@ -10,6 +12,12 @@ export interface Config {
|
|||
|
||||
experimentalFeatures?: string[];
|
||||
|
||||
visionLLMPrompt?: string;
|
||||
|
||||
enableCategoryDetection?: boolean; // this is rudimentarily implemented, so defaulting to false
|
||||
|
||||
notesLLMPrompt?: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
@ -18,5 +26,8 @@ export const DefaultConfig: Config = {
|
|||
enablePeriodicIntakeFolderProcessing: false,
|
||||
intakeFolderPollingInterval: 300, // 300 seconds, or 5 minutes
|
||||
defaultMinimizedProgressDisplay: false, // Currently not functional
|
||||
experimentalFeatures: []
|
||||
experimentalFeatures: [],
|
||||
visionLLMPrompt: VISION_LLM_PROMPT,
|
||||
enableCategoryDetection: false,
|
||||
notesLLMPrompt: NOTES_LLM_PROMPT,
|
||||
};
|
||||
|
|
@ -14,5 +14,6 @@
|
|||
"1.0.12": "1.8.3",
|
||||
"1.0.13": "1.8.3",
|
||||
"1.0.14": "1.8.3",
|
||||
"1.1.0": "1.8.3"
|
||||
"1.1.0": "1.8.3",
|
||||
"1.2.0": "1.8.3"
|
||||
}
|
||||
Loading…
Reference in a new issue