mirror of
https://github.com/lossless-group/perplexed-plugin.git
synced 2026-07-22 06:49:50 +00:00
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:
parent
b5a70c72a4
commit
d7551d7bc7
10 changed files with 1288 additions and 406 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:135–391 │
|
||||
│ 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.
|
||||
14
context-v/specs/Using-Files-as-Prompt-Outlines.md
Normal file
14
context-v/specs/Using-Files-as-Prompt-Outlines.md
Normal 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.
|
||||
|
|
@ -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 30–60 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }));
|
||||
|
|
|
|||
|
|
@ -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 30–60 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 30–60s — 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
316
styles.css
316
styles.css
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Reference in a new issue