mirror of
https://github.com/lossless-group/perplexed-plugin.git
synced 2026-07-22 06:49:50 +00:00
improve(settings), new(api): add new LM Studio API, improve settings
On branch development Your branch is up to date with 'origin/development'. Changes to be committed: modified: README.md modified: main.ts modified: styles.css
This commit is contained in:
parent
b01599a617
commit
15bbc0f565
3 changed files with 596 additions and 2 deletions
18
README.md
18
README.md
|
|
@ -26,6 +26,24 @@ ## Settings
|
|||
|
||||
## Getting Started
|
||||
|
||||
If you want to use Perplexica, you will need to host it yourself. You can find the instructions for doing so [here](https://perplexica.io/).
|
||||
|
||||
Troubleshooting the config file can be annoying. You have to duplicate the example.config.toml and put in your own values.
|
||||
|
||||
These were for my mac, if it helps.
|
||||
|
||||
```toml
|
||||
|
||||
[MODELS.OLLAMA]
|
||||
API_URL = "http://host.docker.internal:11434"
|
||||
|
||||
[MODELS.LM_STUDIO]
|
||||
API_URL = "http://192.168.0.23:1234"
|
||||
|
||||
[API_ENDPOINTS]
|
||||
SEARXNG = "http://localhost:4000"
|
||||
```
|
||||
|
||||
```
|
||||
pnpm install
|
||||
pnpm add -D esbuild @types/node builtin-modules
|
||||
|
|
|
|||
438
main.ts
438
main.ts
|
|
@ -12,9 +12,12 @@ interface PerplexedPluginSettings {
|
|||
perplexityApiKey: string;
|
||||
perplexicaEndpoint: string;
|
||||
perplexityEndpoint: string;
|
||||
lmStudioEndpoint: string;
|
||||
lmStudioRequestTemplate: string;
|
||||
defaultModel: string;
|
||||
defaultOptimizationMode: string;
|
||||
defaultFocusMode: string;
|
||||
defaultLMStudioModel: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: PerplexedPluginSettings = {
|
||||
|
|
@ -23,6 +26,7 @@ const DEFAULT_SETTINGS: PerplexedPluginSettings = {
|
|||
localLLMPath: 'http://host.docker.internal:3030/api/search',
|
||||
perplexicaEndpoint: 'http://localhost:3030/api/search',
|
||||
perplexityEndpoint: 'https://api.perplexity.ai/chat/completions',
|
||||
lmStudioEndpoint: 'http://localhost:1234/v1/chat/completions',
|
||||
requestBodyTemplate: `{
|
||||
"chatModel": {
|
||||
"provider": "ollama",
|
||||
|
|
@ -71,10 +75,23 @@ const DEFAULT_SETTINGS: PerplexedPluginSettings = {
|
|||
"stream": false,
|
||||
"presence_penalty": 0,
|
||||
"frequency_penalty": 1
|
||||
}`,
|
||||
lmStudioRequestTemplate: `{
|
||||
"model": "ibm/granite-3.2-8b",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, can you help me with this question?"
|
||||
}
|
||||
],
|
||||
"max_tokens": 2048,
|
||||
"temperature": 0.7,
|
||||
"stream": false
|
||||
}`,
|
||||
defaultModel: 'llama3.2:latest',
|
||||
defaultOptimizationMode: 'speed',
|
||||
defaultFocusMode: 'webSearch'
|
||||
defaultFocusMode: 'webSearch',
|
||||
defaultLMStudioModel: 'ibm/granite-3.2-8b'
|
||||
};
|
||||
|
||||
export default class PerplexedPlugin extends Plugin {
|
||||
|
|
@ -95,6 +112,7 @@ export default class PerplexedPlugin extends Plugin {
|
|||
// Register commands
|
||||
this.registerPerplexicaCommands();
|
||||
this.registerPerplexityCommands();
|
||||
this.registerLMStudioCommands();
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
|
|
@ -846,6 +864,358 @@ export default class PerplexedPlugin extends Plugin {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async queryLMStudio(query: string, model: string, stream: boolean, editor: Editor, options?: {
|
||||
max_tokens?: number;
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
system_prompt?: string;
|
||||
}): Promise<void> {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Insert query header at the current cursor position
|
||||
const cursor = editor.getCursor();
|
||||
console.log('Initial cursor position:', cursor);
|
||||
|
||||
const headerText = `\n\n***\n## LM Studio Query (${timestamp})\n**Question:** ${query}\n**Model:** ${model}\n\n### **Response from ${model}**:\n\n`;
|
||||
|
||||
// Insert the header at the cursor position
|
||||
editor.replaceRange(headerText, cursor, cursor);
|
||||
|
||||
// Calculate where the response content should start
|
||||
const headerLines = headerText.split('\n');
|
||||
const lastLine = headerLines[headerLines.length - 1] || '';
|
||||
const responseCursor = {
|
||||
line: cursor.line + headerLines.length - 1,
|
||||
ch: lastLine.length
|
||||
};
|
||||
|
||||
console.log('Response cursor position:', responseCursor);
|
||||
|
||||
try {
|
||||
const messages: any[] = [];
|
||||
|
||||
// Add system message if provided
|
||||
if (options?.system_prompt) {
|
||||
messages.push({ role: 'system', content: options.system_prompt });
|
||||
}
|
||||
|
||||
// Add user query
|
||||
messages.push({ role: 'user', content: query });
|
||||
|
||||
const payload: any = {
|
||||
model,
|
||||
messages,
|
||||
stream,
|
||||
max_tokens: options?.max_tokens ?? 2048,
|
||||
temperature: options?.temperature ?? 0.7,
|
||||
top_p: options?.top_p ?? 0.9
|
||||
};
|
||||
|
||||
const response = await fetch(this.settings.lmStudioEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
// LM Studio doesn't require API key for local access
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
let finalCursor = responseCursor;
|
||||
|
||||
if (stream) {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error('No response body');
|
||||
|
||||
let buffer = '';
|
||||
let currentPos = responseCursor;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = new TextDecoder().decode(value);
|
||||
buffer += chunk;
|
||||
|
||||
// Process complete lines from buffer
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith('data: ')) {
|
||||
const data = line.replace('data: ', '').trim();
|
||||
if (data === '[DONE]') continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.choices?.[0]?.delta?.content) {
|
||||
const content = parsed.choices[0].delta.content;
|
||||
editor.replaceRange(content, currentPos);
|
||||
// Update cursor position after insertion
|
||||
const lines = content.split('\n');
|
||||
if (lines.length === 1) {
|
||||
currentPos = { line: currentPos.line, ch: currentPos.ch + content.length };
|
||||
} else {
|
||||
currentPos = {
|
||||
line: currentPos.line + lines.length - 1,
|
||||
ch: lines[lines.length - 1].length
|
||||
};
|
||||
}
|
||||
finalCursor = currentPos; // Track final position
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore JSON parse errors for partial chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const data = await response.json();
|
||||
const content = data.choices?.[0]?.message?.content || 'No response received';
|
||||
|
||||
editor.replaceRange(content, responseCursor);
|
||||
// Calculate final cursor position for non-streaming
|
||||
const lines = content.split('\n');
|
||||
if (lines.length === 1) {
|
||||
finalCursor = { line: responseCursor.line, ch: responseCursor.ch + content.length };
|
||||
} else {
|
||||
finalCursor = {
|
||||
line: responseCursor.line + lines.length - 1,
|
||||
ch: lines[lines.length - 1].length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add separator at the final cursor position
|
||||
editor.replaceRange('\n\n***\n', finalCursor);
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
new Notice(`LM Studio Error: ${errorMsg}`);
|
||||
editor.replaceRange(`\n**Error:** ${errorMsg}\n\n***\n`, editor.getCursor());
|
||||
}
|
||||
}
|
||||
|
||||
private registerLMStudioCommands(): void {
|
||||
// Command to update LM Studio URL
|
||||
this.addCommand({
|
||||
id: 'update-lmstudio-url',
|
||||
name: 'Update LM Studio URL',
|
||||
callback: () => {
|
||||
const modal = new (class extends Modal {
|
||||
private urlInput!: HTMLInputElement;
|
||||
|
||||
constructor(app: App, private plugin: PerplexedPlugin) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.createEl('h2', {text: 'Update LM Studio API URL'});
|
||||
const form = contentEl.createEl('form');
|
||||
const div = form.createDiv({cls: 'setting-item'});
|
||||
|
||||
div.createEl('label', {
|
||||
text: 'LM Studio API URL',
|
||||
attr: {for: 'lmstudio-url-input'}
|
||||
});
|
||||
|
||||
this.urlInput = div.createEl('input', {
|
||||
type: 'text',
|
||||
value: this.plugin.settings.lmStudioEndpoint,
|
||||
cls: 'text-input',
|
||||
attr: {id: 'lmstudio-url-input'}
|
||||
});
|
||||
|
||||
const buttonDiv = contentEl.createDiv({cls: 'setting-item'});
|
||||
const saveButton = buttonDiv.createEl('button', {
|
||||
text: 'Save',
|
||||
cls: 'mod-cta'
|
||||
});
|
||||
|
||||
form.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.onSubmit();
|
||||
};
|
||||
|
||||
saveButton.onclick = () => this.onSubmit();
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const newUrl = this.urlInput.value.trim();
|
||||
if (newUrl) {
|
||||
this.plugin.settings.lmStudioEndpoint = newUrl;
|
||||
this.plugin.saveSettings();
|
||||
new Notice(`LM Studio URL updated to: ${newUrl}`);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
})(this.app, this);
|
||||
|
||||
modal.open();
|
||||
}
|
||||
});
|
||||
|
||||
// Command to show current LM Studio settings
|
||||
this.addCommand({
|
||||
id: 'show-lmstudio-settings',
|
||||
name: 'Show LM Studio Settings',
|
||||
callback: () => {
|
||||
new Notice(`Current LM Studio URL: ${this.settings.lmStudioEndpoint}`);
|
||||
console.log('LM Studio Settings:', this.settings);
|
||||
}
|
||||
});
|
||||
|
||||
// Command to ask LM Studio
|
||||
this.addCommand({
|
||||
id: 'ask-lmstudio',
|
||||
name: 'Ask LM Studio',
|
||||
editorCallback: (editor: Editor) => {
|
||||
const modal = new (class extends Modal {
|
||||
private queryInput!: HTMLTextAreaElement;
|
||||
private modelSelect!: HTMLSelectElement;
|
||||
private streamToggle!: HTMLInputElement;
|
||||
private maxTokensInput!: HTMLInputElement;
|
||||
private temperatureInput!: HTMLInputElement;
|
||||
private systemPromptInput!: HTMLTextAreaElement;
|
||||
|
||||
constructor(app: App, private plugin: PerplexedPlugin, private editor: Editor) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.createEl('h2', {text: 'Ask LM Studio'});
|
||||
|
||||
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',
|
||||
attr: {
|
||||
rows: '4',
|
||||
placeholder: 'What would you like to ask?'
|
||||
}
|
||||
});
|
||||
this.queryInput.style.width = '100%';
|
||||
this.queryInput.style.minHeight = '100px';
|
||||
|
||||
// Model selection
|
||||
const modelDiv = form.createDiv({cls: 'setting-item'});
|
||||
modelDiv.createEl('label', {text: 'Model'});
|
||||
this.modelSelect = modelDiv.createEl('select', {cls: 'dropdown'});
|
||||
// Use common LM Studio models - these would be dynamically loaded ideally
|
||||
['ibm/granite-3.2-8b', 'microsoft/phi-4-reasoning-plus', 'google/gemma-3-12b', 'meta-llama/llama-3.2-3b-instruct', 'custom-model'].forEach(model => {
|
||||
const option = this.modelSelect.createEl('option', {value: model, text: model});
|
||||
if (model === this.plugin.settings.defaultLMStudioModel) option.selected = true;
|
||||
});
|
||||
|
||||
// System prompt
|
||||
const systemDiv = form.createDiv({cls: 'setting-item'});
|
||||
systemDiv.createEl('label', {text: 'System Prompt (Optional)'});
|
||||
this.systemPromptInput = systemDiv.createEl('textarea', {
|
||||
cls: 'text-input',
|
||||
attr: {
|
||||
rows: '2',
|
||||
placeholder: 'You are a helpful AI assistant...'
|
||||
}
|
||||
});
|
||||
this.systemPromptInput.style.width = '100%';
|
||||
this.systemPromptInput.style.minHeight = '60px';
|
||||
|
||||
// Max tokens
|
||||
const maxTokensDiv = form.createDiv({cls: 'setting-item'});
|
||||
maxTokensDiv.createEl('label', {text: 'Max Tokens'});
|
||||
this.maxTokensInput = maxTokensDiv.createEl('input', {
|
||||
type: 'number',
|
||||
value: '2048',
|
||||
cls: 'text-input'
|
||||
});
|
||||
|
||||
// Temperature
|
||||
const tempDiv = form.createDiv({cls: 'setting-item'});
|
||||
tempDiv.createEl('label', {text: 'Temperature (0.0 - 2.0)'});
|
||||
this.temperatureInput = tempDiv.createEl('input', {
|
||||
type: 'number',
|
||||
value: '0.7',
|
||||
attr: {
|
||||
step: '0.1',
|
||||
min: '0',
|
||||
max: '2'
|
||||
},
|
||||
cls: 'text-input'
|
||||
});
|
||||
|
||||
// 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', {
|
||||
text: 'Ask LM Studio',
|
||||
cls: 'mod-cta'
|
||||
});
|
||||
|
||||
form.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.onSubmit();
|
||||
};
|
||||
|
||||
askButton.onclick = () => this.onSubmit();
|
||||
|
||||
// Focus on the query input
|
||||
setTimeout(() => this.queryInput.focus(), 100);
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
const query = this.queryInput.value.trim();
|
||||
if (!query) {
|
||||
new Notice('Please enter a question');
|
||||
return;
|
||||
}
|
||||
|
||||
const options: {
|
||||
max_tokens: number;
|
||||
temperature: number;
|
||||
system_prompt?: string;
|
||||
} = {
|
||||
max_tokens: parseInt(this.maxTokensInput.value) || 2048,
|
||||
temperature: parseFloat(this.temperatureInput.value) || 0.7
|
||||
};
|
||||
|
||||
const systemPrompt = this.systemPromptInput.value.trim();
|
||||
if (systemPrompt) {
|
||||
options.system_prompt = systemPrompt;
|
||||
}
|
||||
|
||||
this.close();
|
||||
await this.plugin.queryLMStudio(query, this.modelSelect.value, this.streamToggle.checked, this.editor, options);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
})(this.app, this, editor);
|
||||
|
||||
modal.open();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PerplexedSettingTab extends PluginSettingTab {
|
||||
|
|
@ -1006,5 +1376,71 @@ class PerplexedSettingTab extends PluginSettingTab {
|
|||
// Add the textarea to the setting
|
||||
perplexicaJsonSetting.settingEl.appendChild(perplexicaTextArea);
|
||||
|
||||
// LM Studio Section
|
||||
const lmStudioHeader = containerEl.createEl('h3', { text: 'LM Studio (Local Models)' });
|
||||
lmStudioHeader.style.color = 'var(--text-accent)';
|
||||
containerEl.createEl('p', {
|
||||
text: 'Configure settings for your local LM Studio installation with loaded models',
|
||||
cls: 'setting-item-description'
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Endpoint')
|
||||
.setDesc('API endpoint for your local LM Studio instance')
|
||||
.addText(text => text
|
||||
.setPlaceholder('http://localhost:1234/v1/chat/completions')
|
||||
.setValue(this.plugin.settings.lmStudioEndpoint)
|
||||
.onChange(async (value: string) => {
|
||||
this.plugin.settings.lmStudioEndpoint = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default Model')
|
||||
.setDesc('Default model name for LM Studio to use')
|
||||
.addText(text => text
|
||||
.setPlaceholder('ibm/granite-3.2-8b')
|
||||
.setValue(this.plugin.settings.defaultLMStudioModel)
|
||||
.onChange(async (value: string) => {
|
||||
this.plugin.settings.defaultLMStudioModel = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
// LM Studio Request Template
|
||||
const lmStudioJsonSetting = new Setting(containerEl)
|
||||
.setName('Request Body Template')
|
||||
.setDesc('JSON template for LM Studio API requests');
|
||||
|
||||
// Create a textarea element for LM Studio
|
||||
const lmStudioTextArea = document.createElement('textarea');
|
||||
lmStudioTextArea.rows = 10;
|
||||
lmStudioTextArea.cols = 50;
|
||||
lmStudioTextArea.style.width = '100%';
|
||||
lmStudioTextArea.style.minHeight = '300px';
|
||||
lmStudioTextArea.style.fontFamily = 'monospace';
|
||||
lmStudioTextArea.placeholder = 'Enter LM Studio JSON request template...';
|
||||
|
||||
// Set initial value if it exists
|
||||
if (this.plugin.settings.lmStudioRequestTemplate) {
|
||||
try {
|
||||
const config = JSON.parse(this.plugin.settings.lmStudioRequestTemplate);
|
||||
lmStudioTextArea.value = JSON.stringify(config, null, 2);
|
||||
} catch (e) {
|
||||
// If not valid JSON, use as is
|
||||
lmStudioTextArea.value = this.plugin.settings.lmStudioRequestTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
// Add input event listener for LM Studio
|
||||
lmStudioTextArea.addEventListener('input', async () => {
|
||||
this.plugin.settings.lmStudioRequestTemplate = lmStudioTextArea.value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
// Add the textarea to the setting
|
||||
lmStudioJsonSetting.settingEl.appendChild(lmStudioTextArea);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
142
styles.css
142
styles.css
|
|
@ -1 +1,141 @@
|
|||
.cite-wide-modal{padding:1.5rem;max-width:800px;margin:0 auto}.cite-wide-title{margin-top:0;padding-bottom:.75rem;border-bottom:1px solid var(--background-modifier-border)}.cite-wide-container{margin-top:1rem}.cite-wide-group{margin-bottom:1.5rem;border:1px solid var(--background-modifier-border);border-radius:6px;overflow:hidden}.cite-wide-group-header{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background-color:var(--background-secondary);cursor:pointer;transition:background-color .2s}.cite-wide-group-header:hover{background-color:var(--background-modifier-hover)}.cite-wide-group-header-content{display:flex;align-items:center;gap:.5rem}.cite-wide-group-title{margin:0;font-size:1rem;font-weight:600}.cite-wide-source-link{font-size:.85rem;opacity:.8;text-decoration:none}.cite-wide-group-content{padding:.75rem 1rem;background-color:var(--background-primary);border-top:1px solid var(--background-modifier-border)}.cite-wide-instance{display:flex;justify-content:space-between;align-items:center;padding:.5rem 0;border-bottom:1px solid var(--background-modifier-border)}.cite-wide-instance.cite-wide-reference-source{background-color:var(--background-secondary-alt);margin:0 -1rem;padding:.75rem 1rem;border-radius:4px;border:1px solid var(--background-modifier-border);border-left:3px solid var(--interactive-accent)}.cite-wide-instance:last-child{border-bottom:none}.cite-wide-line-info{flex:1;font-family:var(--font-monospace);font-size:.9em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cite-wide-line-number{color:var(--text-muted);margin-right:.5rem}.cite-wide-line-preview{opacity:.9}.cite-wide-badge{display:inline-block;padding:.15em .5em;font-size:.75em;font-weight:600;line-height:1.2;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:3px;text-transform:uppercase;letter-spacing:.5px}.cite-wide-badge-reference{color:var(--text-normal);background-color:var(--background-modifier-border)}.cite-wide-line-info>.cite-wide-badge+span{margin-left:.5rem}.cite-wide-instance-actions{display:flex;gap:.5rem;margin-left:.75rem}.cite-wide-convert-btn,.cite-wide-view-btn{white-space:nowrap;padding:.25rem .75rem;font-size:.85em;line-height:1.4;border-radius:4px;transition:all .1s ease}.cite-wide-convert-btn{background-color:var(--interactive-accent);color:var(--text-on-accent);border:1px solid var(--interactive-accent-hover)}.cite-wide-convert-btn:hover{background-color:var(--interactive-accent-hover)}.cite-wide-view-btn{background-color:var(--background-secondary);border:1px solid var(--background-modifier-border)}.cite-wide-view-btn:hover{background-color:var(--background-modifier-hover)}.cite-wide-footer{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--background-modifier-border);display:flex;justify-content:flex-end}
|
||||
/* src/styles/citations.css */
|
||||
.cite-wide-modal {
|
||||
padding: 1.5rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.cite-wide-title {
|
||||
margin-top: 0;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.cite-wide-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.cite-wide-group {
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cite-wide-group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--background-secondary);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.cite-wide-group-header:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
.cite-wide-group-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.cite-wide-group-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cite-wide-source-link {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
text-decoration: none;
|
||||
}
|
||||
.cite-wide-group-content {
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--background-primary);
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.cite-wide-instance {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.cite-wide-instance.cite-wide-reference-source {
|
||||
background-color: var(--background-secondary-alt);
|
||||
margin: 0 -1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-left: 3px solid var(--interactive-accent);
|
||||
}
|
||||
.cite-wide-instance:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.cite-wide-line-info {
|
||||
flex: 1;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.cite-wide-line-number {
|
||||
color: var(--text-muted);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.cite-wide-line-preview {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.cite-wide-badge {
|
||||
display: inline-block;
|
||||
padding: 0.15em 0.5em;
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.cite-wide-badge-reference {
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-border);
|
||||
}
|
||||
.cite-wide-line-info > .cite-wide-badge + span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.cite-wide-instance-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
.cite-wide-convert-btn,
|
||||
.cite-wide-view-btn {
|
||||
white-space: nowrap;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.4;
|
||||
border-radius: 4px;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
.cite-wide-convert-btn {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border: 1px solid var(--interactive-accent-hover);
|
||||
}
|
||||
.cite-wide-convert-btn:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
.cite-wide-view-btn {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.cite-wide-view-btn:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
.cite-wide-footer {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue