mirror of
https://github.com/bitsofchris/openaugi-obsidian-plugin.git
synced 2026-07-22 12:40:27 +00:00
custom lens, date range filtering, note header date parsing
This commit is contained in:
parent
6085173752
commit
ec004dd25f
9 changed files with 951 additions and 52 deletions
105
src/main.ts
105
src/main.ts
|
|
@ -7,6 +7,7 @@ import { OpenAugiSettingTab } from './ui/settings-tab';
|
|||
import { LoadingIndicator } from './ui/loading-indicator';
|
||||
import { sanitizeFilename, createFileWithCollisionHandling } from './utils/filename-utils';
|
||||
import { RecentActivityModal, RecentActivityConfig } from './ui/recent-activity-modal';
|
||||
import { PromptSelectionModal, PromptSelectionConfig } from './ui/prompt-selection-modal';
|
||||
|
||||
/**
|
||||
* A simple tokeinzer to estimate the number of tokens
|
||||
|
|
@ -168,6 +169,23 @@ export default class OpenAugiPlugin extends Plugin {
|
|||
* @param rootFile The root file containing links to distill
|
||||
*/
|
||||
private async distillLinkedNotes(rootFile: TFile): Promise<void> {
|
||||
// Show prompt selection modal
|
||||
const modal = new PromptSelectionModal(
|
||||
this.app,
|
||||
this.settings.promptsFolder,
|
||||
async (config: PromptSelectionConfig) => {
|
||||
await this.executeDistillLinkedNotes(rootFile, config);
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the distillation of linked notes with the selected prompt configuration
|
||||
* @param rootFile The root note file
|
||||
* @param promptConfig The prompt configuration from the modal
|
||||
*/
|
||||
private async executeDistillLinkedNotes(rootFile: TFile, promptConfig: PromptSelectionConfig): Promise<void> {
|
||||
try {
|
||||
// Show loading indicator
|
||||
this.loadingIndicator?.show('Distilling linked notes');
|
||||
|
|
@ -210,12 +228,24 @@ export default class OpenAugiPlugin extends Plugin {
|
|||
// Show notice about linked files
|
||||
new Notice(`Found ${linkedFiles.length} linked notes to process.`);
|
||||
|
||||
// Read custom prompt if selected
|
||||
let customPrompt: string | undefined;
|
||||
if (promptConfig.useCustomPrompt && promptConfig.selectedPrompt) {
|
||||
try {
|
||||
customPrompt = await this.app.vault.read(promptConfig.selectedPrompt);
|
||||
} catch (error) {
|
||||
console.error('Failed to read custom prompt:', error);
|
||||
new Notice('Failed to read custom prompt, using default');
|
||||
}
|
||||
}
|
||||
|
||||
// Let the distill service handle all the content aggregation (no time filtering)
|
||||
const distilledData = await this.distillService.distillFromRootNote(
|
||||
rootFile,
|
||||
undefined,
|
||||
undefined,
|
||||
0 // No time filtering for regular distill command
|
||||
0, // No time filtering for regular distill command
|
||||
customPrompt
|
||||
);
|
||||
|
||||
// Write result to files
|
||||
|
|
@ -257,7 +287,15 @@ export default class OpenAugiPlugin extends Plugin {
|
|||
this.app,
|
||||
this.settings.recentActivityDefaults,
|
||||
async (config: RecentActivityConfig) => {
|
||||
await this.executeRecentActivityDistill(config);
|
||||
// After recent activity config, show prompt selection
|
||||
const promptModal = new PromptSelectionModal(
|
||||
this.app,
|
||||
this.settings.promptsFolder,
|
||||
async (promptConfig: PromptSelectionConfig) => {
|
||||
await this.executeRecentActivityDistill(config, promptConfig);
|
||||
}
|
||||
);
|
||||
promptModal.open();
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
|
|
@ -266,8 +304,9 @@ export default class OpenAugiPlugin extends Plugin {
|
|||
/**
|
||||
* Execute the recent activity distillation with the given configuration
|
||||
* @param config The configuration for recent activity distillation
|
||||
* @param promptConfig The prompt configuration from the modal
|
||||
*/
|
||||
private async executeRecentActivityDistill(config: RecentActivityConfig): Promise<void> {
|
||||
private async executeRecentActivityDistill(config: RecentActivityConfig, promptConfig: PromptSelectionConfig): Promise<void> {
|
||||
try {
|
||||
// Check API key
|
||||
if (!this.settings.apiKey) {
|
||||
|
|
@ -286,15 +325,23 @@ export default class OpenAugiPlugin extends Plugin {
|
|||
this.settings
|
||||
);
|
||||
|
||||
// Get recently modified notes
|
||||
const recentFiles = await this.distillService.getRecentlyModifiedNotes(
|
||||
config.daysBack,
|
||||
config.excludeFolders
|
||||
);
|
||||
// Use selected notes if provided, otherwise get all recent notes
|
||||
let recentFiles: TFile[];
|
||||
if (config.selectedNotes && config.selectedNotes.length > 0) {
|
||||
recentFiles = config.selectedNotes;
|
||||
} else {
|
||||
// Fallback to getting all recent notes (shouldn't happen with new UI)
|
||||
recentFiles = await this.distillService.getRecentlyModifiedNotes(
|
||||
config.daysBack,
|
||||
config.excludeFolders,
|
||||
config.useDateRange ? config.fromDate : undefined,
|
||||
config.useDateRange ? config.toDate : undefined
|
||||
);
|
||||
}
|
||||
|
||||
if (recentFiles.length === 0 && !config.rootNote) {
|
||||
this.loadingIndicator?.hide();
|
||||
new Notice(`No notes modified in the last ${config.daysBack} days`);
|
||||
new Notice(`No notes selected for processing`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -306,8 +353,8 @@ export default class OpenAugiPlugin extends Plugin {
|
|||
|
||||
// Show notice about discovered files
|
||||
const message = config.rootNote
|
||||
? `Found ${recentFiles.length} recent notes plus root note: ${config.rootNote.basename}`
|
||||
: `Found ${recentFiles.length} notes modified in the last ${config.daysBack} days`;
|
||||
? `Processing ${recentFiles.length} selected notes plus root note: ${config.rootNote.basename}`
|
||||
: `Processing ${recentFiles.length} selected notes`;
|
||||
new Notice(message);
|
||||
|
||||
// Update loading message
|
||||
|
|
@ -317,9 +364,16 @@ export default class OpenAugiPlugin extends Plugin {
|
|||
const timeWindow = config.filterJournalSections ? config.daysBack : 0;
|
||||
|
||||
// Create a synthetic root file for the distillation
|
||||
let timeWindowDesc: string;
|
||||
if (config.useDateRange && config.fromDate && config.toDate) {
|
||||
timeWindowDesc = `from ${config.fromDate} to ${config.toDate}`;
|
||||
} else {
|
||||
timeWindowDesc = `in the last ${config.daysBack} days`;
|
||||
}
|
||||
|
||||
const syntheticRootContent = `# Recent Activity Summary
|
||||
|
||||
This is an automated summary of notes modified in the last ${config.daysBack} days.
|
||||
This is an automated summary of notes modified ${timeWindowDesc}.
|
||||
${config.rootNote ? `\nUsing [[${config.rootNote.basename}]] as context root.` : ''}
|
||||
|
||||
## Recently Modified Notes:
|
||||
|
|
@ -342,17 +396,38 @@ ${allFiles.map(f => `- [[${f.basename}]]`).join('\n')}`;
|
|||
);
|
||||
|
||||
// Combine with synthetic root content
|
||||
const combinedContent = `# Recent Activity: Last ${config.daysBack} Days\n\n${syntheticRootContent}\n\n${aggregatedContent}`;
|
||||
const combinedContent = `# Recent Activity: ${timeWindowDesc}\n\n${syntheticRootContent}\n\n${aggregatedContent}`;
|
||||
|
||||
// Read custom prompt if selected
|
||||
let customPrompt: string | undefined;
|
||||
if (promptConfig.useCustomPrompt && promptConfig.selectedPrompt) {
|
||||
try {
|
||||
customPrompt = await this.app.vault.read(promptConfig.selectedPrompt);
|
||||
} catch (error) {
|
||||
console.error('Failed to read custom prompt:', error);
|
||||
new Notice('Failed to read custom prompt, using default');
|
||||
}
|
||||
}
|
||||
|
||||
// Distill the recent activity
|
||||
const distilledData = await this.distillService.distillFromRootNote(
|
||||
tempRootFile,
|
||||
combinedContent,
|
||||
sourceNotes
|
||||
sourceNotes,
|
||||
undefined, // No time window needed here since we already filtered
|
||||
customPrompt
|
||||
);
|
||||
|
||||
// Update the distilled data to reflect recent activity
|
||||
distilledData.summary = `## Recent Activity Summary (Last ${config.daysBack} Days)\n\n${distilledData.summary}`;
|
||||
const summaryTimeDesc = config.useDateRange && config.fromDate && config.toDate
|
||||
? `${config.fromDate} to ${config.toDate}`
|
||||
: `Last ${config.daysBack} Days`;
|
||||
distilledData.summary = `## Recent Activity Summary (${summaryTimeDesc})\n\n${distilledData.summary}`;
|
||||
|
||||
// Remove the temp file from source notes before writing
|
||||
distilledData.sourceNotes = distilledData.sourceNotes?.filter(
|
||||
note => !note.includes('temp-recent-activity')
|
||||
);
|
||||
|
||||
// Write result to files
|
||||
const summaryPath = await this.fileService.writeDistilledFiles(tempRootFile, distilledData);
|
||||
|
|
|
|||
|
|
@ -80,10 +80,37 @@ ${content}
|
|||
* Get recently modified notes based on specified criteria
|
||||
* @param daysBack Number of days to look back
|
||||
* @param excludeFolders Folders to exclude from search
|
||||
* @param fromDate Optional start date for date range mode
|
||||
* @param toDate Optional end date for date range mode
|
||||
* @returns Array of recently modified files
|
||||
*/
|
||||
async getRecentlyModifiedNotes(daysBack: number, excludeFolders: string[]): Promise<TFile[]> {
|
||||
const cutoffTime = Date.now() - (daysBack * 24 * 60 * 60 * 1000);
|
||||
async getRecentlyModifiedNotes(
|
||||
daysBack: number,
|
||||
excludeFolders: string[],
|
||||
fromDate?: string,
|
||||
toDate?: string
|
||||
): Promise<TFile[]> {
|
||||
let startTime: number;
|
||||
let endTime: number;
|
||||
let cutoffDate: Date;
|
||||
|
||||
if (fromDate && toDate) {
|
||||
// Use date range
|
||||
const from = new Date(fromDate);
|
||||
from.setHours(0, 0, 0, 0);
|
||||
startTime = from.getTime();
|
||||
cutoffDate = from;
|
||||
|
||||
const to = new Date(toDate);
|
||||
to.setHours(23, 59, 59, 999);
|
||||
endTime = to.getTime();
|
||||
} else {
|
||||
// Use days back
|
||||
endTime = Date.now();
|
||||
startTime = endTime - (daysBack * 24 * 60 * 60 * 1000);
|
||||
cutoffDate = new Date(startTime);
|
||||
}
|
||||
|
||||
const recentFiles: TFile[] = [];
|
||||
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
|
|
@ -98,21 +125,43 @@ ${content}
|
|||
continue;
|
||||
}
|
||||
|
||||
// Check modification time
|
||||
const stats = await this.app.vault.adapter.stat(file.path);
|
||||
if (stats && stats.mtime >= cutoffTime) {
|
||||
let includeFile = false;
|
||||
|
||||
// First, check if filename starts with a date
|
||||
const filenameDate = this.extractDateFromFilename(file.basename);
|
||||
if (filenameDate) {
|
||||
const fileTime = filenameDate.getTime();
|
||||
if (fileTime >= startTime && fileTime <= endTime) {
|
||||
includeFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If not included by filename date, check modification time
|
||||
if (!includeFile) {
|
||||
const stats = await this.app.vault.adapter.stat(file.path);
|
||||
if (stats && stats.mtime >= startTime && stats.mtime <= endTime) {
|
||||
includeFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (includeFile) {
|
||||
recentFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by modification time, most recent first
|
||||
recentFiles.sort((a, b) => {
|
||||
const aStats = this.app.vault.adapter.stat(a.path);
|
||||
const bStats = this.app.vault.adapter.stat(b.path);
|
||||
return (bStats as any).mtime - (aStats as any).mtime;
|
||||
});
|
||||
// Sort by effective date (filename date or modification time), most recent first
|
||||
const filesWithDates = await Promise.all(
|
||||
recentFiles.map(async (file) => {
|
||||
const filenameDate = this.extractDateFromFilename(file.basename);
|
||||
const stats = await this.app.vault.adapter.stat(file.path);
|
||||
const effectiveTime = filenameDate ? filenameDate.getTime() : (stats?.mtime || 0);
|
||||
return { file, effectiveTime };
|
||||
})
|
||||
);
|
||||
|
||||
return recentFiles;
|
||||
filesWithDates.sort((a, b) => b.effectiveTime - a.effectiveTime);
|
||||
|
||||
return filesWithDates.map(item => item.file);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -304,10 +353,40 @@ ${content}
|
|||
async getLinkedNotes(file: TFile): Promise<TFile[]> {
|
||||
let linkedFiles: TFile[] = [];
|
||||
|
||||
// First check if content contains dataview query and settings allow using dataview
|
||||
if (this.settings.useDataviewIfAvailable) {
|
||||
const fileContent = await this.app.vault.read(file);
|
||||
// Read file content once
|
||||
const fileContent = await this.app.vault.read(file);
|
||||
|
||||
// Check if this is a collection note with checkboxes
|
||||
const hasCheckboxLinks = /- \[[ x]\] \[\[.*?\]\]/.test(fileContent);
|
||||
|
||||
if (hasCheckboxLinks) {
|
||||
// Extract only checked links from checkbox format
|
||||
const checkboxRegex = /- \[x\] \[\[(.*?)\]\]/gi;
|
||||
let match;
|
||||
while ((match = checkboxRegex.exec(fileContent)) !== null) {
|
||||
let linkPath = match[1];
|
||||
|
||||
// Handle aliased links: [[path|alias]] -> path
|
||||
if (linkPath.includes("|")) {
|
||||
linkPath = linkPath.split("|")[0];
|
||||
}
|
||||
|
||||
// Resolve the file
|
||||
const linkedFile = this.app.metadataCache.getFirstLinkpathDest(linkPath, file.path);
|
||||
|
||||
if (linkedFile && linkedFile instanceof TFile) {
|
||||
if (!linkedFiles.some(existingFile => existingFile.path === linkedFile.path)) {
|
||||
linkedFiles.push(linkedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For collection notes, only use checked items
|
||||
return linkedFiles;
|
||||
}
|
||||
|
||||
// Check if content contains dataview query and settings allow using dataview
|
||||
if (this.settings.useDataviewIfAvailable) {
|
||||
if (this.containsDataviewQuery(fileContent)) {
|
||||
const queries = this.extractDataviewQueries(fileContent);
|
||||
|
||||
|
|
@ -428,6 +507,33 @@ ${content}
|
|||
return isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract date from filename if it starts with YYYY-MM-DD format
|
||||
* @param filename The filename to parse
|
||||
* @returns Date object or null if filename doesn't start with a date
|
||||
*/
|
||||
private extractDateFromFilename(filename: string): Date | null {
|
||||
// Match YYYY-MM-DD at the start of the filename
|
||||
const dateMatch = filename.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
|
||||
if (dateMatch) {
|
||||
const year = parseInt(dateMatch[1]);
|
||||
const month = parseInt(dateMatch[2]);
|
||||
const day = parseInt(dateMatch[3]);
|
||||
|
||||
// Validate the date components
|
||||
if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
|
||||
const date = new Date(year, month - 1, day);
|
||||
// Check if the date is valid
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a note is journal-style (contains headers with configured date format)
|
||||
* @param content The content to check
|
||||
|
|
@ -435,6 +541,10 @@ ${content}
|
|||
*/
|
||||
private isJournalStyleNote(content: string): boolean {
|
||||
const dateFormat = this.settings.recentActivityDefaults.dateHeaderFormat;
|
||||
// Skip journal parsing if dateHeaderFormat is empty
|
||||
if (!dateFormat || dateFormat.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
const dateHeaderRegex = this.dateFormatToRegex(dateFormat);
|
||||
return dateHeaderRegex.test(content);
|
||||
}
|
||||
|
|
@ -446,6 +556,10 @@ ${content}
|
|||
*/
|
||||
private parseDateFromHeader(header: string): Date | null {
|
||||
const dateFormat = this.settings.recentActivityDefaults.dateHeaderFormat;
|
||||
// Skip journal parsing if dateHeaderFormat is empty
|
||||
if (!dateFormat || dateFormat.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
return this.extractDateFromHeader(header, dateFormat);
|
||||
}
|
||||
|
||||
|
|
@ -585,13 +699,15 @@ ${content}
|
|||
* @param preparedContent Optional preprocessed combined content
|
||||
* @param preparedSourceNotes Optional preprocessed source notes
|
||||
* @param timeWindowDays Optional time window in days for filtering journal content
|
||||
* @param customPrompt Optional custom prompt to use for processing
|
||||
* @returns Distilled content as a DistillResponse
|
||||
*/
|
||||
async distillFromRootNote(
|
||||
rootFile: TFile,
|
||||
preparedContent?: string,
|
||||
preparedSourceNotes?: string[],
|
||||
timeWindowDays?: number
|
||||
timeWindowDays?: number,
|
||||
customPrompt?: string
|
||||
): Promise<DistillResponse> {
|
||||
let combinedContent: string;
|
||||
let sourceNotes: string[];
|
||||
|
|
@ -624,8 +740,8 @@ ${content}
|
|||
// Log the combined content before sending to AI
|
||||
await this.logDistillContext(combinedContent, rootFile.basename);
|
||||
|
||||
// Send to OpenAI for distillation
|
||||
const distilledContent = await this.openAIService.distillContent(combinedContent);
|
||||
// Send to OpenAI for distillation with optional custom prompt
|
||||
const distilledContent = await this.openAIService.distillContent(combinedContent, customPrompt);
|
||||
|
||||
// Add source notes to the response
|
||||
distilledContent.sourceNotes = [rootFile.basename, ...sourceNotes];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,37 @@ export class FileService {
|
|||
this.backlinkMapper = new BacklinkMapper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a session folder name based on type and timestamp
|
||||
* @param type Type of distillation: 'transcript', 'distill', or 'recent'
|
||||
* @param rootName Optional root note name for context
|
||||
* @returns Formatted folder name
|
||||
*/
|
||||
private generateSessionFolderName(type: 'transcript' | 'distill' | 'recent', rootName?: string): string {
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString()
|
||||
.replace(/T/, ' ')
|
||||
.replace(/\..+/, '')
|
||||
.replace(/:/g, '-');
|
||||
|
||||
switch (type) {
|
||||
case 'transcript':
|
||||
return `Transcript ${timestamp}`;
|
||||
case 'distill':
|
||||
if (rootName) {
|
||||
const sanitizedRoot = sanitizeFilename(rootName);
|
||||
// Truncate root name if too long
|
||||
const truncatedRoot = sanitizedRoot.length > 30
|
||||
? sanitizedRoot.substring(0, 30) + '...'
|
||||
: sanitizedRoot;
|
||||
return `Distill ${truncatedRoot} ${timestamp}`;
|
||||
}
|
||||
return `Distill ${timestamp}`;
|
||||
case 'recent':
|
||||
return `Recent Activity ${timestamp}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure output directories exist
|
||||
*/
|
||||
|
|
@ -44,6 +75,14 @@ export class FileService {
|
|||
// Ensure directories exist
|
||||
await this.ensureDirectoriesExist();
|
||||
|
||||
// Create session folder for atomic notes
|
||||
const sessionFolder = this.generateSessionFolderName('transcript');
|
||||
const sessionPath = `${this.notesFolder}/${sessionFolder}`;
|
||||
const sessionExists = await this.vault.adapter.exists(sessionPath);
|
||||
if (!sessionExists) {
|
||||
await this.vault.createFolder(sessionPath);
|
||||
}
|
||||
|
||||
// Sanitize filename
|
||||
const sanitizedFilename = sanitizeFilename(filename);
|
||||
|
||||
|
|
@ -72,7 +111,7 @@ export class FileService {
|
|||
summaryContent
|
||||
);
|
||||
|
||||
// Output Notes
|
||||
// Output Notes to session folder
|
||||
for (const note of data.notes) {
|
||||
// Sanitize note title for filename
|
||||
const sanitizedTitle = sanitizeFilename(note.title);
|
||||
|
|
@ -80,7 +119,7 @@ export class FileService {
|
|||
const processedContent = this.backlinkMapper.processBacklinks(note.content);
|
||||
await createFileWithCollisionHandling(
|
||||
this.vault,
|
||||
`${this.notesFolder}/${sanitizedTitle}.md`,
|
||||
`${sessionPath}/${sanitizedTitle}.md`,
|
||||
processedContent
|
||||
);
|
||||
}
|
||||
|
|
@ -95,8 +134,41 @@ export class FileService {
|
|||
// Ensure directories exist
|
||||
await this.ensureDirectoriesExist();
|
||||
|
||||
// Sanitize filename
|
||||
const sanitizedFilename = sanitizeFilename(rootFile.basename);
|
||||
// Determine session type based on rootFile
|
||||
const isRecentActivity = rootFile.basename.includes('temp-recent-activity');
|
||||
const sessionType = isRecentActivity ? 'recent' : 'distill';
|
||||
const sessionFolder = this.generateSessionFolderName(
|
||||
sessionType,
|
||||
isRecentActivity ? undefined : rootFile.basename
|
||||
);
|
||||
const sessionPath = `${this.notesFolder}/${sessionFolder}`;
|
||||
|
||||
// Create session folder for atomic notes
|
||||
const sessionExists = await this.vault.adapter.exists(sessionPath);
|
||||
if (!sessionExists) {
|
||||
await this.vault.createFolder(sessionPath);
|
||||
}
|
||||
|
||||
// Generate appropriate filename based on content
|
||||
let summaryFilename: string;
|
||||
if (isRecentActivity) {
|
||||
// For recent activity, try to extract a meaningful title from the first atomic note
|
||||
// or use a descriptive name
|
||||
if (data.notes.length > 0) {
|
||||
const firstNoteTitle = sanitizeFilename(data.notes[0].title);
|
||||
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
summaryFilename = `Recent Activity ${timestamp} - ${firstNoteTitle}`;
|
||||
} else {
|
||||
const timestamp = new Date().toISOString()
|
||||
.replace(/T/, ' ')
|
||||
.replace(/\..+/, '')
|
||||
.replace(/:/g, '-');
|
||||
summaryFilename = `Recent Activity Summary ${timestamp}`;
|
||||
}
|
||||
} else {
|
||||
// For regular distillation, use the root file name
|
||||
summaryFilename = `${sanitizeFilename(rootFile.basename)} - distilled`;
|
||||
}
|
||||
|
||||
// Register all note titles for backlink processing
|
||||
this.backlinkMapper = new BacklinkMapper(); // Reset the mapper
|
||||
|
|
@ -127,11 +199,11 @@ export class FileService {
|
|||
// Output Summary
|
||||
const summaryPath = await createFileWithCollisionHandling(
|
||||
this.vault,
|
||||
`${this.summaryFolder}/${sanitizedFilename} - distilled.md`,
|
||||
`${this.summaryFolder}/${summaryFilename}.md`,
|
||||
summaryContent
|
||||
);
|
||||
|
||||
// Output Notes
|
||||
// Output Notes to session folder
|
||||
for (const note of data.notes) {
|
||||
// Sanitize note title for filename
|
||||
const sanitizedTitle = sanitizeFilename(note.title);
|
||||
|
|
@ -139,7 +211,7 @@ export class FileService {
|
|||
const processedContent = this.backlinkMapper.processBacklinks(note.content);
|
||||
await createFileWithCollisionHandling(
|
||||
this.vault,
|
||||
`${this.notesFolder}/${sanitizedTitle}.md`,
|
||||
`${sessionPath}/${sanitizedTitle}.md`,
|
||||
processedContent
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,14 +208,21 @@ export class OpenAIService {
|
|||
/**
|
||||
* Get the distillation prompt for the OpenAI API
|
||||
* @param content The aggregated content from linked notes
|
||||
* @param customPrompt Optional custom prompt to replace the default instructions
|
||||
* @returns The formatted prompt
|
||||
*/
|
||||
private getDistillPrompt(content: string): string {
|
||||
private getDistillPrompt(content: string, customPrompt?: string): string {
|
||||
// Extract any custom context from the content (which should include the root note)
|
||||
const customContext = this.extractCustomContext(content);
|
||||
|
||||
// Base prompt
|
||||
let prompt = `
|
||||
let prompt: string;
|
||||
|
||||
if (customPrompt) {
|
||||
// Use the custom prompt as the main instructions
|
||||
prompt = customPrompt;
|
||||
} else {
|
||||
// Use the default prompt
|
||||
prompt = `
|
||||
You are an expert knowledge curator helping users distill and organize information from their notes. Your task is to analyze multiple related notes and create a coherent set of atomic notes and a summary.
|
||||
|
||||
# Instructions
|
||||
|
|
@ -242,8 +249,9 @@ export class OpenAIService {
|
|||
- Highlight connections between ideas
|
||||
- Use \`[[Backlinks]]\` to connect to relevant atomic notes
|
||||
`;
|
||||
}
|
||||
|
||||
// Add custom context if available
|
||||
// Add custom context if available (this is from the context: section in notes)
|
||||
if (customContext) {
|
||||
prompt += `\n\n${customContext}`;
|
||||
}
|
||||
|
|
@ -257,14 +265,15 @@ export class OpenAIService {
|
|||
/**
|
||||
* Call the OpenAI API to distill content from linked notes
|
||||
* @param content The aggregated content from linked notes
|
||||
* @param customPrompt Optional custom prompt to replace the default instructions
|
||||
* @returns Distilled content data
|
||||
*/
|
||||
async distillContent(content: string): Promise<DistillResponse> {
|
||||
async distillContent(content: string, customPrompt?: string): Promise<DistillResponse> {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('OpenAI API key not set');
|
||||
}
|
||||
|
||||
const prompt = this.getDistillPrompt(content);
|
||||
const prompt = this.getDistillPrompt(content, customPrompt);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface OpenAugiSettings {
|
|||
apiKey: string;
|
||||
summaryFolder: string;
|
||||
notesFolder: string;
|
||||
promptsFolder: string;
|
||||
useDataviewIfAvailable: boolean;
|
||||
enableDistillLogging: boolean;
|
||||
recentActivityDefaults: RecentActivitySettings;
|
||||
|
|
@ -20,6 +21,7 @@ export const DEFAULT_SETTINGS: OpenAugiSettings = {
|
|||
apiKey: '',
|
||||
summaryFolder: 'OpenAugi/Summaries',
|
||||
notesFolder: 'OpenAugi/Notes',
|
||||
promptsFolder: 'OpenAugi/Prompts',
|
||||
useDataviewIfAvailable: true,
|
||||
enableDistillLogging: false,
|
||||
recentActivityDefaults: {
|
||||
|
|
|
|||
138
src/ui/prompt-selection-modal.ts
Normal file
138
src/ui/prompt-selection-modal.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { App, Modal, Setting, TFile } from 'obsidian';
|
||||
|
||||
export interface PromptSelectionConfig {
|
||||
selectedPrompt?: TFile;
|
||||
useCustomPrompt: boolean;
|
||||
}
|
||||
|
||||
export class PromptSelectionModal extends Modal {
|
||||
private config: PromptSelectionConfig;
|
||||
private onSubmit: (config: PromptSelectionConfig) => void;
|
||||
private promptsFolder: string;
|
||||
private availablePrompts: TFile[] = [];
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
promptsFolder: string,
|
||||
onSubmit: (config: PromptSelectionConfig) => void
|
||||
) {
|
||||
super(app);
|
||||
this.promptsFolder = promptsFolder;
|
||||
this.config = { useCustomPrompt: false };
|
||||
this.onSubmit = onSubmit;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl('h2', { text: 'Select Processing Lens' });
|
||||
|
||||
contentEl.createEl('p', {
|
||||
text: 'Choose a custom prompt to guide how the content is processed, or use the default prompt.',
|
||||
cls: 'openaugi-prompt-description'
|
||||
});
|
||||
|
||||
// Load available prompts
|
||||
await this.loadAvailablePrompts();
|
||||
|
||||
if (this.availablePrompts.length === 0) {
|
||||
contentEl.createEl('p', {
|
||||
text: `No prompt files found in "${this.promptsFolder}". Create markdown files in this folder to use as custom prompts.`,
|
||||
cls: 'openaugi-prompt-warning'
|
||||
});
|
||||
} else {
|
||||
// Show preview of selected prompt
|
||||
const previewEl = contentEl.createDiv({ cls: 'openaugi-prompt-preview' });
|
||||
previewEl.style.display = 'none';
|
||||
|
||||
// Create dropdown for prompt selection
|
||||
new Setting(contentEl)
|
||||
.setName('Custom prompt')
|
||||
.setDesc('Select a prompt template to customize processing')
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('', 'Use default prompt');
|
||||
|
||||
this.availablePrompts.forEach(prompt => {
|
||||
dropdown.addOption(prompt.path, prompt.basename);
|
||||
});
|
||||
|
||||
dropdown.onChange(async value => {
|
||||
if (value) {
|
||||
this.config.useCustomPrompt = true;
|
||||
this.config.selectedPrompt = this.availablePrompts.find(p => p.path === value);
|
||||
previewEl.style.display = 'block';
|
||||
await this.updatePreview(previewEl);
|
||||
} else {
|
||||
this.config.useCustomPrompt = false;
|
||||
this.config.selectedPrompt = undefined;
|
||||
previewEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
new Setting(contentEl)
|
||||
.addButton(button => button
|
||||
.setButtonText('Cancel')
|
||||
.onClick(() => this.close())
|
||||
)
|
||||
.addButton(button => button
|
||||
.setButtonText('Continue')
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.onSubmit(this.config);
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async loadAvailablePrompts() {
|
||||
try {
|
||||
// Check if prompts folder exists
|
||||
if (!await this.app.vault.adapter.exists(this.promptsFolder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all markdown files in the prompts folder
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
this.availablePrompts = files.filter(file =>
|
||||
file.path.startsWith(this.promptsFolder + '/') ||
|
||||
file.path === this.promptsFolder
|
||||
);
|
||||
|
||||
// Sort by name
|
||||
this.availablePrompts.sort((a, b) => a.basename.localeCompare(b.basename));
|
||||
} catch (error) {
|
||||
console.error('Error loading prompts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async updatePreview(previewEl: HTMLElement) {
|
||||
previewEl.empty();
|
||||
|
||||
if (this.config.selectedPrompt) {
|
||||
try {
|
||||
const content = await this.app.vault.read(this.config.selectedPrompt);
|
||||
const preview = content.length > 300 ? content.substring(0, 300) + '...' : content;
|
||||
|
||||
previewEl.createEl('h4', { text: 'Preview:' });
|
||||
previewEl.createEl('pre', {
|
||||
text: preview,
|
||||
cls: 'openaugi-prompt-preview-content'
|
||||
});
|
||||
} catch (error) {
|
||||
previewEl.createEl('p', {
|
||||
text: 'Unable to preview prompt',
|
||||
cls: 'openaugi-prompt-error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,30 @@
|
|||
import { App, Modal, Setting, TFile } from 'obsidian';
|
||||
import { App, Modal, Setting, TFile, ToggleComponent, Notice } from 'obsidian';
|
||||
import { RecentActivitySettings } from '../types/settings';
|
||||
import { createFileWithCollisionHandling } from '../utils/filename-utils';
|
||||
|
||||
export interface RecentActivityConfig extends RecentActivitySettings {
|
||||
rootNote?: TFile;
|
||||
selectedNotes?: TFile[];
|
||||
useDateRange?: boolean;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
}
|
||||
|
||||
interface NoteSelection {
|
||||
file: TFile;
|
||||
selected: boolean;
|
||||
modifiedTime: number;
|
||||
}
|
||||
|
||||
export class RecentActivityModal extends Modal {
|
||||
private config: RecentActivityConfig;
|
||||
private onSubmit: (config: RecentActivityConfig) => void;
|
||||
private noteSelections: NoteSelection[] = [];
|
||||
private notesListEl: HTMLElement | null = null;
|
||||
private previewButton: HTMLElement | null = null;
|
||||
private daysBackSetting: Setting | null = null;
|
||||
private fromDateSetting: Setting | null = null;
|
||||
private toDateSetting: Setting | null = null;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
|
|
@ -25,20 +42,95 @@ export class RecentActivityModal extends Modal {
|
|||
|
||||
contentEl.createEl('h2', { text: 'Configure Recent Activity Distillation' });
|
||||
|
||||
// Date range toggle
|
||||
new Setting(contentEl)
|
||||
.setName('Use specific date range')
|
||||
.setDesc('Toggle between "last N days" and specific date range')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.config.useDateRange || false)
|
||||
.onChange(async value => {
|
||||
this.config.useDateRange = value;
|
||||
this.refreshDateInputs();
|
||||
await this.updateNotesList();
|
||||
})
|
||||
);
|
||||
|
||||
// Container for date inputs
|
||||
const dateInputsContainer = contentEl.createDiv('date-inputs-container');
|
||||
|
||||
// Days back setting (shown when not using date range)
|
||||
const daysBackSetting = new Setting(dateInputsContainer)
|
||||
.setName('Days to look back')
|
||||
.setDesc('Include notes modified within this many days')
|
||||
.addText(text => text
|
||||
.setPlaceholder('7')
|
||||
.setValue(String(this.config.daysBack))
|
||||
.onChange(value => {
|
||||
.onChange(async value => {
|
||||
const days = parseInt(value);
|
||||
if (!isNaN(days) && days > 0) {
|
||||
this.config.daysBack = days;
|
||||
await this.updateNotesList();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Date range inputs (shown when using date range)
|
||||
const fromDateSetting = new Setting(dateInputsContainer)
|
||||
.setName('From date')
|
||||
.setDesc('Start date for the range (YYYY-MM-DD)')
|
||||
.addText(text => {
|
||||
// Set default from date to 7 days ago
|
||||
if (!this.config.fromDate) {
|
||||
const defaultFrom = new Date();
|
||||
defaultFrom.setDate(defaultFrom.getDate() - 7);
|
||||
this.config.fromDate = defaultFrom.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
text
|
||||
.setPlaceholder('YYYY-MM-DD')
|
||||
.setValue(this.config.fromDate)
|
||||
.onChange(async value => {
|
||||
if (this.isValidDate(value)) {
|
||||
this.config.fromDate = value;
|
||||
await this.updateNotesList();
|
||||
}
|
||||
});
|
||||
|
||||
// Add date input type for better UX
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
const toDateSetting = new Setting(dateInputsContainer)
|
||||
.setName('To date')
|
||||
.setDesc('End date for the range (YYYY-MM-DD)')
|
||||
.addText(text => {
|
||||
// Set default to date to today
|
||||
if (!this.config.toDate) {
|
||||
this.config.toDate = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
text
|
||||
.setPlaceholder('YYYY-MM-DD')
|
||||
.setValue(this.config.toDate)
|
||||
.onChange(async value => {
|
||||
if (this.isValidDate(value)) {
|
||||
this.config.toDate = value;
|
||||
await this.updateNotesList();
|
||||
}
|
||||
});
|
||||
|
||||
// Add date input type for better UX
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
// Store references for show/hide
|
||||
this.daysBackSetting = daysBackSetting;
|
||||
this.fromDateSetting = fromDateSetting;
|
||||
this.toDateSetting = toDateSetting;
|
||||
|
||||
// Initial visibility
|
||||
this.refreshDateInputs();
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Filter journal sections by date')
|
||||
.setDesc('For notes with date headers, only include sections within the time window')
|
||||
|
|
@ -80,15 +172,57 @@ export class RecentActivityModal extends Modal {
|
|||
.addText(text => text
|
||||
.setPlaceholder('Templates, Archive, OpenAugi')
|
||||
.setValue(this.config.excludeFolders.join(', '))
|
||||
.onChange(value => {
|
||||
.onChange(async value => {
|
||||
this.config.excludeFolders = value
|
||||
.split(',')
|
||||
.map(f => f.trim())
|
||||
.filter(f => f.length > 0);
|
||||
await this.updateNotesList();
|
||||
})
|
||||
);
|
||||
|
||||
// Preview button to show/refresh the notes list
|
||||
const previewSetting = new Setting(contentEl)
|
||||
.setName('Select notes to include')
|
||||
.setDesc('Choose which notes to include in the distillation')
|
||||
.addButton(button => {
|
||||
this.previewButton = button.buttonEl;
|
||||
button
|
||||
.setButtonText('Select Notes')
|
||||
.onClick(async () => {
|
||||
if (this.notesListEl && this.notesListEl.style.display !== 'none') {
|
||||
// Hide the list
|
||||
this.notesListEl.style.display = 'none';
|
||||
button.setButtonText('Select Notes');
|
||||
} else {
|
||||
// Show/update the list
|
||||
await this.updateNotesList();
|
||||
if (this.notesListEl) {
|
||||
this.notesListEl.style.display = 'block';
|
||||
}
|
||||
button.setButtonText('Hide Selection');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Container for the notes list (initially hidden)
|
||||
this.notesListEl = contentEl.createDiv('recent-notes-list');
|
||||
this.notesListEl.style.display = 'none';
|
||||
this.notesListEl.style.maxHeight = '300px';
|
||||
this.notesListEl.style.overflowY = 'auto';
|
||||
this.notesListEl.style.border = '1px solid var(--background-modifier-border)';
|
||||
this.notesListEl.style.borderRadius = '4px';
|
||||
this.notesListEl.style.padding = '10px';
|
||||
this.notesListEl.style.marginBottom = '20px';
|
||||
|
||||
// Action buttons
|
||||
new Setting(contentEl)
|
||||
.addButton(button => button
|
||||
.setButtonText('Save as Collection')
|
||||
.onClick(async () => {
|
||||
await this.saveAsCollection();
|
||||
})
|
||||
)
|
||||
.addButton(button => button
|
||||
.setButtonText('Cancel')
|
||||
.onClick(() => this.close())
|
||||
|
|
@ -97,14 +231,314 @@ export class RecentActivityModal extends Modal {
|
|||
.setButtonText('Distill')
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
// Only include selected notes
|
||||
this.config.selectedNotes = this.noteSelections
|
||||
.filter(ns => ns.selected)
|
||||
.map(ns => ns.file);
|
||||
this.onSubmit(this.config);
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async updateNotesList(): Promise<void> {
|
||||
if (!this.notesListEl) return;
|
||||
|
||||
// Clear existing content
|
||||
this.notesListEl.empty();
|
||||
|
||||
// Show loading
|
||||
this.notesListEl.createEl('div', {
|
||||
text: 'Loading recent notes...',
|
||||
cls: 'loading-text'
|
||||
});
|
||||
|
||||
try {
|
||||
// Get recent notes using the distill service logic
|
||||
const files = await this.getRecentlyModifiedNotes(
|
||||
this.config.daysBack,
|
||||
this.config.excludeFolders
|
||||
);
|
||||
|
||||
// Clear loading text
|
||||
this.notesListEl.empty();
|
||||
|
||||
if (files.length === 0) {
|
||||
this.notesListEl.createEl('div', {
|
||||
text: 'No notes found in the specified time range.',
|
||||
cls: 'no-notes-text'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create header with select all
|
||||
const headerEl = this.notesListEl.createDiv('notes-list-header');
|
||||
headerEl.style.marginBottom = '10px';
|
||||
headerEl.style.paddingBottom = '10px';
|
||||
headerEl.style.borderBottom = '1px solid var(--background-modifier-border)';
|
||||
|
||||
// Create descriptive header text
|
||||
let headerText: string;
|
||||
if (this.config.useDateRange && this.config.fromDate && this.config.toDate) {
|
||||
headerText = `Found ${files.length} notes (${this.config.fromDate} to ${this.config.toDate})`;
|
||||
} else {
|
||||
headerText = `Found ${files.length} notes (last ${this.config.daysBack} days)`;
|
||||
}
|
||||
|
||||
const selectAllSetting = new Setting(headerEl)
|
||||
.setName(headerText)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(true)
|
||||
.onChange(value => {
|
||||
// Update all selections
|
||||
this.noteSelections.forEach(ns => ns.selected = value);
|
||||
// Update all checkboxes
|
||||
this.notesListEl?.querySelectorAll('.note-checkbox input[type="checkbox"]')
|
||||
.forEach((checkbox: HTMLInputElement) => {
|
||||
checkbox.checked = value;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize note selections
|
||||
this.noteSelections = await Promise.all(files.map(async file => {
|
||||
const stats = await this.app.vault.adapter.stat(file.path);
|
||||
return {
|
||||
file,
|
||||
selected: true,
|
||||
modifiedTime: stats?.mtime || 0
|
||||
};
|
||||
}));
|
||||
|
||||
// Sort by modification time (most recent first)
|
||||
this.noteSelections.sort((a, b) => b.modifiedTime - a.modifiedTime);
|
||||
|
||||
// Create individual note items
|
||||
const listEl = this.notesListEl.createDiv('notes-items');
|
||||
this.noteSelections.forEach((noteSelection, index) => {
|
||||
const itemEl = listEl.createDiv('note-item');
|
||||
itemEl.style.marginBottom = '8px';
|
||||
|
||||
const itemSetting = new Setting(itemEl)
|
||||
.setClass('note-item-setting')
|
||||
.setName(noteSelection.file.basename)
|
||||
.setDesc(this.formatNoteInfo(noteSelection))
|
||||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(noteSelection.selected)
|
||||
.onChange(value => {
|
||||
noteSelection.selected = value;
|
||||
// Update select all toggle if needed
|
||||
const allSelected = this.noteSelections.every(ns => ns.selected);
|
||||
const selectAllToggle = headerEl.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
if (selectAllToggle) {
|
||||
selectAllToggle.checked = allSelected;
|
||||
}
|
||||
});
|
||||
toggle.toggleEl.addClass('note-checkbox');
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading recent notes:', error);
|
||||
this.notesListEl.empty();
|
||||
this.notesListEl.createEl('div', {
|
||||
text: 'Error loading notes. Check console for details.',
|
||||
cls: 'error-text'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private formatNoteInfo(noteSelection: NoteSelection): string {
|
||||
const date = new Date(noteSelection.modifiedTime);
|
||||
const dateStr = date.toLocaleDateString();
|
||||
const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
const folder = noteSelection.file.parent?.path || 'Root';
|
||||
return `${folder} • Modified: ${dateStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
private refreshDateInputs(): void {
|
||||
if (!this.daysBackSetting || !this.fromDateSetting || !this.toDateSetting) return;
|
||||
|
||||
if (this.config.useDateRange) {
|
||||
// Hide days back, show date range
|
||||
this.daysBackSetting.settingEl.style.display = 'none';
|
||||
this.fromDateSetting.settingEl.style.display = '';
|
||||
this.toDateSetting.settingEl.style.display = '';
|
||||
} else {
|
||||
// Show days back, hide date range
|
||||
this.daysBackSetting.settingEl.style.display = '';
|
||||
this.fromDateSetting.settingEl.style.display = 'none';
|
||||
this.toDateSetting.settingEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
private isValidDate(dateStr: string): boolean {
|
||||
const date = new Date(dateStr);
|
||||
return !isNaN(date.getTime()) && /^\d{4}-\d{2}-\d{2}$/.test(dateStr);
|
||||
}
|
||||
|
||||
private async getRecentlyModifiedNotes(daysBack: number, excludeFolders: string[]): Promise<TFile[]> {
|
||||
let startTime: number;
|
||||
let endTime: number;
|
||||
|
||||
if (this.config.useDateRange && this.config.fromDate && this.config.toDate) {
|
||||
// Use date range
|
||||
const fromDate = new Date(this.config.fromDate);
|
||||
fromDate.setHours(0, 0, 0, 0);
|
||||
startTime = fromDate.getTime();
|
||||
|
||||
const toDate = new Date(this.config.toDate);
|
||||
toDate.setHours(23, 59, 59, 999);
|
||||
endTime = toDate.getTime();
|
||||
} else {
|
||||
// Use days back
|
||||
endTime = Date.now();
|
||||
startTime = endTime - (daysBack * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
const recentFiles: TFile[] = [];
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
|
||||
for (const file of files) {
|
||||
// Check if file is in excluded folder
|
||||
const isExcluded = excludeFolders.some(folder =>
|
||||
file.path.startsWith(folder + '/') || file.path.includes('/' + folder + '/')
|
||||
);
|
||||
|
||||
if (isExcluded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let includeFile = false;
|
||||
|
||||
// Check if filename starts with a date (YYYY-MM-DD format)
|
||||
const dateMatch = file.basename.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (dateMatch) {
|
||||
const year = parseInt(dateMatch[1]);
|
||||
const month = parseInt(dateMatch[2]);
|
||||
const day = parseInt(dateMatch[3]);
|
||||
|
||||
if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
|
||||
const fileDate = new Date(year, month - 1, day);
|
||||
const fileTime = fileDate.getTime();
|
||||
|
||||
if (!isNaN(fileTime) && fileTime >= startTime && fileTime <= endTime) {
|
||||
includeFile = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not included by filename date, check modification time
|
||||
if (!includeFile) {
|
||||
const stats = await this.app.vault.adapter.stat(file.path);
|
||||
if (stats && stats.mtime >= startTime && stats.mtime <= endTime) {
|
||||
includeFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (includeFile) {
|
||||
recentFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
return recentFiles;
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current selection as a collection note
|
||||
*/
|
||||
private async saveAsCollection(): Promise<void> {
|
||||
if (this.noteSelections.length === 0) {
|
||||
new Notice('No notes to save. Please show and select notes first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate collection content
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString();
|
||||
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
let timeWindowDesc: string;
|
||||
if (this.config.useDateRange && this.config.fromDate && this.config.toDate) {
|
||||
timeWindowDesc = `Date Range: ${this.config.fromDate} to ${this.config.toDate}`;
|
||||
} else {
|
||||
timeWindowDesc = `Last ${this.config.daysBack} days`;
|
||||
}
|
||||
|
||||
let content = `# Recent Activity Collection
|
||||
Created: ${dateStr} ${timeStr}
|
||||
Time Window: ${timeWindowDesc}
|
||||
${this.config.rootNote ? `Context Root: [[${this.config.rootNote.basename}]]` : ''}
|
||||
|
||||
## Notes
|
||||
|
||||
`;
|
||||
|
||||
// Add notes with checkboxes
|
||||
const selectedCount = this.noteSelections.filter(ns => ns.selected).length;
|
||||
content += `Selected ${selectedCount} of ${this.noteSelections.length} notes:\n\n`;
|
||||
|
||||
for (const noteSelection of this.noteSelections) {
|
||||
const checkbox = noteSelection.selected ? '[x]' : '[ ]';
|
||||
const date = new Date(noteSelection.modifiedTime);
|
||||
const modifiedStr = date.toLocaleDateString();
|
||||
content += `- ${checkbox} [[${noteSelection.file.basename}]] - Modified: ${modifiedStr}\n`;
|
||||
}
|
||||
|
||||
// Add instructions
|
||||
content += `\n## Instructions
|
||||
|
||||
This collection was generated from recent activity. You can:
|
||||
1. Check/uncheck notes to adjust the selection
|
||||
2. Run "Distill Linked Notes" on this note to process the checked items
|
||||
3. Add additional notes manually using standard Obsidian links
|
||||
|
||||
## Configuration
|
||||
|
||||
The following settings were used:
|
||||
- Time window: ${timeWindowDesc}
|
||||
- Filter journal sections: ${this.config.filterJournalSections}
|
||||
- Excluded folders: ${this.config.excludeFolders.join(', ')}
|
||||
`;
|
||||
|
||||
// Create the collection file
|
||||
try {
|
||||
const collectionsFolder = 'OpenAugi/Collections';
|
||||
|
||||
// Ensure collections folder exists
|
||||
if (!await this.app.vault.adapter.exists(collectionsFolder)) {
|
||||
await this.app.vault.createFolder(collectionsFolder);
|
||||
}
|
||||
|
||||
const timestamp = now.toISOString()
|
||||
.replace(/T/, ' ')
|
||||
.replace(/\..+/, '')
|
||||
.replace(/:/g, '-');
|
||||
const filename = `Recent Activity ${timestamp}.md`;
|
||||
const filepath = `${collectionsFolder}/${filename}`;
|
||||
|
||||
const createdPath = await createFileWithCollisionHandling(
|
||||
this.app.vault,
|
||||
filepath,
|
||||
content
|
||||
);
|
||||
|
||||
new Notice(`Collection saved to ${createdPath}`);
|
||||
|
||||
// Open the created file
|
||||
const file = this.app.vault.getAbstractFileByPath(createdPath);
|
||||
if (file instanceof TFile) {
|
||||
await this.app.workspace.getLeaf().openFile(file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save collection:', error);
|
||||
new Notice('Failed to save collection. Check console for details.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,18 @@ export class OpenAugiSettingTab extends PluginSettingTab {
|
|||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Prompts folder')
|
||||
.setDesc('Folder path where custom prompt templates are stored')
|
||||
.addText(text => text
|
||||
.setPlaceholder('OpenAugi/Prompts')
|
||||
.setValue(this.plugin.settings.promptsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.promptsFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
// Check if Dataview plugin is installed
|
||||
// @ts-ignore - Dataview API is not typed
|
||||
const dataviewPluginInstalled = this.app.plugins.plugins["dataview"] !== undefined;
|
||||
|
|
@ -112,12 +124,12 @@ export class OpenAugiSettingTab extends PluginSettingTab {
|
|||
|
||||
new Setting(containerEl)
|
||||
.setName('Date header format')
|
||||
.setDesc('Markdown format for date headers in journal notes. Use YYYY for year, MM for month, DD for day.')
|
||||
.setDesc('Markdown format for date headers in journal notes. Use YYYY for year, MM for month, DD for day. Leave empty to disable journal parsing.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('### YYYY-MM-DD')
|
||||
.setValue(this.plugin.settings.recentActivityDefaults.dateHeaderFormat)
|
||||
.onChange(async (value) => {
|
||||
if (value.includes('YYYY') && value.includes('MM') && value.includes('DD')) {
|
||||
// Allow empty value to disable journal parsing
|
||||
if (value === '' || (value.includes('YYYY') && value.includes('MM') && value.includes('DD'))) {
|
||||
this.plugin.settings.recentActivityDefaults.dateHeaderFormat = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
|
|
|
|||
41
styles.css
41
styles.css
|
|
@ -47,4 +47,45 @@
|
|||
|
||||
.openaugi-dot.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Prompt Selection Modal Styles */
|
||||
.openaugi-prompt-description {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.openaugi-prompt-warning {
|
||||
color: var(--text-warning);
|
||||
background-color: var(--background-modifier-warning);
|
||||
padding: 0.5em;
|
||||
border-radius: 4px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.openaugi-prompt-preview {
|
||||
margin-top: 1em;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 1em;
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.openaugi-prompt-preview h4 {
|
||||
margin-top: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.openaugi-prompt-preview-content {
|
||||
font-size: 0.85em;
|
||||
line-height: 1.4;
|
||||
color: var(--text-normal);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.openaugi-prompt-error {
|
||||
color: var(--text-error);
|
||||
font-style: italic;
|
||||
}
|
||||
Loading…
Reference in a new issue