Compare commits

..

No commits in common. "main" and "1.1.0" have entirely different histories.
main ... 1.1.0

11 changed files with 40 additions and 167 deletions

View file

@ -10,9 +10,7 @@
<a href="https://github.com/travisvn/obsidian-vision-recall/issues">
<img src="https://img.shields.io/github/issues/travisvn/obsidian-vision-recall" alt="GitHub issues"></a>
<img src="https://img.shields.io/github/last-commit/travisvn/obsidian-vision-recall?color=red" alt="GitHub last commit">
<a href="https://visionrecall.com/discord">
<img src="https://img.shields.io/badge/Discord-Vision_Recall-blue?logo=discord&logoColor=white" alt="Discord">
</a>
<img src="https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Ftravisvn%2Fobsidian-vision-recall&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false" alt="Hits">
</p>
<h1 align="center">Screenshot anything you want to recall later. Let AI handle the rest.</h1>
@ -83,4 +81,4 @@
Visit the [wiki](https://github.com/travisvn/obsidian-vision-recall/wiki)
If you still need help, [join our new Discord](https://visionrecall.com/discord)
If you still need help, [join our new Discord](https://visionrecall.com/discord) or [post on the forum](https://forum.visionrecall.com)

View file

@ -1,7 +1,7 @@
{
"id": "vision-recall",
"name": "Vision Recall",
"version": "1.2.0",
"version": "1.1.0",
"minAppVersion": "1.8.3",
"description": "Transform screenshots into searchable notes using AI vision and text analysis.",
"author": "Travis Van Nimwegen",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-vision-recall",
"version": "1.2.0",
"version": "1.1.0",
"description": "Transform screenshots into searchable Obsidian notes using AI vision and text analysis.",
"main": "main.js",
"scripts": {

View file

@ -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 { TestSetupModal } from './modals/TestSetupModal';
import { TestConfigModal } from './modals/TestConfigModal';
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 setup'
aria-label='Test config'
className='cursor-pointer flex flex-row items-center gap-2'
onClick={() => {
new TestSetupModal(
new TestConfigModal(
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 setup
Test config
</div>
</button>
@ -264,7 +264,7 @@ export const MainViewHeader = ({ metadata, refreshMetadata, viewMode = 'list', o
</div>
<button
aria-label='Advanced configuration'
aria-label='Advanced settings'
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'>
Config
Settings
</div>
</button>
</div>

View file

@ -1,10 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, 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,
@ -17,17 +16,11 @@ 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();
@ -41,26 +34,9 @@ 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>;
}
@ -120,83 +96,6 @@ 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">

View file

@ -41,6 +41,18 @@ 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">

View file

@ -6,13 +6,13 @@ import { DateTime } from 'luxon';
import { DataManager } from '@/data/DataManager';
import { getModels } from '@/services/llm-service';
interface TestSetupModalProps {
interface TestConfigModalProps {
dataManager: DataManager;
plugin: VisionRecallPlugin;
onClose: () => void;
}
const TestSetupView: React.FC<TestSetupModalProps> = ({ dataManager, plugin, onClose }) => {
const TestConfigView: React.FC<TestConfigModalProps> = ({ dataManager, plugin, onClose }) => {
const settings = plugin.settings;
const [models, setModels] = useState<string[]>([]);
@ -127,7 +127,7 @@ const TestSetupView: React.FC<TestSetupModalProps> = ({ dataManager, plugin, onC
);
};
export class TestSetupModal extends Modal {
export class TestConfigModal extends Modal {
private metadata: any;
private plugin: VisionRecallPlugin;
@ -139,11 +139,11 @@ export class TestSetupModal extends Modal {
onOpen() {
const { contentEl, titleEl } = this;
titleEl.setText('Test your setup');
titleEl.setText('Test config');
const root = createRoot(contentEl);
root.render(
<TestSetupView
<TestConfigView
dataManager={this.plugin.dataManager}
plugin={this.plugin}
onClose={() => this.close()}

View file

@ -1,30 +1,11 @@
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'

View file

@ -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, getNotesLLMPrompt, getVisionLLMPrompt } from '@/services/llm-service';
import { DEFAULT_TAGS_AND_TITLE, TagsAndTitle, VISION_LLM_PROMPT, callLLMAPI } 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') || '';
this.plugin.logger.debug('ocrText (after cleaning)', ocrText);
console.log('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 = getVisionLLMPrompt(this.plugin.dataManager.getConfig()) + languagePromptModifier;
let textPayload = VISION_LLM_PROMPT + languagePromptModifier;
this.plugin.logger.debug('Vision LLM prompt:', textPayload);
const visionPayload = {
@ -399,23 +399,19 @@ 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 = getNotesLLMPrompt(config);
let endpointPrompt = `The following text is from a screenshot. Summarize the text and identify key information.`;
if (config.enableCategoryDetection) {
const visionLLMCategories = Object.keys(visionLLMResponseCategoriesMap);
const visionLLMCategories = Object.keys(visionLLMResponseCategoriesMap);
const visionLLMCategory = visionLLMCategories.find(category => visionLLMResponse.toLowerCase().includes(category));
const visionLLMCategory = visionLLMCategories.find(category => visionLLMResponse.toLowerCase().includes(category));
if (visionLLMCategory) {
endpointPrompt = visionLLMResponseCategoriesMap[visionLLMCategory];
}
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}`
const endpointPayload = {
@ -434,8 +430,8 @@ export class ScreenshotProcessor {
);
this.plugin.logger.debug('Generated notes:', generatedNotes ? generatedNotes.substring(0, 300) + '...' : 'API call failed or no notes generated');
return generatedNotes;
} catch (error) {
this.plugin.logger.error('Endpoint LLM API error:', error);
new Notice('Endpoint LLM API call failed. See console for details.');
@ -506,7 +502,6 @@ 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++;

View file

@ -1,5 +1,3 @@
import { VISION_LLM_PROMPT, NOTES_LLM_PROMPT } from '@/services/llm-service';
export interface Config {
enableAutoIntakeFolderProcessing?: boolean;
@ -12,12 +10,6 @@ export interface Config {
experimentalFeatures?: string[];
visionLLMPrompt?: string;
enableCategoryDetection?: boolean; // this is rudimentarily implemented, so defaulting to false
notesLLMPrompt?: string;
[key: string]: unknown;
}
@ -26,8 +18,5 @@ export const DefaultConfig: Config = {
enablePeriodicIntakeFolderProcessing: false,
intakeFolderPollingInterval: 300, // 300 seconds, or 5 minutes
defaultMinimizedProgressDisplay: false, // Currently not functional
experimentalFeatures: [],
visionLLMPrompt: VISION_LLM_PROMPT,
enableCategoryDetection: false,
notesLLMPrompt: NOTES_LLM_PROMPT,
experimentalFeatures: []
};

View file

@ -14,6 +14,5 @@
"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.2.0": "1.8.3"
"1.1.0": "1.8.3"
}