feat: add multi-target export profiles for one-click multi-format export
Export Profiles bundle targets into named groups, enabling instant multi-format export (e.g. XML for Claude + Markdown for ChatGPT + Plain for Gemini) via a single "Export context (multi-target)" command. - Add ExportProfile interface, generateProfileId, BUILTIN_PROFILES - Add per-target outputPath override for file output location - Add export-multi-target command with active profile resolution - Add ExportProfileModal for creating/editing profiles - Add profile management UI in Settings > Output - Add profile dropdown in generator modal for target pre-selection - Clean up stale profile references on target deletion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
40ce34c9b4
commit
45a0de063e
6 changed files with 467 additions and 9 deletions
213
src/export-profile-modal.ts
Normal file
213
src/export-profile-modal.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import { App, Modal, Notice, Setting } from 'obsidian';
|
||||||
|
import PromptfirePlugin from './main';
|
||||||
|
import {
|
||||||
|
ExportProfile,
|
||||||
|
generateProfileId,
|
||||||
|
getTargetIcon,
|
||||||
|
} from './targets';
|
||||||
|
|
||||||
|
export class ExportProfileModal extends Modal {
|
||||||
|
plugin: PromptfirePlugin;
|
||||||
|
profile: ExportProfile | null;
|
||||||
|
isNew: boolean;
|
||||||
|
onSave: () => void;
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
name: string = '';
|
||||||
|
description: string = '';
|
||||||
|
selectedTargetIds: Set<string> = new Set();
|
||||||
|
primaryTargetId: string | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
app: App,
|
||||||
|
plugin: PromptfirePlugin,
|
||||||
|
profile: ExportProfile | null,
|
||||||
|
onSave: () => void
|
||||||
|
) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.profile = profile;
|
||||||
|
this.isNew = profile === null;
|
||||||
|
this.onSave = onSave;
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
this.name = profile.name;
|
||||||
|
this.description = profile.description || '';
|
||||||
|
this.selectedTargetIds = new Set(profile.targetIds);
|
||||||
|
this.primaryTargetId = profile.primaryTargetId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
contentEl.addClass('export-profile-modal');
|
||||||
|
|
||||||
|
contentEl.createEl('h2', { text: this.isNew ? 'Add Export Profile' : 'Edit Export Profile' });
|
||||||
|
|
||||||
|
// Name
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName('Name')
|
||||||
|
.setDesc('Display name for this profile')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('e.g. Full LLM Export')
|
||||||
|
.setValue(this.name)
|
||||||
|
.onChange(v => this.name = v));
|
||||||
|
|
||||||
|
// Description
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName('Description')
|
||||||
|
.setDesc('Optional description')
|
||||||
|
.addTextArea(text => {
|
||||||
|
text.setPlaceholder('Describe what this profile does...')
|
||||||
|
.setValue(this.description)
|
||||||
|
.onChange(v => this.description = v);
|
||||||
|
text.inputEl.rows = 2;
|
||||||
|
text.inputEl.style.width = '100%';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Target checklist
|
||||||
|
contentEl.createEl('h3', { text: 'Targets' });
|
||||||
|
|
||||||
|
const targets = this.plugin.settings.targets;
|
||||||
|
if (targets.length === 0) {
|
||||||
|
contentEl.createEl('p', {
|
||||||
|
text: 'No targets configured. Create targets first in Settings > Output.',
|
||||||
|
cls: 'setting-item-description',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const checklistContainer = contentEl.createDiv({ cls: 'profile-targets-checklist' });
|
||||||
|
checklistContainer.style.display = 'flex';
|
||||||
|
checklistContainer.style.flexDirection = 'column';
|
||||||
|
checklistContainer.style.gap = '8px';
|
||||||
|
checklistContainer.style.padding = '10px';
|
||||||
|
checklistContainer.style.backgroundColor = 'var(--background-secondary)';
|
||||||
|
checklistContainer.style.borderRadius = '4px';
|
||||||
|
checklistContainer.style.marginBottom = '15px';
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
const row = checklistContainer.createDiv();
|
||||||
|
row.style.display = 'flex';
|
||||||
|
row.style.alignItems = 'center';
|
||||||
|
row.style.gap = '10px';
|
||||||
|
|
||||||
|
const checkbox = row.createEl('input', { type: 'checkbox' });
|
||||||
|
checkbox.checked = this.selectedTargetIds.has(target.id);
|
||||||
|
checkbox.addEventListener('change', () => {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
this.selectedTargetIds.add(target.id);
|
||||||
|
} else {
|
||||||
|
this.selectedTargetIds.delete(target.id);
|
||||||
|
if (this.primaryTargetId === target.id) {
|
||||||
|
this.primaryTargetId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.refreshPrimaryDropdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
row.createEl('span', { text: getTargetIcon(target.format) });
|
||||||
|
|
||||||
|
const label = row.createEl('span', { text: target.name });
|
||||||
|
label.style.flex = '1';
|
||||||
|
|
||||||
|
const tokenInfo = row.createEl('span', {
|
||||||
|
text: `${target.maxTokens.toLocaleString()} tokens · ${target.format}`
|
||||||
|
});
|
||||||
|
tokenInfo.style.fontSize = '11px';
|
||||||
|
tokenInfo.style.color = 'var(--text-muted)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary target dropdown
|
||||||
|
this.primaryDropdownContainer = contentEl.createDiv();
|
||||||
|
this.refreshPrimaryDropdown();
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
const buttonContainer = contentEl.createDiv();
|
||||||
|
buttonContainer.style.display = 'flex';
|
||||||
|
buttonContainer.style.justifyContent = 'flex-end';
|
||||||
|
buttonContainer.style.gap = '10px';
|
||||||
|
buttonContainer.style.marginTop = '20px';
|
||||||
|
|
||||||
|
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel' });
|
||||||
|
cancelBtn.addEventListener('click', () => this.close());
|
||||||
|
|
||||||
|
const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'mod-cta' });
|
||||||
|
saveBtn.addEventListener('click', () => this.save());
|
||||||
|
}
|
||||||
|
|
||||||
|
private primaryDropdownContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
|
private refreshPrimaryDropdown() {
|
||||||
|
if (!this.primaryDropdownContainer) return;
|
||||||
|
this.primaryDropdownContainer.empty();
|
||||||
|
|
||||||
|
const selectedTargets = this.plugin.settings.targets.filter(
|
||||||
|
t => this.selectedTargetIds.has(t.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedTargets.length === 0) return;
|
||||||
|
|
||||||
|
// Validate current primary is still in selection
|
||||||
|
if (this.primaryTargetId && !this.selectedTargetIds.has(this.primaryTargetId)) {
|
||||||
|
this.primaryTargetId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
new Setting(this.primaryDropdownContainer)
|
||||||
|
.setName('Primary target')
|
||||||
|
.setDesc('This target\'s output goes to clipboard; others are saved as files')
|
||||||
|
.addDropdown(dropdown => {
|
||||||
|
dropdown.addOption('', 'First selected target');
|
||||||
|
for (const target of selectedTargets) {
|
||||||
|
dropdown.addOption(target.id, target.name);
|
||||||
|
}
|
||||||
|
dropdown.setValue(this.primaryTargetId || '');
|
||||||
|
dropdown.onChange(v => this.primaryTargetId = v || null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
if (!this.name.trim()) {
|
||||||
|
new Notice('Name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedTargetIds.size === 0) {
|
||||||
|
new Notice('Select at least one target');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate primary is in selection
|
||||||
|
if (this.primaryTargetId && !this.selectedTargetIds.has(this.primaryTargetId)) {
|
||||||
|
this.primaryTargetId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileData: ExportProfile = {
|
||||||
|
id: this.profile?.id || generateProfileId(),
|
||||||
|
name: this.name.trim(),
|
||||||
|
description: this.description.trim() || undefined,
|
||||||
|
targetIds: Array.from(this.selectedTargetIds),
|
||||||
|
primaryTargetId: this.primaryTargetId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isNew) {
|
||||||
|
this.plugin.settings.exportProfiles.push(profileData);
|
||||||
|
} else {
|
||||||
|
const index = this.plugin.settings.exportProfiles.findIndex(p => p.id === this.profile!.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.plugin.settings.exportProfiles[index] = profileData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
new Notice(this.isNew ? 'Profile added' : 'Profile updated');
|
||||||
|
this.onSave();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
this.primaryDropdownContainer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { App, Modal, Notice, Setting, TFile, TFolder } from 'obsidian';
|
||||||
import PromptfirePlugin from './main';
|
import PromptfirePlugin from './main';
|
||||||
import { createFreetextSource, SourcePosition } from './sources';
|
import { createFreetextSource, SourcePosition } from './sources';
|
||||||
import { estimateTokens } from './history';
|
import { estimateTokens } from './history';
|
||||||
import { OutputTarget, OutputFormat, getTargetIcon, formatTokenCount } from './targets';
|
import { OutputTarget, ExportProfile, OutputFormat, getTargetIcon, formatTokenCount } from './targets';
|
||||||
|
|
||||||
interface FolderConfig {
|
interface FolderConfig {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -425,6 +425,29 @@ export class ContextGeneratorModal extends Modal {
|
||||||
if (this.plugin.settings.targets.length > 0) {
|
if (this.plugin.settings.targets.length > 0) {
|
||||||
this.targetsHeadingEl = configZone.createEl('h3', { text: 'Output Targets' });
|
this.targetsHeadingEl = configZone.createEl('h3', { text: 'Output Targets' });
|
||||||
|
|
||||||
|
// Profile dropdown (only if profiles exist)
|
||||||
|
if (this.plugin.settings.exportProfiles.length > 0) {
|
||||||
|
new Setting(configZone)
|
||||||
|
.setName('Profile')
|
||||||
|
.setDesc('Pre-select targets from an export profile')
|
||||||
|
.addDropdown(dropdown => {
|
||||||
|
dropdown.addOption('', 'Manual selection');
|
||||||
|
for (const profile of this.plugin.settings.exportProfiles) {
|
||||||
|
dropdown.addOption(profile.id, profile.name);
|
||||||
|
}
|
||||||
|
dropdown.setValue('');
|
||||||
|
dropdown.onChange(v => {
|
||||||
|
if (v) {
|
||||||
|
const profile = this.plugin.settings.exportProfiles.find(p => p.id === v);
|
||||||
|
if (profile) {
|
||||||
|
this.selectedTargetIds = new Set(profile.targetIds);
|
||||||
|
this.onOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const targetsContainer = configZone.createDiv({ cls: 'targets-checkboxes' });
|
const targetsContainer = configZone.createDiv({ cls: 'targets-checkboxes' });
|
||||||
targetsContainer.style.display = 'flex';
|
targetsContainer.style.display = 'flex';
|
||||||
targetsContainer.style.flexDirection = 'column';
|
targetsContainer.style.flexDirection = 'column';
|
||||||
|
|
|
||||||
54
src/main.ts
54
src/main.ts
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from './presets';
|
} from './presets';
|
||||||
import {
|
import {
|
||||||
OutputTarget,
|
OutputTarget,
|
||||||
|
ExportProfile,
|
||||||
TargetExecutor,
|
TargetExecutor,
|
||||||
TargetResult,
|
TargetResult,
|
||||||
saveTargetToFile,
|
saveTargetToFile,
|
||||||
|
|
@ -92,6 +93,12 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
callback: () => this.copySmartContext()
|
callback: () => this.copySmartContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'export-multi-target',
|
||||||
|
name: 'Export context (multi-target)',
|
||||||
|
callback: () => this.exportMultiTarget()
|
||||||
|
});
|
||||||
|
|
||||||
this.addSettingTab(new PromptfireSettingTab(this.app, this));
|
this.addSettingTab(new PromptfireSettingTab(this.app, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,6 +123,12 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!loaded?.exportProfiles) {
|
||||||
|
this.settings.exportProfiles = [];
|
||||||
|
}
|
||||||
|
if (loaded?.activeProfileId === undefined) {
|
||||||
|
this.settings.activeProfileId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
|
|
@ -277,7 +290,8 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
forceIncludeNote = false,
|
forceIncludeNote = false,
|
||||||
temporaryFreetext?: string,
|
temporaryFreetext?: string,
|
||||||
templateId?: string | null,
|
templateId?: string | null,
|
||||||
targets?: OutputTarget[]
|
targets?: OutputTarget[],
|
||||||
|
profilePrimaryTargetId?: string | null
|
||||||
) {
|
) {
|
||||||
const folder = this.app.vault.getAbstractFileByPath(this.settings.contextFolder);
|
const folder = this.app.vault.getAbstractFileByPath(this.settings.contextFolder);
|
||||||
|
|
||||||
|
|
@ -412,7 +426,7 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
|
|
||||||
// Process targets if provided
|
// Process targets if provided
|
||||||
if (targets && targets.length > 0) {
|
if (targets && targets.length > 0) {
|
||||||
await this.processTargets(combined, targets, historyMetadata, fileCount, sourceCount, templateName);
|
await this.processTargets(combined, targets, historyMetadata, fileCount, sourceCount, templateName, profilePrimaryTargetId);
|
||||||
} else {
|
} else {
|
||||||
// Copy and save to history (legacy mode)
|
// Copy and save to history (legacy mode)
|
||||||
const copyAndSave = async () => {
|
const copyAndSave = async () => {
|
||||||
|
|
@ -437,7 +451,8 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
historyMetadata: Omit<HistoryMetadata, 'charCount' | 'estimatedTokens'>,
|
historyMetadata: Omit<HistoryMetadata, 'charCount' | 'estimatedTokens'>,
|
||||||
fileCount: number,
|
fileCount: number,
|
||||||
sourceCount: number,
|
sourceCount: number,
|
||||||
templateName: string | null
|
templateName: string | null,
|
||||||
|
overridePrimaryId?: string | null
|
||||||
) {
|
) {
|
||||||
const executor = new TargetExecutor();
|
const executor = new TargetExecutor();
|
||||||
const results: TargetResult[] = [];
|
const results: TargetResult[] = [];
|
||||||
|
|
@ -449,7 +464,8 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine primary target
|
// Determine primary target
|
||||||
const primaryId = this.settings.primaryTargetId ||
|
const primaryId = overridePrimaryId ??
|
||||||
|
this.settings.primaryTargetId ??
|
||||||
(targets.find(t => t.enabled)?.id ?? targets[0]?.id);
|
(targets.find(t => t.enabled)?.id ?? targets[0]?.id);
|
||||||
const primaryResult = results.find(r => r.target.id === primaryId) || results[0];
|
const primaryResult = results.find(r => r.target.id === primaryId) || results[0];
|
||||||
|
|
||||||
|
|
@ -626,6 +642,36 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
return checkAll(selection.headings);
|
return checkAll(selection.headings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async exportMultiTarget() {
|
||||||
|
const profileId = this.settings.activeProfileId;
|
||||||
|
if (!profileId) {
|
||||||
|
new Notice('No active export profile. Set one in Settings > Output > Export Profiles.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = this.settings.exportProfiles.find(p => p.id === profileId);
|
||||||
|
if (!profile) {
|
||||||
|
new Notice('Active export profile not found. Check Settings > Output > Export Profiles.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = profile.targetIds
|
||||||
|
.map(id => this.settings.targets.find(t => t.id === id))
|
||||||
|
.filter((t): t is OutputTarget => t != null);
|
||||||
|
|
||||||
|
if (targets.length === 0) {
|
||||||
|
new Notice(`Profile "${profile.name}" has no valid targets.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targets.length < profile.targetIds.length) {
|
||||||
|
const missing = profile.targetIds.length - targets.length;
|
||||||
|
new Notice(`${missing} target(s) in profile no longer exist and were skipped.`, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.copyContextToClipboard(false, undefined, undefined, targets, profile.primaryTargetId);
|
||||||
|
}
|
||||||
|
|
||||||
async copySmartContext() {
|
async copySmartContext() {
|
||||||
if (!this.settings.intelligence.enabled) {
|
if (!this.settings.intelligence.enabled) {
|
||||||
new Notice('Context intelligence is disabled. Enable it in Settings > Intelligence.');
|
new Notice('Context intelligence is disabled. Enable it in Settings > Intelligence.');
|
||||||
|
|
|
||||||
142
src/settings.ts
142
src/settings.ts
|
|
@ -5,8 +5,9 @@ import { SourceModal } from './source-modal';
|
||||||
import { PromptTemplate, STARTER_TEMPLATES } from './templates';
|
import { PromptTemplate, STARTER_TEMPLATES } from './templates';
|
||||||
import { TemplateModal, TemplateImportExportModal } from './template-modal';
|
import { TemplateModal, TemplateImportExportModal } from './template-modal';
|
||||||
import { HistorySettings, DEFAULT_HISTORY_SETTINGS } from './history';
|
import { HistorySettings, DEFAULT_HISTORY_SETTINGS } from './history';
|
||||||
import { OutputTarget, BUILTIN_TARGETS, getTargetIcon } from './targets';
|
import { OutputTarget, ExportProfile, BUILTIN_TARGETS, BUILTIN_PROFILES, getTargetIcon } from './targets';
|
||||||
import { TargetModal } from './target-modal';
|
import { TargetModal } from './target-modal';
|
||||||
|
import { ExportProfileModal } from './export-profile-modal';
|
||||||
import { IntelligenceSettings, DEFAULT_INTELLIGENCE_SETTINGS } from './context-intelligence';
|
import { IntelligenceSettings, DEFAULT_INTELLIGENCE_SETTINGS } from './context-intelligence';
|
||||||
|
|
||||||
export interface PromptfireSettings {
|
export interface PromptfireSettings {
|
||||||
|
|
@ -24,6 +25,8 @@ export interface PromptfireSettings {
|
||||||
targets: OutputTarget[];
|
targets: OutputTarget[];
|
||||||
primaryTargetId: string | null;
|
primaryTargetId: string | null;
|
||||||
targetOutputFolder: string;
|
targetOutputFolder: string;
|
||||||
|
exportProfiles: ExportProfile[];
|
||||||
|
activeProfileId: string | null;
|
||||||
lastSettingsTab: string;
|
lastSettingsTab: string;
|
||||||
collapsedSections: string[];
|
collapsedSections: string[];
|
||||||
generatorPreviewOpen: boolean;
|
generatorPreviewOpen: boolean;
|
||||||
|
|
@ -45,6 +48,8 @@ export const DEFAULT_SETTINGS: PromptfireSettings = {
|
||||||
targets: [],
|
targets: [],
|
||||||
primaryTargetId: null,
|
primaryTargetId: null,
|
||||||
targetOutputFolder: '_context/outputs',
|
targetOutputFolder: '_context/outputs',
|
||||||
|
exportProfiles: [],
|
||||||
|
activeProfileId: null,
|
||||||
lastSettingsTab: 'general',
|
lastSettingsTab: 'general',
|
||||||
collapsedSections: [],
|
collapsedSections: [],
|
||||||
generatorPreviewOpen: false,
|
generatorPreviewOpen: false,
|
||||||
|
|
@ -530,6 +535,21 @@ export class PromptfireSettingTab extends PluginSettingTab {
|
||||||
this.setDynamicDesc(primaryTargetSetting, 'This target\'s output is copied to clipboard',
|
this.setDynamicDesc(primaryTargetSetting, 'This target\'s output is copied to clipboard',
|
||||||
totalTargets > 0 ? `${enabledTargets} of ${totalTargets} enabled` : '');
|
totalTargets > 0 ? `${enabledTargets} of ${totalTargets} enabled` : '');
|
||||||
|
|
||||||
|
new Setting(oc)
|
||||||
|
.setName('Active export profile')
|
||||||
|
.setDesc('Profile used by the "Export context (multi-target)" command')
|
||||||
|
.addDropdown(dropdown => {
|
||||||
|
dropdown.addOption('', 'None');
|
||||||
|
for (const profile of this.plugin.settings.exportProfiles) {
|
||||||
|
dropdown.addOption(profile.id, profile.name);
|
||||||
|
}
|
||||||
|
dropdown.setValue(this.plugin.settings.activeProfileId || '');
|
||||||
|
dropdown.onChange(async (value) => {
|
||||||
|
this.plugin.settings.activeProfileId = value || null;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const outputFolderSetting = new Setting(oc)
|
const outputFolderSetting = new Setting(oc)
|
||||||
.setName('Output folder')
|
.setName('Output folder')
|
||||||
.addText(text => text
|
.addText(text => text
|
||||||
|
|
@ -541,6 +561,37 @@ export class PromptfireSettingTab extends PluginSettingTab {
|
||||||
this.describeOutputFolder(outputFolderSetting, this.plugin.settings.targetOutputFolder);
|
this.describeOutputFolder(outputFolderSetting, this.plugin.settings.targetOutputFolder);
|
||||||
}));
|
}));
|
||||||
this.describeOutputFolder(outputFolderSetting, this.plugin.settings.targetOutputFolder);
|
this.describeOutputFolder(outputFolderSetting, this.plugin.settings.targetOutputFolder);
|
||||||
|
|
||||||
|
// Section: Export Profiles
|
||||||
|
const profilesSection = new CollapsibleSection(el, 'output-profiles', 'Export profiles', this.plugin);
|
||||||
|
const pc = profilesSection.contentEl;
|
||||||
|
|
||||||
|
const profileButtonContainer = pc.createDiv({ cls: 'profiles-button-container' });
|
||||||
|
profileButtonContainer.style.display = 'flex';
|
||||||
|
profileButtonContainer.style.gap = '8px';
|
||||||
|
profileButtonContainer.style.marginBottom = '15px';
|
||||||
|
|
||||||
|
const addProfileBtn = profileButtonContainer.createEl('button', { text: '+ New Profile' });
|
||||||
|
addProfileBtn.addEventListener('click', () => {
|
||||||
|
new ExportProfileModal(this.app, this.plugin, null, () => this.display()).open();
|
||||||
|
});
|
||||||
|
|
||||||
|
const addBuiltinProfilesBtn = profileButtonContainer.createEl('button', { text: 'Add Built-in Profiles' });
|
||||||
|
addBuiltinProfilesBtn.addEventListener('click', async () => {
|
||||||
|
const existingIds = this.plugin.settings.exportProfiles.map(p => p.id);
|
||||||
|
const newProfiles = BUILTIN_PROFILES.filter(p => !existingIds.includes(p.id));
|
||||||
|
if (newProfiles.length === 0) {
|
||||||
|
new Notice('All built-in profiles already added');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.plugin.settings.exportProfiles.push(...newProfiles);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
new Notice(`Added ${newProfiles.length} built-in profile(s)`);
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
|
||||||
|
const profilesContainer = pc.createDiv({ cls: 'profiles-list-container' });
|
||||||
|
this.renderProfilesList(profilesContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === TAB: History ===
|
// === TAB: History ===
|
||||||
|
|
@ -1100,6 +1151,13 @@ export class PromptfireSettingTab extends PluginSettingTab {
|
||||||
if (this.plugin.settings.primaryTargetId === target.id) {
|
if (this.plugin.settings.primaryTargetId === target.id) {
|
||||||
this.plugin.settings.primaryTargetId = null;
|
this.plugin.settings.primaryTargetId = null;
|
||||||
}
|
}
|
||||||
|
// Clean up profile references
|
||||||
|
for (const profile of this.plugin.settings.exportProfiles) {
|
||||||
|
profile.targetIds = profile.targetIds.filter(id => id !== target.id);
|
||||||
|
if (profile.primaryTargetId === target.id) {
|
||||||
|
profile.primaryTargetId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
this.display();
|
this.display();
|
||||||
});
|
});
|
||||||
|
|
@ -1111,4 +1169,86 @@ export class PromptfireSettingTab extends PluginSettingTab {
|
||||||
lastTargetRow.style.borderBottom = 'none';
|
lastTargetRow.style.borderBottom = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderProfilesList(container: HTMLElement) {
|
||||||
|
container.empty();
|
||||||
|
|
||||||
|
if (this.plugin.settings.exportProfiles.length === 0) {
|
||||||
|
const emptyMsg = container.createEl('p', {
|
||||||
|
text: 'No export profiles configured yet.',
|
||||||
|
cls: 'setting-item-description'
|
||||||
|
});
|
||||||
|
emptyMsg.style.fontStyle = 'italic';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = container.createDiv({ cls: 'profiles-list' });
|
||||||
|
list.style.border = '1px solid var(--background-modifier-border)';
|
||||||
|
list.style.borderRadius = '4px';
|
||||||
|
list.style.marginBottom = '15px';
|
||||||
|
|
||||||
|
for (const profile of this.plugin.settings.exportProfiles) {
|
||||||
|
const row = list.createDiv({ cls: 'profile-row' });
|
||||||
|
row.style.display = 'flex';
|
||||||
|
row.style.alignItems = 'center';
|
||||||
|
row.style.padding = '8px 12px';
|
||||||
|
row.style.borderBottom = '1px solid var(--background-modifier-border)';
|
||||||
|
row.style.gap = '10px';
|
||||||
|
|
||||||
|
// Name and info
|
||||||
|
const textContainer = row.createDiv();
|
||||||
|
textContainer.style.flex = '1';
|
||||||
|
|
||||||
|
const name = textContainer.createEl('span', { text: profile.name });
|
||||||
|
name.style.fontWeight = '500';
|
||||||
|
|
||||||
|
// Show target names
|
||||||
|
const targetNames = profile.targetIds
|
||||||
|
.map(id => this.plugin.settings.targets.find(t => t.id === id)?.name)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ');
|
||||||
|
if (targetNames) {
|
||||||
|
const info = textContainer.createEl('div', { text: targetNames });
|
||||||
|
info.style.fontSize = '11px';
|
||||||
|
info.style.color = 'var(--text-muted)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active badge
|
||||||
|
if (this.plugin.settings.activeProfileId === profile.id) {
|
||||||
|
const badge = row.createEl('span', { text: 'active' });
|
||||||
|
badge.style.padding = '2px 6px';
|
||||||
|
badge.style.borderRadius = '3px';
|
||||||
|
badge.style.fontSize = '11px';
|
||||||
|
badge.style.backgroundColor = 'var(--interactive-accent)';
|
||||||
|
badge.style.color = 'var(--text-on-accent)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit button
|
||||||
|
const editBtn = row.createEl('button', { text: '✎' });
|
||||||
|
editBtn.style.padding = '2px 8px';
|
||||||
|
editBtn.addEventListener('click', () => {
|
||||||
|
new ExportProfileModal(this.app, this.plugin, profile, () => this.display()).open();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
const deleteBtn = row.createEl('button', { text: '✕' });
|
||||||
|
deleteBtn.style.padding = '2px 8px';
|
||||||
|
deleteBtn.addEventListener('click', async () => {
|
||||||
|
this.plugin.settings.exportProfiles = this.plugin.settings.exportProfiles.filter(
|
||||||
|
p => p.id !== profile.id
|
||||||
|
);
|
||||||
|
if (this.plugin.settings.activeProfileId === profile.id) {
|
||||||
|
this.plugin.settings.activeProfileId = null;
|
||||||
|
}
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove bottom border from last item
|
||||||
|
const lastProfileRow = list.lastElementChild as HTMLElement;
|
||||||
|
if (lastProfileRow) {
|
||||||
|
lastProfileRow.style.borderBottom = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export class TargetModal extends Modal {
|
||||||
wrapperPrefix: string = '';
|
wrapperPrefix: string = '';
|
||||||
wrapperSuffix: string = '';
|
wrapperSuffix: string = '';
|
||||||
separator: string = '\n\n---\n\n';
|
separator: string = '\n\n---\n\n';
|
||||||
|
outputPath: string = '';
|
||||||
enabled: boolean = true;
|
enabled: boolean = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -44,6 +45,7 @@ export class TargetModal extends Modal {
|
||||||
this.wrapperPrefix = target.wrapper?.prefix || '';
|
this.wrapperPrefix = target.wrapper?.prefix || '';
|
||||||
this.wrapperSuffix = target.wrapper?.suffix || '';
|
this.wrapperSuffix = target.wrapper?.suffix || '';
|
||||||
this.separator = target.separator || '\n\n---\n\n';
|
this.separator = target.separator || '\n\n---\n\n';
|
||||||
|
this.outputPath = target.outputPath || '';
|
||||||
this.enabled = target.enabled;
|
this.enabled = target.enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +140,15 @@ export class TargetModal extends Modal {
|
||||||
text.inputEl.style.width = '100%';
|
text.inputEl.style.width = '100%';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Output path
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName('Output path')
|
||||||
|
.setDesc('Custom folder for this target\'s file output (leave empty for default)')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('_context/outputs')
|
||||||
|
.setValue(this.outputPath)
|
||||||
|
.onChange(v => this.outputPath = v));
|
||||||
|
|
||||||
// Enabled toggle
|
// Enabled toggle
|
||||||
new Setting(contentEl)
|
new Setting(contentEl)
|
||||||
.setName('Enabled')
|
.setName('Enabled')
|
||||||
|
|
@ -180,6 +191,7 @@ export class TargetModal extends Modal {
|
||||||
format: this.format,
|
format: this.format,
|
||||||
strategy: this.strategy,
|
strategy: this.strategy,
|
||||||
separator: this.separator,
|
separator: this.separator,
|
||||||
|
outputPath: this.outputPath.trim() || undefined,
|
||||||
enabled: this.enabled,
|
enabled: this.enabled,
|
||||||
isBuiltin: false,
|
isBuiltin: false,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export interface OutputTarget {
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
};
|
};
|
||||||
separator?: string;
|
separator?: string;
|
||||||
|
outputPath?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
isBuiltin?: boolean;
|
isBuiltin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -29,12 +30,24 @@ export interface TargetResult {
|
||||||
sectionsDropped: number;
|
sectionsDropped: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExportProfile {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
targetIds: string[];
|
||||||
|
primaryTargetId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
// === ID Generation ===
|
// === ID Generation ===
|
||||||
|
|
||||||
export function generateTargetId(): string {
|
export function generateTargetId(): string {
|
||||||
return 'target_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
|
return 'target_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateProfileId(): string {
|
||||||
|
return 'profile_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
|
||||||
|
}
|
||||||
|
|
||||||
// === Built-in Targets ===
|
// === Built-in Targets ===
|
||||||
|
|
||||||
export const BUILTIN_TARGETS: OutputTarget[] = [
|
export const BUILTIN_TARGETS: OutputTarget[] = [
|
||||||
|
|
@ -74,6 +87,16 @@ export const BUILTIN_TARGETS: OutputTarget[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const BUILTIN_PROFILES: ExportProfile[] = [
|
||||||
|
{
|
||||||
|
id: 'builtin_profile_full',
|
||||||
|
name: 'Full LLM Export',
|
||||||
|
description: 'All built-in targets: XML for Claude, Markdown for GPT-4o, Plain for compact',
|
||||||
|
targetIds: ['builtin_promptfire', 'builtin_gpt4', 'builtin_compact'],
|
||||||
|
primaryTargetId: 'builtin_promptfire',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// === Content Transformer ===
|
// === Content Transformer ===
|
||||||
|
|
||||||
export class ContentTransformer {
|
export class ContentTransformer {
|
||||||
|
|
@ -480,14 +503,15 @@ export async function saveTargetToFile(
|
||||||
result: TargetResult,
|
result: TargetResult,
|
||||||
outputFolder: string
|
outputFolder: string
|
||||||
): Promise<TFile | null> {
|
): Promise<TFile | null> {
|
||||||
|
const effectiveFolder = result.target.outputPath || outputFolder;
|
||||||
const filename = `context-${result.target.name.toLowerCase().replace(/\s+/g, '-')}.md`;
|
const filename = `context-${result.target.name.toLowerCase().replace(/\s+/g, '-')}.md`;
|
||||||
const path = `${outputFolder}/${filename}`;
|
const path = `${effectiveFolder}/${filename}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure folder exists
|
// Ensure folder exists
|
||||||
const folder = app.vault.getAbstractFileByPath(outputFolder);
|
const folder = app.vault.getAbstractFileByPath(effectiveFolder);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
await app.vault.createFolder(outputFolder);
|
await app.vault.createFolder(effectiveFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or update file
|
// Create or update file
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue