stuck(model): stuck trying to get model Claude API working

On branch development
 Your branch is up to date with 'origin/development'.
 Changes to be committed:
	new file:   .logs/obsidian.md-1777711786079.log
	modified:   context-v/issue-resolutions/Getting-Claude-to-Respond-With-Research.md
	new file:   context-v/specs/Using-Files-as-Prompt-Outlines.md
	modified:   src/modals/ArticleGeneratorModal.ts
	modified:   src/modals/ClaudeModal.ts
	modified:   src/modals/PerplexityModal.ts
	modified:   src/services/claudeService.ts
	modified:   src/styles/article-generator-modal.css
	modified:   src/styles/perplexity-modal.css
	modified:   styles.css
This commit is contained in:
mpstaton 2026-05-02 04:16:09 -05:00
parent b5a70c72a4
commit d7551d7bc7
10 changed files with 1288 additions and 406 deletions

5
.gitignore vendored
View file

@ -21,5 +21,10 @@ main.js
# obsidian
data.json
# Logs (Obsidian dev console exports often contain plaintext API keys
# from the loaded data.json — never commit these)
.logs/
*.log
# Exclude macOS Finder (System Explorer) View States
.DS_Store

View file

@ -30,4 +30,141 @@
No file edits this turn per your direction. When you've run a test and pasted me the
[ClaudeService] log line, I'll know which fix to make. If you want me to just ship the tool_choice:
any + system-prompt fix preemptively (it'd handle both failure modes), say the word.
any + system-prompt fix preemptively (it'd handle both failure modes), say the word.
But your question opens the door wider — we could parse everything else in the
response object too:
Block type: web_search_tool_result
What's in it: URL + title + encrypted_content + page_age per result
Currently parsed?: ✓ URL + title only
Could be: + page_age for date-aware refdefs
────────────────────────────────────────
Block type: server_tool_use (web_search)
What's in it: The actual query string Claude searched for
Currently parsed?: ✗
Could be: Group references under the query that produced them
────────────────────────────────────────
Block type: code_execution_tool_result
What's in it: Output of dynamic-filter code — likely contains the filtered snippets Claude actually
used
Currently parsed?: ✗
Could be: Pull cited_text-equivalents from here
────────────────────────────────────────
Block type: text blocks (no citations attached)
What's in it: Just the narrative
Currently parsed?: ✓ for body
Could be: —
Going deeper means we don't need Claude to attach per-claim citations — we synthesize what we need
from the surrounding blocks. Trade-off: it's heuristic (we can't know exactly which sentence came
from which URL without per-claim attachment), but a Citations section enriched with search query +
URL+title + snippet from filter output is genuinely useful research provenance, not just a flat URL
list.
But before I go bigger — the gating question is still whether addCitations is actually writing the
section to the file. The log proves we extract 60 entries. If they're not reaching the file,
expanding the extractor doesn't help. The simplest way to find out: I add one more log line right
after the editor.replaceRange call in addCitations that prints [ClaudeService] Citations section
written: N entries, lastLine=X. Then on next run, the log tells us definitively whether the write
happened.
Proposed two-step:
1. Now (small): instrument addCitations with a post-write log. You re-run, paste the log line. We
confirm the write or catch the silent failure.
2. Then (bigger): extend the parser to pull search queries from server_tool_use blocks and snippets
from code_execution_tool_result blocks. Citations section becomes:
### Citations
#### Search: "agent builder market 2025 startups"
[1]: [Title](url). > snippet from filter output...
[2]: [Title](url). > snippet...
#### Search: "OpenAI AgentKit launch DevDay"
[3]: [Title](url). > snippet...
The data IS in the response object. There are two distinct citation surfaces
in Anthropic's web search responses:
┌────────────────┬────────────────────────────┬───────────────────────────────────┬────────────┐
│ │ │ │ Status in │
│ Surface │ What it contains │ Where it lives │ your last │
│ │ │ │ run │
├────────────────┼────────────────────────────┼───────────────────────────────────┼────────────┤
│ │ URL + title + │ │ ✓ Present │
│ Search-result │ encrypted_content + │ web_search_tool_result.content[] │ — 60 of │
│ blocks │ page_age for every page │ array │ them │
│ │ Claude searched │ │ │
├────────────────┼────────────────────────────┼───────────────────────────────────┼────────────┤
│ Per-claim │ cited_text quote + URL + │ │ ✗ Zero │
│ citations │ title, attached to a │ TextBlock.citations[] array │ attached │
│ │ specific text block │ │ │
└────────────────┴────────────────────────────┴───────────────────────────────────┴────────────┘
We have surface #1 (the URLs and titles of every searched page). We don't have surface #2 (the
mapping from "this specific sentence" to "this specific URL"). The dynamic-filtering pass on
web_search_20260209 is doing its post-processing in a code-execution sandbox and the per-claim
attachment isn't surviving the round-trip — that's the apparent root cause.
What that means practically:
- We CAN populate a Citations section at the bottom of the note with all 60 URLs+titles. My
fallback already does this. The references are real and verifiable.
- We CAN'T put inline [N] markers next to specific claims, because the response doesn't tell us
which sentence came from which URL. Building that mapping ourselves would be heuristic guesswork
(string-matching titles against text) — fragile and easy to get wrong.
So the real question is: did your test file get a ### Citations section appended at the very end
with 60 entries? That tells us where the gap actually is:
- Yes, it has the section → we have references-but-no-inline-markers. The fix for inline markers is
to switch to web_search_20250305 (the older tool version) which reliably attaches per-claim
citations.
- No, the section never appears → my addCitations writer is silently failing for a reason I haven't
pinned yet, and I need to add another log line that fires after the editor write.
Could you scroll to the bottom of the file you tested on (looks like OpenClaw.md from the log) and
tell me which of those two it is? If it's "no section" I'll instrument addCitations to log
success/failure of the write itself and we'll catch it on the next run.
┌─────────────────────────────────┬──────────────────────────────────┬────────────────────────┐
│ Step │ Who │ What │
├─────────────────────────────────┼──────────────────────────────────┼────────────────────────┤
│ 1. Search the web │ Claude │ Issued 6+ search │
│ │ │ queries, got results │
├─────────────────────────────────┼──────────────────────────────────┼────────────────────────┤
│ │ Claude (via │ Post-processed results │
│ 2. Filter results in code │ web_search_20260209's dynamic │ in its sandbox │
│ │ filter) │ │
├─────────────────────────────────┼──────────────────────────────────┼────────────────────────┤
│ 3. Emit web_search_tool_result │ │ 125 entries in the │
│ blocks with URL+title for every │ Claude │ response object │
│ searched page │ │ │
├─────────────────────────────────┼──────────────────────────────────┼────────────────────────┤
│ 4. Emit │ │ │
│ web_search_result_location │ Claude │ 0 entries — Claude │
│ per-claim citations on text │ │ didn't do this │
│ blocks │ │ │
├─────────────────────────────────┼──────────────────────────────────┼────────────────────────┤
│ 5. Parse the response object's │ │ The 125 numbered │
│ web_search_tool_result blocks │ My fallback code │ refdefs you see in AI │
│ and write a ### Citations │ (claudeService.ts:addCitations) │ Copilots.md:135391 │
│ section │ │ │
└─────────────────────────────────┴──────────────────────────────────┴────────────────────────┘
So Claude provided the raw URL data; my code authored the section. The trailing-period format [N]:
[Title](url). is my code's signature; if Claude had attached per-claim citations directly, you'd
also see (a) inline [N] markers within the prose and (b) > cited_text blockquotes on each refdef.
Neither is present, confirming step 4 didn't happen.
About the blog you linked. It's the launch announcement for the Citations API as a
document-grounding feature — quote: "user-provided source documents (PDF documents and plain text
files)." It predates web-search-citation integration entirely. The blog's primary use case is "give
Claude a PDF, get answers with cited passages from that PDF." Web search citations are a later
addition with different mechanics — and with web_search_20260209 + dynamic filtering, per-claim
attachment to text blocks isn't surviving the post-processing round-trip. That's the real gap.

View file

@ -0,0 +1,14 @@
On the Outlines folder feature — yes, totally feasible and a great unlock. Sketch:
- New plugin setting: outlinesFolderPath (default Outlines-Perplexed).
- Each .md file in that folder = one template. Frontmatter declares name, description, provider
(perplexity | claude | both), optional model, optional effort. Body is the prompt with {{TERM}} /
{{TOPIC}} / {{QUERY}} placeholders.
- New command: "Generate from Outline…" opens a modal with: outline picker (dropdown of files in
the folder), term/topic input, provider/model overrides, submit.
- ArticleGeneratorModal becomes one built-in outline (deep-research one-pager) — same flow, no
special-casing.
This decouples your prompt library from the codebase entirely. Add a new template = drop a .md in
the folder, no rebuild needed. It ALSO means when Claude does work, you can author Claude-specific
outlines without touching code.

View file

@ -1,151 +1,254 @@
import { App, Editor, Notice } from 'obsidian';
import { PerplexityService, PerplexityOptions } from '../services/perplexityService';
import { PerplexityModal } from './PerplexityModal';
import { PromptsService } from '../services/promptsService';
import { App, Modal, Notice, Setting } from 'obsidian';
import type { Editor } from 'obsidian';
import type { PerplexityService, PerplexityOptions } from '../services/perplexityService';
import type { PromptsService } from '../services/promptsService';
export class ArticleGeneratorModal extends PerplexityModal {
private termInput!: HTMLInputElement;
const PERPLEXITY_MODELS: Array<{ value: string; label: string; tagline?: string }> = [
{ value: 'sonar-deep-research', label: 'sonar-deep-research', tagline: 'Recommended — exhaustive multi-source article research; takes 3060 seconds' },
{ value: 'sonar-pro', label: 'sonar-pro', tagline: 'Faster, less detail' },
{ value: 'sonar-small', label: 'sonar-small', tagline: 'Cheapest, briefest' },
{ value: 'llama-3.1-sonar-small-128k-online', label: 'llama-3.1-sonar-small-128k-online', tagline: 'Llama 3.1 small, 128k context, online' },
{ value: 'llama-3.1-sonar-large-128k-online', label: 'llama-3.1-sonar-large-128k-online', tagline: 'Llama 3.1 large, 128k context, online' },
];
constructor(app: App, editor: Editor, perplexityService: PerplexityService, promptsService: PromptsService) {
super(app, editor, perplexityService, promptsService);
}
onOpen() {
const {contentEl} = this;
contentEl.addClass('article-generator-modal');
contentEl.createEl('h2', {text: 'Generate One-Page Article'});
const form = contentEl.createEl('form');
// Term input
const termDiv = form.createDiv({cls: 'setting-item'});
termDiv.createEl('label', {text: 'Vocabulary Term'});
this.termInput = termDiv.createEl('input', {
cls: 'text-input',
attr: {
placeholder: this.promptsService.getArticleTermPlaceholder()
}
});
// Set default value to the current file name (without extension)
const DEFAULT_MODEL = 'sonar-deep-research';
const RECENCY_OPTIONS: Array<{ value: string; label: string }> = [
{ value: '', label: 'No filter — search all content' },
{ value: 'day', label: 'Past day' },
{ value: 'week', label: 'Past week' },
{ value: 'month', label: 'Past month' },
{ value: 'year', label: 'Past year' },
{ value: '2years', label: 'Past 2+ years (falls back to "year")' },
{ value: '3years', label: 'Past 3+ years (falls back to "year")' },
{ value: '5years', label: 'Past 5+ years (falls back to "year")' },
];
export class ArticleGeneratorModal extends Modal {
private editor: Editor;
private perplexityService: PerplexityService;
private promptsService: PromptsService;
private term = '';
private model: string = DEFAULT_MODEL;
private recencyFilter = '';
private citations = true;
private images = true;
private relatedQuestions = false;
private stream = true;
// Live-updated DOM bits
private modelDescEl: HTMLElement | null = null;
private compatibilityWarningEl: HTMLElement | null = null;
private loadingInterval: number | null = null;
constructor(
app: App,
editor: Editor,
perplexityService: PerplexityService,
promptsService: PromptsService
) {
super(app);
this.editor = editor;
this.perplexityService = perplexityService;
this.promptsService = promptsService;
// Default the term to the active file's basename (preserves prior UX)
const currentFile = this.app.workspace.getActiveFile();
if (currentFile) {
const fileName = currentFile.basename; // This gets the filename without extension
this.termInput.value = fileName;
this.term = currentFile.basename;
}
// Add description for term input below the input
const termDesc = termDiv.createDiv({cls: 'setting-item-description term-description'});
termDesc.textContent = this.promptsService.getArticleTermDescription();
// Call parent onOpen to add the rest of the form elements
super.onOpen();
// Override the query input to be hidden since we'll generate it from the term
if (this.queryInput) {
this.queryInput.addClass('hidden-input');
const queryLabel = this.queryInput.previousElementSibling as HTMLElement;
if (queryLabel) {
queryLabel.addClass('hidden-input');
}
}
// Set default model to sonar-deep-research for article generation
if (this.modelSelect) {
this.modelSelect.value = 'sonar-deep-research';
// Trigger the onchange event to update the description
this.modelSelect.dispatchEvent(new Event('change'));
}
// Ensure streaming is enabled for article generation
if (this.streamToggle) {
this.streamToggle.checked = true;
// Add a description to explain why streaming is recommended for articles
const streamDiv = this.streamToggle.closest('.setting-item');
if (streamDiv) {
const streamDesc = streamDiv.createDiv({cls: 'setting-item-description'});
streamDesc.textContent = 'Streaming is recommended for article generation to see content as it\'s being created. Note: Deep Research with streaming may not support images.';
}
}
// Add warning if images and Deep Research are enabled together
if (this.imagesToggle && this.modelSelect) {
const checkCompatibility = () => {
if (this.imagesToggle.checked && this.modelSelect.value === 'sonar-deep-research') {
// Show a notice about the limitation
const imagesDiv = this.imagesToggle.closest('.setting-item');
if (imagesDiv) {
// Remove any existing warning
const existingWarning = imagesDiv.querySelector('.compatibility-warning');
if (existingWarning) {
existingWarning.remove();
}
// Add new warning
const warning = imagesDiv.createDiv({cls: 'setting-item-description compatibility-warning'});
warning.style.color = '#ff6b6b';
warning.style.fontWeight = 'bold';
warning.textContent = '⚠️ Warning: Images are very unstable in Deep Research mode. Consider using a different model for reliable image support.';
}
} else {
// Remove warning if conditions are not met
const imagesDiv = this.imagesToggle.closest('.setting-item');
if (imagesDiv) {
const existingWarning = imagesDiv.querySelector('.compatibility-warning');
if (existingWarning) {
existingWarning.remove();
}
}
}
};
// Check on initial load
checkCompatibility();
// Add event listeners to check when settings change
this.imagesToggle.addEventListener('change', checkCompatibility);
this.modelSelect.addEventListener('change', checkCompatibility);
}
// Update the submit button text
const submitButton = contentEl.querySelector('button.mod-cta') as HTMLButtonElement;
if (submitButton) {
submitButton.textContent = 'Generate Article';
}
// Focus on the term input instead of query input
setTimeout(() => this.termInput.focus(), 100);
}
onOpen(): void {
const { contentEl, modalEl } = this;
modalEl.addClass('article-generator-modal');
contentEl.empty();
// ----- Header -----
const header = contentEl.createDiv({ cls: 'article-generator-modal__header' });
header.createEl('h2', { text: 'Generate One-Page Article', cls: 'article-generator-modal__title' });
header.createEl('p', {
cls: 'article-generator-modal__subtitle',
text: 'Generates a structured article from a single vocabulary term. Streams into the active note.',
});
// ----- Term -----
const termSection = contentEl.createDiv({ cls: 'article-generator-modal__section' });
termSection.createEl('label', {
text: 'Vocabulary Term',
cls: 'article-generator-modal__label',
attr: { for: 'article-generator-modal-term' },
});
const termInput = termSection.createEl('input', {
cls: 'article-generator-modal__input',
attr: {
id: 'article-generator-modal-term',
type: 'text',
placeholder: this.promptsService.getArticleTermPlaceholder(),
},
});
termInput.value = this.term;
termInput.addEventListener('input', () => {
this.term = termInput.value;
});
termInput.addEventListener('keydown', (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
void this.onSubmit();
}
});
termSection.createEl('p', {
cls: 'article-generator-modal__hint',
text: this.promptsService.getArticleTermDescription(),
});
// ----- Model -----
const modelSection = contentEl.createDiv({ cls: 'article-generator-modal__section' });
modelSection.createEl('h3', { text: 'Model', cls: 'article-generator-modal__section-title' });
new Setting(modelSection)
.setName('Model')
.setDesc(this.modelTagline(this.model))
.addDropdown(dd => {
PERPLEXITY_MODELS.forEach(({ value, label }) => dd.addOption(value, label));
dd.setValue(this.model);
dd.onChange((value) => {
this.model = value;
this.applyModelChange(value);
this.updateCompatibilityWarning();
});
});
this.modelDescEl = modelSection.querySelector('.setting-item-description');
// Apply initial deep-research description if default is sonar-deep-research
this.applyModelChange(this.model);
new Setting(modelSection)
.setName('Recency Filter')
.setDesc('Restrict search to recent content. Multi-year options fall back to "year" (the API ceiling).')
.addDropdown(dd => {
RECENCY_OPTIONS.forEach(({ value, label }) => dd.addOption(value, label));
dd.setValue(this.recencyFilter);
dd.onChange((value) => {
this.recencyFilter = value;
});
});
// ----- Returns -----
const returnsSection = contentEl.createDiv({ cls: 'article-generator-modal__section' });
returnsSection.createEl('h3', { text: 'Include in response', cls: 'article-generator-modal__section-title' });
new Setting(returnsSection)
.setName('Citations')
.setDesc('Append a Citations section with source links — recommended for research articles.')
.addToggle(t => t
.setValue(this.citations)
.onChange(v => { this.citations = v; }));
const imagesSetting = new Setting(returnsSection)
.setName('Images')
.setDesc(this.promptsService.getImagesToggleDescription())
.addToggle(t => t
.setValue(this.images)
.onChange(v => {
this.images = v;
this.updateCompatibilityWarning();
}));
// Slot for the live compatibility warning under the Images row
this.compatibilityWarningEl = imagesSetting.settingEl.createDiv({
cls: 'article-generator-modal__warning',
});
this.updateCompatibilityWarning();
new Setting(returnsSection)
.setName('Related Questions')
.setDesc('Surface follow-up questions Perplexity suggests at the end of the response.')
.addToggle(t => t
.setValue(this.relatedQuestions)
.onChange(v => { this.relatedQuestions = v; }));
// ----- Behavior -----
const behaviorSection = contentEl.createDiv({ cls: 'article-generator-modal__section' });
behaviorSection.createEl('h3', { text: 'Behavior', cls: 'article-generator-modal__section-title' });
new Setting(behaviorSection)
.setName('Stream Response')
.setDesc('Recommended for articles — see content as it generates. Note: Deep Research with streaming may not support images.')
.addToggle(t => t
.setValue(this.stream)
.onChange(v => { this.stream = v; }));
// ----- Footer -----
const footer = contentEl.createDiv({ cls: 'article-generator-modal__footer' });
const cancelBtn = footer.createEl('button', {
text: 'Cancel',
cls: 'article-generator-modal__button',
});
cancelBtn.addEventListener('click', () => this.close());
const generateBtn = footer.createEl('button', {
text: 'Generate Article',
cls: 'article-generator-modal__button mod-cta',
});
generateBtn.addEventListener('click', () => void this.onSubmit());
setTimeout(() => termInput.focus(), 50);
}
private modelTagline(value: string): string {
const found = PERPLEXITY_MODELS.find(m => m.value === value);
return found?.tagline ?? '';
}
private applyModelChange(value: string): void {
if (!this.modelDescEl) return;
if (value === 'sonar-deep-research') {
const baseTagline = this.modelTagline(value);
const deepDesc = this.promptsService.getDeepResearchDescription();
this.modelDescEl.textContent = deepDesc
? `${baseTagline}${deepDesc}`
: baseTagline;
} else {
this.modelDescEl.textContent = this.modelTagline(value);
}
}
/**
* Show a warning when both Images and sonar-deep-research are on they don't
* combine reliably (preserves the prior modal's compatibility check).
*/
private updateCompatibilityWarning(): void {
if (!this.compatibilityWarningEl) return;
const incompatible = this.images && this.model === 'sonar-deep-research';
if (incompatible) {
this.compatibilityWarningEl.textContent =
'⚠ Images are unstable in Deep Research mode. Consider a different model for reliable image support.';
this.compatibilityWarningEl.addClass('is-active');
} else {
this.compatibilityWarningEl.textContent = '';
this.compatibilityWarningEl.removeClass('is-active');
}
}
/**
* Insert an animated "🔍 Deep Research Loading..." line at the cursor and
* keep it ticking until PerplexityService clears it. Preserves the prior UX.
*/
private async showDeepResearchLoading(): Promise<void> {
console.log('🎬 Initializing Deep Research loading animation...');
const loadingText = "🔍 Deep Research Loading...";
const loadingText = '🔍 Deep Research Loading...';
const cursor = this.editor.getCursor();
console.log('📍 Inserting loading text at cursor position:', cursor);
// Insert loading text at cursor position
this.editor.replaceRange(loadingText, cursor);
// Animate the loading text with dots
let dots = 0;
const maxDots = 3;
let animationCount = 0;
console.log('🔄 Starting loading animation interval...');
const animationInterval = setInterval(() => {
animationCount++;
this.loadingInterval = window.setInterval(() => {
dots = (dots + 1) % (maxDots + 1);
const animatedText = "🔍 Deep Research Loading" + ".".repeat(dots);
console.log(`🎬 Animation frame ${animationCount}: ${animatedText}`);
// Update the loading text
const animatedText = '🔍 Deep Research Loading' + '.'.repeat(dots);
const currentPos = this.editor.getCursor();
const loadingLine = currentPos.line;
const lineContent = this.editor.getLine(loadingLine);
if (lineContent.includes("🔍 Deep Research Loading")) {
const startCh = lineContent.indexOf("🔍 Deep Research Loading");
if (lineContent.includes('🔍 Deep Research Loading')) {
const startCh = lineContent.indexOf('🔍 Deep Research Loading');
const endCh = lineContent.length;
this.editor.replaceRange(
animatedText,
@ -153,86 +256,65 @@ export class ArticleGeneratorModal extends PerplexityModal {
{ line: loadingLine, ch: endCh }
);
}
}, 500); // Update every 500ms
// Store the interval ID so we can clear it when streaming starts
(this as any).loadingInterval = animationInterval;
console.log('⏳ Setting up loading promise...');
// Return a promise that resolves when streaming starts
}, 500);
// Resolve quickly so the API request can start; PerplexityService clears
// the text + interval when the first content chunk arrives.
return new Promise((resolve) => {
console.log('✅ Loading promise setup complete, resolving immediately');
// Store the resolve function so we can call it when streaming starts
(this as any).loadingResolve = resolve;
// For now, resolve immediately to avoid blocking the request
// We'll let the PerplexityService handle clearing the loading text
setTimeout(() => {
console.log('🚀 Resolving loading promise to continue with API request');
resolve();
}, 100); // Small delay to show the loading text briefly
setTimeout(() => resolve(), 100);
});
}
async onSubmit() {
const term = this.termInput.value.trim();
if (!term) {
async onSubmit(): Promise<void> {
const trimmedTerm = this.term.trim();
if (!trimmedTerm) {
new Notice(this.promptsService.getEnterTermNotice());
return;
}
// Generate the query using the appropriate prompt template based on the selected model
const isDeepResearch = this.modelSelect.value === 'sonar-deep-research';
let query = isDeepResearch
? this.promptsService.getDeepResearchArticleTemplate(term)
: this.promptsService.getArticleGeneratorTemplate(term);
const isDeepResearch = this.model === 'sonar-deep-research';
let query = isDeepResearch
? this.promptsService.getDeepResearchArticleTemplate(trimmedTerm)
: this.promptsService.getArticleGeneratorTemplate(trimmedTerm);
// If images are enabled, add the configurable image references prompt
if (this.imagesToggle.checked) {
if (this.images) {
query = `${query}\n\n${this.promptsService.getImageReferencesPrompt()}`;
}
const options: PerplexityOptions = {
return_citations: this.citationsToggle.checked,
return_images: this.imagesToggle.checked,
return_related_questions: this.relatedQuestionsToggle.checked,
search_recency_filter: this.recencyFilterSelect.value
return_citations: this.citations,
return_images: this.images,
return_related_questions: this.relatedQuestions,
search_recency_filter: this.recencyFilter,
};
this.close();
// Show loading animation for Deep Research
if (this.modelSelect.value === 'sonar-deep-research') {
console.log('🚀 Starting Deep Research loading animation...');
if (isDeepResearch) {
await this.showDeepResearchLoading();
console.log('✅ Deep Research loading animation completed');
}
console.log('🚀 Making Perplexity API request...');
console.log('📊 Request details:', {
model: this.modelSelect.value,
stream: this.streamToggle.checked,
options: options,
queryLength: query.length
});
try {
await this.perplexityService.queryPerplexity(
query,
this.modelSelect.value,
this.streamToggle.checked,
this.editor,
query,
this.model,
this.stream,
this.editor,
options
);
console.log('✅ Perplexity API request completed');
} finally {
// Clear the loading animation interval if it exists
if ((this as any).loadingInterval) {
console.log('🛑 Clearing loading animation interval from modal');
clearInterval((this as any).loadingInterval);
(this as any).loadingInterval = null;
if (this.loadingInterval !== null) {
window.clearInterval(this.loadingInterval);
this.loadingInterval = null;
}
}
}
}
onClose(): void {
if (this.loadingInterval !== null) {
window.clearInterval(this.loadingInterval);
this.loadingInterval = null;
}
this.contentEl.empty();
}
}

View file

@ -116,7 +116,7 @@ export class ClaudeModal extends Modal {
new Setting(togglesSection)
.setName('Enable Web Search')
.setDesc('Server-side web_search_20260209 tool. Returns citations automatically.')
.setDesc('Server-side web_search_20250305 tool. Returns per-claim citations attached to text blocks.')
.addToggle(t => t
.setValue(this.webSearch)
.onChange(v => { this.webSearch = v; }));

View file

@ -1,171 +1,242 @@
import { App, Modal, Notice, Editor } from 'obsidian';
import { PerplexityService, PerplexityOptions } from '../services/perplexityService';
import { PromptsService } from '../services/promptsService';
import { App, Modal, Notice, Setting } from 'obsidian';
import type { Editor } from 'obsidian';
import type { PerplexityService, PerplexityOptions } from '../services/perplexityService';
import type { PromptsService } from '../services/promptsService';
const PERPLEXITY_MODELS: Array<{ value: string; label: string; tagline?: string }> = [
{ value: 'sonar-pro', label: 'sonar-pro', tagline: 'Default — fast web-grounded answers' },
{ value: 'sonar-small', label: 'sonar-small', tagline: 'Cheaper, faster, less detail' },
{ value: 'sonar-deep-research', label: 'sonar-deep-research', tagline: 'Exhaustive multi-source research; takes 3060 seconds' },
{ value: 'llama-3.1-sonar-small-128k-online', label: 'llama-3.1-sonar-small-128k-online', tagline: 'Llama 3.1 small, 128k context, online' },
{ value: 'llama-3.1-sonar-large-128k-online', label: 'llama-3.1-sonar-large-128k-online', tagline: 'Llama 3.1 large, 128k context, online' },
];
const DEFAULT_MODEL = 'sonar-pro';
const RECENCY_OPTIONS: Array<{ value: string; label: string }> = [
{ value: '', label: 'No filter — search all content' },
{ value: 'day', label: 'Past day' },
{ value: 'week', label: 'Past week' },
{ value: 'month', label: 'Past month' },
{ value: 'year', label: 'Past year' },
{ value: '2years', label: 'Past 2+ years (falls back to "year")' },
{ value: '3years', label: 'Past 3+ years (falls back to "year")' },
{ value: '5years', label: 'Past 5+ years (falls back to "year")' },
];
export class PerplexityModal extends Modal {
protected editor: Editor;
protected perplexityService: PerplexityService;
protected promptsService: PromptsService;
protected queryInput!: HTMLTextAreaElement;
protected modelSelect!: HTMLSelectElement;
protected streamToggle!: HTMLInputElement;
protected citationsToggle!: HTMLInputElement;
protected imagesToggle!: HTMLInputElement;
protected relatedQuestionsToggle!: HTMLInputElement;
protected recencyFilterSelect!: HTMLSelectElement;
private editor: Editor;
private perplexityService: PerplexityService;
private promptsService: PromptsService;
constructor(app: App, editor: Editor, perplexityService: PerplexityService, promptsService: PromptsService) {
private query = '';
private model: string = DEFAULT_MODEL;
private recencyFilter = '';
private citations = true;
private images = true;
private relatedQuestions = false;
private stream = true;
// Live-updating description below the model dropdown
private modelDescEl: HTMLElement | null = null;
constructor(
app: App,
editor: Editor,
perplexityService: PerplexityService,
promptsService: PromptsService
) {
super(app);
this.editor = editor;
this.perplexityService = perplexityService;
this.promptsService = promptsService;
}
onOpen() {
const {contentEl} = this;
contentEl.addClass('perplexity-modal');
contentEl.createEl('h2', {text: 'Ask Perplexity'});
const form = contentEl.createEl('form');
// Query input
const queryDiv = form.createDiv({cls: 'setting-item'});
queryDiv.createEl('label', {text: 'Your Question'});
this.queryInput = queryDiv.createEl('textarea', {
cls: 'text-input',
onOpen(): void {
const { contentEl, modalEl } = this;
// Attach to modalEl (outer .modal element) so the width rule actually widens the popup.
// Setting width on contentEl (the inner .modal-content) only sizes the inner area —
// see context-v/issue-resolutions/Widen-Modals-in-Obsidian-using-CSS.md
modalEl.addClass('perplexity-modal');
contentEl.empty();
// ----- Header -----
const header = contentEl.createDiv({ cls: 'perplexity-modal__header' });
header.createEl('h2', { text: 'Ask Perplexity', cls: 'perplexity-modal__title' });
header.createEl('p', {
cls: 'perplexity-modal__subtitle',
text: 'Web-grounded research with native citations. Streams into the active note at the cursor.',
});
// ----- Question -----
const querySection = contentEl.createDiv({ cls: 'perplexity-modal__section' });
querySection.createEl('label', {
text: 'Question',
cls: 'perplexity-modal__label',
attr: { for: 'perplexity-modal-query' },
});
const queryTextarea = querySection.createEl('textarea', {
cls: 'perplexity-modal__textarea',
attr: {
rows: '4',
placeholder: this.promptsService.getPerplexityQueryPlaceholder()
id: 'perplexity-modal-query',
rows: '6',
placeholder: this.promptsService.getPerplexityQueryPlaceholder(),
},
});
queryTextarea.value = this.query;
queryTextarea.addEventListener('input', () => {
this.query = queryTextarea.value;
});
// Cmd/Ctrl+Enter submits
queryTextarea.addEventListener('keydown', (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
void this.onSubmit();
}
});
// Model selection
const modelDiv = form.createDiv({cls: 'setting-item'});
modelDiv.createEl('label', {text: 'Model'});
this.modelSelect = modelDiv.createEl('select', {cls: 'dropdown'});
['sonar-deep-research', 'sonar-pro', 'sonar-small', 'llama-3.1-sonar-small-128k-online', 'llama-3.1-sonar-large-128k-online'].forEach(model => {
const option = this.modelSelect.createEl('option', {value: model, text: model});
if (model === 'sonar-pro') option.selected = true;
// ----- Model -----
const modelSection = contentEl.createDiv({ cls: 'perplexity-modal__section' });
modelSection.createEl('h3', { text: 'Model', cls: 'perplexity-modal__section-title' });
new Setting(modelSection)
.setName('Model')
.setDesc(this.modelTagline(this.model))
.addDropdown(dd => {
PERPLEXITY_MODELS.forEach(({ value, label }) => dd.addOption(value, label));
dd.setValue(this.model);
dd.onChange((value) => {
this.model = value;
this.applyModelChange(value);
});
});
// Stash the description element so we can live-update it on model changes
this.modelDescEl = modelSection.querySelector('.setting-item-description');
new Setting(modelSection)
.setName('Recency Filter')
.setDesc('Restrict search to recent content. Multi-year options fall back to "year" (the API ceiling).')
.addDropdown(dd => {
RECENCY_OPTIONS.forEach(({ value, label }) => dd.addOption(value, label));
dd.setValue(this.recencyFilter);
dd.onChange((value) => {
this.recencyFilter = value;
});
});
// ----- Returns -----
const returnsSection = contentEl.createDiv({ cls: 'perplexity-modal__section' });
returnsSection.createEl('h3', { text: 'Include in response', cls: 'perplexity-modal__section-title' });
new Setting(returnsSection)
.setName('Citations')
.setDesc('Append a Citations section with source links — recommended for research notes.')
.addToggle(t => t
.setValue(this.citations)
.onChange(v => { this.citations = v; }));
new Setting(returnsSection)
.setName('Images')
.setDesc(this.promptsService.getImagesToggleDescription())
.addToggle(t => t
.setValue(this.images)
.onChange(v => { this.images = v; }));
new Setting(returnsSection)
.setName('Related Questions')
.setDesc('Surface follow-up questions Perplexity suggests at the end of the response.')
.addToggle(t => t
.setValue(this.relatedQuestions)
.onChange(v => { this.relatedQuestions = v; }));
// ----- Behavior -----
const behaviorSection = contentEl.createDiv({ cls: 'perplexity-modal__section' });
behaviorSection.createEl('h3', { text: 'Behavior', cls: 'perplexity-modal__section-title' });
new Setting(behaviorSection)
.setName('Stream Response')
.setDesc('Recommended for long answers. Deep Research model can take 3060s — streaming makes progress visible.')
.addToggle(t => t
.setValue(this.stream)
.onChange(v => { this.stream = v; }));
// ----- Footer -----
const footer = contentEl.createDiv({ cls: 'perplexity-modal__footer' });
const cancelBtn = footer.createEl('button', {
text: 'Cancel',
cls: 'perplexity-modal__button',
});
// Add description for deep research model
const modelDesc = modelDiv.createDiv({cls: 'setting-item-description'});
this.modelSelect.onchange = () => {
if (this.modelSelect.value === 'sonar-deep-research') {
modelDesc.textContent = this.promptsService.getDeepResearchDescription();
// Enable streaming for deep research but keep it unchecked by default due to longer processing time
this.streamToggle.disabled = false;
if (!this.streamToggle.checked) {
this.streamToggle.checked = false; // Keep unchecked by default for deep research
}
} else {
modelDesc.textContent = '';
this.streamToggle.disabled = false;
}
};
cancelBtn.addEventListener('click', () => this.close());
// Citations toggle
const citationsDiv = form.createDiv({cls: 'setting-item'});
const citationsLabel = citationsDiv.createEl('label');
this.citationsToggle = citationsLabel.createEl('input', {type: 'checkbox'});
this.citationsToggle.checked = true;
citationsLabel.createSpan({text: ' Include Citations'});
// Images toggle
const imagesDiv = form.createDiv({cls: 'setting-item'});
const imagesLabel = imagesDiv.createEl('label');
this.imagesToggle = imagesLabel.createEl('input', {type: 'checkbox'});
this.imagesToggle.checked = true;
imagesLabel.createSpan({text: ' Include Images'});
// Add description for images toggle
const imagesDesc = imagesDiv.createDiv({cls: 'setting-item-description images-description'});
imagesDesc.textContent = this.promptsService.getImagesToggleDescription();
// Related questions toggle
const relatedQuestionsDiv = form.createDiv({cls: 'setting-item'});
const relatedQuestionsLabel = relatedQuestionsDiv.createEl('label');
this.relatedQuestionsToggle = relatedQuestionsLabel.createEl('input', {type: 'checkbox'});
this.relatedQuestionsToggle.checked = false;
relatedQuestionsLabel.createSpan({text: ' Include Related Questions'});
// Recency filter selection
const recencyDiv = form.createDiv({cls: 'setting-item'});
recencyDiv.createEl('label', {text: 'Recency Filter'});
this.recencyFilterSelect = recencyDiv.createEl('select', {cls: 'dropdown'});
[
{value: '', text: 'No filter (all time) - Search all content'},
{value: 'day', text: 'Past day'},
{value: 'week', text: 'Past week'},
{value: 'month', text: 'Past month'},
{value: 'year', text: 'Past year'},
{value: '2years', text: 'Past 2+ years (falls back to "year")'},
{value: '3years', text: 'Past 3+ years (falls back to "year")'},
{value: '5years', text: 'Past 5+ years (falls back to "year")'}
].forEach(option => {
const optionEl = this.recencyFilterSelect.createEl('option', {value: option.value, text: option.text});
if (option.value === '') optionEl.selected = true; // Default to no filter
});
// Stream toggle
const streamDiv = form.createDiv({cls: 'setting-item'});
const streamLabel = streamDiv.createEl('label');
this.streamToggle = streamLabel.createEl('input', {type: 'checkbox'});
this.streamToggle.checked = true;
streamLabel.createSpan({text: ' Stream response'});
const buttonDiv = contentEl.createDiv({cls: 'setting-item'});
const askButton = buttonDiv.createEl('button', {
const askBtn = footer.createEl('button', {
text: 'Ask Perplexity',
cls: 'mod-cta'
cls: 'perplexity-modal__button mod-cta',
});
form.onsubmit = (e) => {
e.preventDefault();
this.onSubmit();
};
askButton.onclick = () => this.onSubmit();
// Focus on the query input
setTimeout(() => this.queryInput.focus(), 100);
askBtn.addEventListener('click', () => void this.onSubmit());
// Focus the question after the DOM has settled
setTimeout(() => queryTextarea.focus(), 50);
}
async onSubmit() {
const query = this.queryInput.value.trim();
if (!query) {
private modelTagline(value: string): string {
const found = PERPLEXITY_MODELS.find(m => m.value === value);
return found?.tagline ?? '';
}
/**
* When the model changes:
* - Update the description below the dropdown
* - For sonar-deep-research, append the long-form description from promptsService
* (preserving the previous modal's behavior of showing extra deep-research guidance)
*/
private applyModelChange(value: string): void {
if (!this.modelDescEl) return;
if (value === 'sonar-deep-research') {
const baseTagline = this.modelTagline(value);
const deepDesc = this.promptsService.getDeepResearchDescription();
this.modelDescEl.textContent = deepDesc
? `${baseTagline}${deepDesc}`
: baseTagline;
} else {
this.modelDescEl.textContent = this.modelTagline(value);
}
}
private async onSubmit(): Promise<void> {
const trimmed = this.query.trim();
if (!trimmed) {
new Notice(this.promptsService.getEnterQuestionNotice());
return;
}
// If images are enabled, add image markers to the query
let processedQuery = query;
if (this.imagesToggle.checked) {
processedQuery = `${query}
${this.promptsService.getImageReferencesPrompt()}`;
// If images are enabled, append the image-references prompt so Perplexity is
// instructed to embed image markers throughout the response (preserves the
// previous modal's submit-time behavior).
let processedQuery = trimmed;
if (this.images) {
processedQuery = `${trimmed}\n\n${this.promptsService.getImageReferencesPrompt()}`;
}
const options: PerplexityOptions = {
return_citations: this.citationsToggle.checked,
return_images: this.imagesToggle.checked,
return_related_questions: this.relatedQuestionsToggle.checked,
search_recency_filter: this.recencyFilterSelect.value
return_citations: this.citations,
return_images: this.images,
return_related_questions: this.relatedQuestions,
search_recency_filter: this.recencyFilter,
};
this.close();
await this.perplexityService.queryPerplexity(
processedQuery,
this.modelSelect.value,
this.streamToggle.checked,
this.editor,
processedQuery,
this.model,
this.stream,
this.editor,
options
);
}
onClose() {
const {contentEl} = this;
contentEl.empty();
onClose(): void {
this.contentEl.empty();
}
}
}

View file

@ -78,7 +78,15 @@ export class ClaudeService {
const tools: Anthropic.Messages.ToolUnion[] = [];
if (options?.enableWebSearch !== false) {
tools.push({ type: 'web_search_20260209', name: 'web_search' });
// Use web_search_20250305 (older tool, no dynamic filtering) instead of
// web_search_20260209. The newer tool's dynamic-filtering pass post-processes
// search results in a code-execution sandbox, and per-claim
// web_search_result_location citations don't survive that round-trip — text
// blocks come back with citations: null. The 20250305 tool reliably attaches
// per-claim citations to text blocks, which is what gives us inline [N] markers
// in the rendered prose. Trade-off: more tokens consumed (no result filtering),
// but traceable per-claim citations are the whole point of this plugin.
tools.push({ type: 'web_search_20250305', name: 'web_search' });
}
const requestParams: Anthropic.Messages.MessageStreamParams = {

View file

@ -1,43 +1,198 @@
/* ArticleGeneratorModal Styles */
/* ============================================================
Article Generator Modal vocabulary-term one-page article
Same design lineage as Claude / Perplexity modals.
See: context-v/issue-resolutions/Widen-Modals-in-Obsidian-using-CSS.md
============================================================ */
.article-generator-modal .text-input {
width: 100%;
margin: 8px 0;
padding: 12px;
.article-generator-modal {
width: 90vw;
max-width: 640px;
}
.article-generator-modal .setting-item-description {
font-size: 12px;
.article-generator-modal .modal-content {
padding: 0;
}
/* ----- Header ----- */
.article-generator-modal__header {
padding: 24px 28px 16px;
border-bottom: 1px solid var(--background-modifier-border);
}
.article-generator-modal__title {
margin: 0 0 6px;
font-size: 1.5em;
font-weight: 600;
color: var(--text-normal);
letter-spacing: -0.01em;
}
.article-generator-modal__subtitle {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: var(--text-muted);
margin-top: 5px;
}
.article-generator-modal .term-description {
display: block !important;
margin-top: 8px;
margin-bottom: 10px;
width: 100% !important;
flex-basis: 100% !important;
order: 2;
/* ----- Sections ----- */
.article-generator-modal__section {
padding: 18px 28px 4px;
}
.article-generator-modal .setting-item {
flex-direction: column !important;
align-items: flex-start !important;
.article-generator-modal__section + .article-generator-modal__section {
border-top: 1px solid var(--background-modifier-border-hover);
margin-top: 4px;
}
.article-generator-modal .setting-item > * {
width: 100% !important;
.article-generator-modal__section-title {
margin: 0 0 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.article-generator-modal .hidden-input {
.article-generator-modal__section .setting-item {
padding: 10px 0;
border-top: none;
}
.article-generator-modal__section .setting-item + .setting-item {
border-top: 1px solid var(--background-modifier-border-hover);
}
/* ----- Term label + input ----- */
.article-generator-modal__label {
display: block;
margin-bottom: 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.article-generator-modal__input {
width: 100%;
padding: 10px 14px;
font-family: var(--font-text);
font-size: 15px;
line-height: 1.4;
color: var(--text-normal);
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
box-sizing: border-box;
}
.article-generator-modal__input:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 3px var(--interactive-accent-hover);
}
.article-generator-modal__input::placeholder {
color: var(--text-faint);
}
.article-generator-modal__hint {
margin: 8px 0 0;
font-size: 12px;
line-height: 1.45;
color: var(--text-muted);
font-style: italic;
}
/* ----- Compatibility warning under Images toggle ----- */
.article-generator-modal__warning {
margin-top: 6px;
font-size: 12px;
line-height: 1.4;
color: var(--text-error);
font-weight: 500;
display: none;
}
/* Style for streaming toggle description in article generator */
.article-generator-modal .setting-item-description {
font-size: 12px;
color: var(--text-muted);
margin-top: 5px;
font-style: italic;
}
.article-generator-modal__warning.is-active {
display: block;
}
/* ----- Footer / actions ----- */
.article-generator-modal__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 28px 24px;
margin-top: 12px;
border-top: 1px solid var(--background-modifier-border);
background-color: var(--background-secondary);
border-bottom-left-radius: var(--modal-radius, 8px);
border-bottom-right-radius: var(--modal-radius, 8px);
}
.article-generator-modal__button {
padding: 8px 18px;
font-size: 14px;
font-weight: 500;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
background-color: var(--background-primary);
color: var(--text-normal);
cursor: pointer;
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
}
.article-generator-modal__button:hover {
background-color: var(--background-modifier-hover);
}
.article-generator-modal__button:active {
transform: translateY(1px);
}
.article-generator-modal__button.mod-cta {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-color: var(--interactive-accent);
}
.article-generator-modal__button.mod-cta:hover {
background-color: var(--interactive-accent-hover);
border-color: var(--interactive-accent-hover);
}
.article-generator-modal__button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ----- Responsive ----- */
@media (max-width: 600px) {
.article-generator-modal {
width: 95vw;
max-width: none;
}
.article-generator-modal__header,
.article-generator-modal__section,
.article-generator-modal__footer {
padding-left: 18px;
padding-right: 18px;
}
.article-generator-modal__footer {
flex-direction: column-reverse;
}
.article-generator-modal__button {
width: 100%;
}
}

View file

@ -1,18 +1,180 @@
/* PerplexityModal Styles */
/* ============================================================
Perplexity Modal research query composer
Built on Obsidian's native Setting components for theming
parity, with a custom textarea + footer for feel.
See: context-v/issue-resolutions/Widen-Modals-in-Obsidian-using-CSS.md
============================================================ */
.perplexity-modal .text-input {
width: 100%;
margin: 8px 0;
padding: 12px;
.perplexity-modal {
/* Modal container — wider and breathier than default */
width: 90vw;
max-width: 640px;
}
.perplexity-modal .setting-item-description {
font-size: 12px;
.perplexity-modal .modal-content {
padding: 0;
}
/* ----- Header ----- */
.perplexity-modal__header {
padding: 24px 28px 16px;
border-bottom: 1px solid var(--background-modifier-border);
}
.perplexity-modal__title {
margin: 0 0 6px;
font-size: 1.5em;
font-weight: 600;
color: var(--text-normal);
letter-spacing: -0.01em;
}
.perplexity-modal__subtitle {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: var(--text-muted);
margin-top: 5px;
}
.perplexity-modal .setting-item-description.images-description {
/* ----- Sections ----- */
.perplexity-modal__section {
padding: 18px 28px 4px;
}
.perplexity-modal__section + .perplexity-modal__section {
border-top: 1px solid var(--background-modifier-border-hover);
margin-top: 4px;
}
.perplexity-modal__section-title {
margin: 0 0 12px;
font-size: 11px;
margin-top: 3px;
}
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
/* Tighten the native Setting spacing inside our sections */
.perplexity-modal__section .setting-item {
padding: 10px 0;
border-top: none;
}
.perplexity-modal__section .setting-item + .setting-item {
border-top: 1px solid var(--background-modifier-border-hover);
}
/* ----- Question label + textarea ----- */
.perplexity-modal__label {
display: block;
margin-bottom: 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.perplexity-modal__textarea {
width: 100%;
min-height: 120px;
padding: 12px 14px;
font-family: var(--font-text);
font-size: 14px;
line-height: 1.5;
color: var(--text-normal);
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
resize: vertical;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
box-sizing: border-box;
}
.perplexity-modal__textarea:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 3px var(--interactive-accent-hover);
}
.perplexity-modal__textarea::placeholder {
color: var(--text-faint);
}
/* ----- Footer / actions ----- */
.perplexity-modal__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 28px 24px;
margin-top: 12px;
border-top: 1px solid var(--background-modifier-border);
background-color: var(--background-secondary);
border-bottom-left-radius: var(--modal-radius, 8px);
border-bottom-right-radius: var(--modal-radius, 8px);
}
.perplexity-modal__button {
padding: 8px 18px;
font-size: 14px;
font-weight: 500;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
background-color: var(--background-primary);
color: var(--text-normal);
cursor: pointer;
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
}
.perplexity-modal__button:hover {
background-color: var(--background-modifier-hover);
}
.perplexity-modal__button:active {
transform: translateY(1px);
}
.perplexity-modal__button.mod-cta {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-color: var(--interactive-accent);
}
.perplexity-modal__button.mod-cta:hover {
background-color: var(--interactive-accent-hover);
border-color: var(--interactive-accent-hover);
}
.perplexity-modal__button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ----- Responsive ----- */
@media (max-width: 600px) {
.perplexity-modal {
width: 95vw;
max-width: none;
}
.perplexity-modal__header,
.perplexity-modal__section,
.perplexity-modal__footer {
padding-left: 18px;
padding-right: 18px;
}
.perplexity-modal__footer {
flex-direction: column-reverse;
}
.perplexity-modal__button {
width: 100%;
}
}

View file

@ -1,53 +1,301 @@
/* src/styles/perplexity-modal.css */
.perplexity-modal .text-input {
width: 100%;
margin: 8px 0;
padding: 12px;
.perplexity-modal {
width: 90vw;
max-width: 640px;
}
.perplexity-modal .setting-item-description {
font-size: 12px;
.perplexity-modal .modal-content {
padding: 0;
}
.perplexity-modal__header {
padding: 24px 28px 16px;
border-bottom: 1px solid var(--background-modifier-border);
}
.perplexity-modal__title {
margin: 0 0 6px;
font-size: 1.5em;
font-weight: 600;
color: var(--text-normal);
letter-spacing: -0.01em;
}
.perplexity-modal__subtitle {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: var(--text-muted);
margin-top: 5px;
}
.perplexity-modal .setting-item-description.images-description {
.perplexity-modal__section {
padding: 18px 28px 4px;
}
.perplexity-modal__section + .perplexity-modal__section {
border-top: 1px solid var(--background-modifier-border-hover);
margin-top: 4px;
}
.perplexity-modal__section-title {
margin: 0 0 12px;
font-size: 11px;
margin-top: 3px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.perplexity-modal__section .setting-item {
padding: 10px 0;
border-top: none;
}
.perplexity-modal__section .setting-item + .setting-item {
border-top: 1px solid var(--background-modifier-border-hover);
}
.perplexity-modal__label {
display: block;
margin-bottom: 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.perplexity-modal__textarea {
width: 100%;
min-height: 120px;
padding: 12px 14px;
font-family: var(--font-text);
font-size: 14px;
line-height: 1.5;
color: var(--text-normal);
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
resize: vertical;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
box-sizing: border-box;
}
.perplexity-modal__textarea:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 3px var(--interactive-accent-hover);
}
.perplexity-modal__textarea::placeholder {
color: var(--text-faint);
}
.perplexity-modal__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 28px 24px;
margin-top: 12px;
border-top: 1px solid var(--background-modifier-border);
background-color: var(--background-secondary);
border-bottom-left-radius: var(--modal-radius, 8px);
border-bottom-right-radius: var(--modal-radius, 8px);
}
.perplexity-modal__button {
padding: 8px 18px;
font-size: 14px;
font-weight: 500;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
background-color: var(--background-primary);
color: var(--text-normal);
cursor: pointer;
transition:
background-color 0.15s ease,
border-color 0.15s ease,
transform 0.05s ease;
}
.perplexity-modal__button:hover {
background-color: var(--background-modifier-hover);
}
.perplexity-modal__button:active {
transform: translateY(1px);
}
.perplexity-modal__button.mod-cta {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-color: var(--interactive-accent);
}
.perplexity-modal__button.mod-cta:hover {
background-color: var(--interactive-accent-hover);
border-color: var(--interactive-accent-hover);
}
.perplexity-modal__button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 600px) {
.perplexity-modal {
width: 95vw;
max-width: none;
}
.perplexity-modal__header,
.perplexity-modal__section,
.perplexity-modal__footer {
padding-left: 18px;
padding-right: 18px;
}
.perplexity-modal__footer {
flex-direction: column-reverse;
}
.perplexity-modal__button {
width: 100%;
}
}
/* src/styles/article-generator-modal.css */
.article-generator-modal .text-input {
width: 100%;
margin: 8px 0;
padding: 12px;
.article-generator-modal {
width: 90vw;
max-width: 640px;
}
.article-generator-modal .setting-item-description {
font-size: 12px;
.article-generator-modal .modal-content {
padding: 0;
}
.article-generator-modal__header {
padding: 24px 28px 16px;
border-bottom: 1px solid var(--background-modifier-border);
}
.article-generator-modal__title {
margin: 0 0 6px;
font-size: 1.5em;
font-weight: 600;
color: var(--text-normal);
letter-spacing: -0.01em;
}
.article-generator-modal__subtitle {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: var(--text-muted);
margin-top: 5px;
}
.article-generator-modal .term-description {
display: block !important;
margin-top: 8px;
margin-bottom: 10px;
width: 100% !important;
flex-basis: 100% !important;
order: 2;
.article-generator-modal__section {
padding: 18px 28px 4px;
}
.article-generator-modal .setting-item {
flex-direction: column !important;
align-items: flex-start !important;
.article-generator-modal__section + .article-generator-modal__section {
border-top: 1px solid var(--background-modifier-border-hover);
margin-top: 4px;
}
.article-generator-modal .setting-item > * {
width: 100% !important;
.article-generator-modal__section-title {
margin: 0 0 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.article-generator-modal .hidden-input {
.article-generator-modal__section .setting-item {
padding: 10px 0;
border-top: none;
}
.article-generator-modal__section .setting-item + .setting-item {
border-top: 1px solid var(--background-modifier-border-hover);
}
.article-generator-modal__label {
display: block;
margin-bottom: 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.article-generator-modal__input {
width: 100%;
padding: 10px 14px;
font-family: var(--font-text);
font-size: 15px;
line-height: 1.4;
color: var(--text-normal);
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
box-sizing: border-box;
}
.article-generator-modal__input:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 3px var(--interactive-accent-hover);
}
.article-generator-modal__input::placeholder {
color: var(--text-faint);
}
.article-generator-modal__hint {
margin: 8px 0 0;
font-size: 12px;
line-height: 1.45;
color: var(--text-muted);
font-style: italic;
}
.article-generator-modal__warning {
margin-top: 6px;
font-size: 12px;
line-height: 1.4;
color: var(--text-error);
font-weight: 500;
display: none;
}
.article-generator-modal .setting-item-description {
font-size: 12px;
color: var(--text-muted);
margin-top: 5px;
font-style: italic;
.article-generator-modal__warning.is-active {
display: block;
}
.article-generator-modal__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 28px 24px;
margin-top: 12px;
border-top: 1px solid var(--background-modifier-border);
background-color: var(--background-secondary);
border-bottom-left-radius: var(--modal-radius, 8px);
border-bottom-right-radius: var(--modal-radius, 8px);
}
.article-generator-modal__button {
padding: 8px 18px;
font-size: 14px;
font-weight: 500;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
background-color: var(--background-primary);
color: var(--text-normal);
cursor: pointer;
transition:
background-color 0.15s ease,
border-color 0.15s ease,
transform 0.05s ease;
}
.article-generator-modal__button:hover {
background-color: var(--background-modifier-hover);
}
.article-generator-modal__button:active {
transform: translateY(1px);
}
.article-generator-modal__button.mod-cta {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-color: var(--interactive-accent);
}
.article-generator-modal__button.mod-cta:hover {
background-color: var(--interactive-accent-hover);
border-color: var(--interactive-accent-hover);
}
.article-generator-modal__button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 600px) {
.article-generator-modal {
width: 95vw;
max-width: none;
}
.article-generator-modal__header,
.article-generator-modal__section,
.article-generator-modal__footer {
padding-left: 18px;
padding-right: 18px;
}
.article-generator-modal__footer {
flex-direction: column-reverse;
}
.article-generator-modal__button {
width: 100%;
}
}
/* src/styles/perplexica-modal.css */