V1.0.0
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# Ollama Sidebar
|
||||
|
||||
A browser sidebar chat extension that connects to a self-hosted Ollama instance. Built for Orion (WebKit) with full Chrome support. No cloud, no accounts, no bundler — loads unpacked directly from disk.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- Ollama running locally or on a remote host
|
||||
- Chrome 114+ or Orion (beta Chrome extension support)
|
||||
- At least one model pulled in Ollama
|
||||
- `OLLAMA_ORIGINS=*` set on the Ollama instance if proxied through Nginx or another reverse proxy
|
||||
|
||||
---
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
| Browser | Status | Notes |
|
||||
|---|---|---|
|
||||
| Orion | ✅ Main supported browser | File picker button does not open a picker — drag and drop files onto the chat instead. Under investigation. |
|
||||
| Chrome | ✅ Fully working | All features including file picker. `sidebar_action` manifest warning is expected and harmless. |
|
||||
| Firefox | 🔜 Not yet supported | Possible future support. |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone or download this repository
|
||||
2. Open the `icons/` folder — if the PNG files are missing, open `generate_icons.html` in a browser and save the three icons into the `icons/` folder
|
||||
3. **Chrome:** go to `chrome://extensions`, enable Developer Mode, click **Load unpacked**, select the project folder
|
||||
4. **Orion:** go to Tools → Extensions → Install from Disk, select the project folder
|
||||
5. Click the extension icon in the toolbar to open the sidebar
|
||||
|
||||
---
|
||||
|
||||
## First-time setup
|
||||
|
||||
1. Click the ⚙ Settings button in the sidebar header
|
||||
2. Set your **Ollama Base URL** (default: `http://localhost:11434`)
|
||||
3. Click **Test Connection** — connected models will appear in the dropdowns
|
||||
4. Set a **Default Chat Model**
|
||||
5. Click **Save Settings**
|
||||
|
||||
If Ollama is behind a reverse proxy with bearer token auth, enter the token in the **Bearer Token** field.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Chat
|
||||
|
||||
- Streaming responses with stop, regenerate, and edit
|
||||
- Thinking/reasoning block support (collapsed by default)
|
||||
- Full markdown rendering with syntax-highlighted code blocks
|
||||
- Token metrics per response (tokens, tok/s, duration)
|
||||
|
||||
### Context
|
||||
|
||||
- Add the current page as context with the 🌐 button — uses a Readability-style algorithm to extract main content, strips navigation and noise
|
||||
- Drag and drop files onto the chat to attach (`.txt`, `.md`, `.csv`, `.pdf`, images)
|
||||
- PDF text extraction via PDF.js
|
||||
- Image support for vision-capable models (llava, qwen-vl, etc.)
|
||||
- Keyword and semantic RAG — relevant chunks are selected before being sent, scaled to your model's context window
|
||||
|
||||
### Sessions
|
||||
|
||||
- Multiple named chat sessions with create, delete, search, and export as Markdown
|
||||
- Auto-generated session names from the first message
|
||||
- Session history persisted in browser storage
|
||||
|
||||
### Personas
|
||||
|
||||
- 8 built-in personas (Assistant, Code Reviewer, Study Buddy, Writing Editor, Rubber Duck, Devil's Advocate, Technical Writer, Be Brief/BLUF)
|
||||
- Create and save custom personas
|
||||
- Each session tracks its active persona's system prompt
|
||||
|
||||
### Prompt Templates
|
||||
|
||||
- 10 built-in templates triggered with `/` in the input
|
||||
- Create and save custom templates
|
||||
|
||||
### Models
|
||||
|
||||
- Per-session model selector
|
||||
- Pull new models from the Settings page without leaving the browser
|
||||
- Multi-model comparison mode — send the same prompt to two models side by side
|
||||
- Semantic RAG via Ollama `/api/embed` endpoint (configure embedding model in Settings)
|
||||
|
||||
### Settings
|
||||
|
||||
- Dark/light theme
|
||||
- Compact message density
|
||||
- Auto-scroll during generation
|
||||
- Response language lock
|
||||
- Model parameters (temperature, top-p, top-k, repeat penalty, seed, context length, max tokens)
|
||||
- Default system prompt
|
||||
- Debug panel with live log, system info, and connection status
|
||||
|
||||
### Keyboard shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|---|---|
|
||||
| `Enter` | Send message |
|
||||
| `Shift+Enter` | New line |
|
||||
| `Ctrl+K` | New chat |
|
||||
| `Ctrl+L` | Clear conversation (press twice to confirm) |
|
||||
| `Ctrl+/` | Open templates |
|
||||
| `Ctrl+,` | Open settings |
|
||||
| `Ctrl+?` | Show all shortcuts |
|
||||
| `Esc` | Close panels and overlays |
|
||||
|
||||
---
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
├── background.js # Service worker — message routing, stream relay
|
||||
├── content.js # Content script — page text extraction
|
||||
├── sidepanel.html # Sidebar UI
|
||||
├── sidepanel.js # Sidebar logic
|
||||
├── sidepanel.css # Sidebar styles
|
||||
├── settings.html # Options page
|
||||
├── settings.js # Options page logic
|
||||
├── settings.css # Options page styles
|
||||
├── manifest.json # Extension manifest (MV3)
|
||||
├── marked.min.js # Markdown rendering
|
||||
├── highlight.min.js # Syntax highlighting
|
||||
├── hljs-github-dark.min.css # Highlight.js theme
|
||||
├── pdf.min.js # PDF.js v3 — PDF text extraction
|
||||
├── pdf.worker.min.js # PDF.js worker
|
||||
├── pdf-init.js # PDF.js worker initialisation
|
||||
└── icons/
|
||||
├── icon16.png
|
||||
├── icon48.png
|
||||
└── icon128.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **File upload:** Chrome supports the native file picker via the attach button. Orion does not currently open the file picker — drag and drop files directly onto the chat area instead. This is under investigation.
|
||||
- **Semantic RAG:** Requires an embedding model pulled in Ollama (recommended: `nomic-embed-text-v2-moe`). Configure it under Settings → Models → Embedding Model. If not configured, keyword matching is used.
|
||||
- **Multi-model comparison:** Results are not saved to session history.
|
||||
- **Debug panel:** Available in Settings → Debug (collapsed by default). Logs auto-clear after 30 minutes.
|
||||
- **Ollama CORS:** If requests are blocked, ensure `OLLAMA_ORIGINS=*` is set in your Ollama environment or your reverse proxy passes the correct CORS headers.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -33,6 +33,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
handleGetPageContent(message.tabId, sendResponse);
|
||||
return true;
|
||||
}
|
||||
if (message.action === 'OLLAMA_FETCH_JSON') {
|
||||
handleFetchJson(message, sendResponse);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -53,6 +57,23 @@ function handleGetPageContent(tabId, sendResponse) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Non-streaming JSON POST ───────────────────────────────────────────────────
|
||||
|
||||
async function handleFetchJson(message, sendResponse) {
|
||||
try {
|
||||
const res = await fetch(message.url, {
|
||||
method: 'POST',
|
||||
headers: message.headers || {},
|
||||
body: message.body
|
||||
});
|
||||
if (!res.ok) { sendResponse({ ok: false, error: 'HTTP ' + res.status }); return; }
|
||||
const data = await res.json();
|
||||
sendResponse({ ok: true, data });
|
||||
} catch (e) {
|
||||
sendResponse({ ok: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Non-streaming GET ─────────────────────────────────────────────────────────
|
||||
|
||||
async function handleGet(message, sendResponse) {
|
||||
|
||||
+170
-10
@@ -1,15 +1,175 @@
|
||||
console.log('[Ollama Sidebar] content script loaded on', location.href);
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'GET_PAGE_CONTENT') {
|
||||
try {
|
||||
const clone = document.body.cloneNode(true);
|
||||
clone.querySelectorAll('script, style, noscript, iframe, svg, canvas').forEach((el) => el.remove());
|
||||
const text = (clone.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 8000);
|
||||
sendResponse({ title: document.title, url: location.href, content: text });
|
||||
} catch (e) {
|
||||
sendResponse({ title: document.title, url: location.href, content: '' });
|
||||
}
|
||||
return true;
|
||||
if (message.type !== 'GET_PAGE_CONTENT') return;
|
||||
try {
|
||||
sendResponse(extractPage());
|
||||
} catch (e) {
|
||||
sendResponse({ title: document.title, url: location.href, content: '', wordCount: 0 });
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
function extractPage() {
|
||||
var meta = getMeta();
|
||||
|
||||
// ── 1. Score candidate content roots ───────────────────────────────────────
|
||||
var candidates = Array.from(document.querySelectorAll(
|
||||
'article, [role="article"], main, [role="main"], ' +
|
||||
'.post-content, .entry-content, .article-body, .article__body, ' +
|
||||
'.story-body, .page-content, .content-body, #article, #content, #main, #post'
|
||||
));
|
||||
|
||||
Array.from(document.querySelectorAll('div, section')).forEach(function (el) {
|
||||
if (el.querySelector('article, main, [role="main"]')) return;
|
||||
var score = contentScore(el);
|
||||
if (score > 20) candidates.push(el);
|
||||
});
|
||||
|
||||
var root = document.body;
|
||||
var best = 0;
|
||||
candidates.forEach(function (el) {
|
||||
var s = contentScore(el);
|
||||
if (s > best) { best = s; root = el; }
|
||||
});
|
||||
|
||||
// ── 2. Clone and strip noise ────────────────────────────────────────────────
|
||||
var clone = root.cloneNode(true);
|
||||
var noise = [
|
||||
'script','style','noscript','iframe','canvas','template',
|
||||
'nav','header','footer','aside',
|
||||
'[role="navigation"]','[role="banner"]',
|
||||
'[role="complementary"]','[role="contentinfo"]','[role="dialog"]',
|
||||
'[aria-hidden="true"]','[hidden]',
|
||||
'[class*="cookie"]','[class*="popup"]','[class*="modal"]',
|
||||
'[class*="banner"]','[class*="sidebar"]','[class*="widget"]',
|
||||
'[class*="advert"]','[class*="sponsor"]','[class*="promo"]',
|
||||
'[class*="newsletter"]','[class*="subscribe"]',
|
||||
'[class*="share-bar"]','[class*="social"]',
|
||||
'[class*="related"]','[class*="recommend"]',
|
||||
'[class*="comment"]','[class*="reply"]',
|
||||
'[id*="cookie"]','[id*="popup"]','[id*="modal"]',
|
||||
'[id*="sidebar"]','[id*="comment"]'
|
||||
].join(',');
|
||||
clone.querySelectorAll(noise).forEach(function (el) { el.remove(); });
|
||||
|
||||
// ── 3. Extract structured text ──────────────────────────────────────────────
|
||||
var lines = [];
|
||||
if (meta.description) lines.push(meta.description + '\n');
|
||||
walkNode(clone, lines);
|
||||
|
||||
var content = lines.join('').replace(/\n{3,}/g, '\n\n').trim();
|
||||
var wordCount = content.split(/\s+/).length;
|
||||
|
||||
return {
|
||||
title: document.title,
|
||||
url: location.href,
|
||||
description: meta.description,
|
||||
content: content,
|
||||
wordCount: wordCount
|
||||
};
|
||||
}
|
||||
|
||||
// ── Content scoring (Readability-lite) ────────────────────────────────────────
|
||||
function contentScore(el) {
|
||||
var text = el.innerText || el.textContent || '';
|
||||
var textLen = text.trim().length;
|
||||
if (textLen < 100) return 0;
|
||||
|
||||
var pCount = el.querySelectorAll('p').length;
|
||||
var links = el.querySelectorAll('a');
|
||||
var linkLen = Array.from(links).reduce(function (n, a) {
|
||||
return n + (a.textContent || '').length;
|
||||
}, 0);
|
||||
var linkDensity = textLen > 0 ? linkLen / textLen : 1;
|
||||
|
||||
var score = pCount * 3 + Math.sqrt(textLen) - (linkDensity * 50);
|
||||
|
||||
var tag = el.tagName;
|
||||
if (tag === 'ARTICLE') score += 30;
|
||||
if (tag === 'MAIN') score += 20;
|
||||
if (tag === 'SECTION') score += 5;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
// ── DOM walker ─────────────────────────────────────────────────────────────────
|
||||
function walkNode(node, out) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
var t = node.textContent.replace(/\s+/g, ' ');
|
||||
if (t.trim()) out.push(t);
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
|
||||
var tag = node.tagName;
|
||||
|
||||
var hLevel = { H1:1, H2:2, H3:3, H4:4, H5:5, H6:6 }[tag];
|
||||
if (hLevel) {
|
||||
var hText = (node.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
if (hText) out.push('\n' + '#'.repeat(hLevel) + ' ' + hText + '\n\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag === 'P' || tag === 'BLOCKQUOTE') {
|
||||
var before = out.length;
|
||||
node.childNodes.forEach(function (c) { walkNode(c, out); });
|
||||
if (out.length > before) out.push('\n\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag === 'LI') {
|
||||
var liParts = [];
|
||||
node.childNodes.forEach(function (c) { walkNode(c, liParts); });
|
||||
var liText = liParts.join('').replace(/\s+/g, ' ').trim();
|
||||
if (liText) out.push('- ' + liText + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag === 'UL' || tag === 'OL') {
|
||||
node.childNodes.forEach(function (c) { walkNode(c, out); });
|
||||
out.push('\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag === 'PRE' || tag === 'CODE') {
|
||||
var code = (node.textContent || '').trim();
|
||||
if (code) out.push('\n```\n' + code + '\n```\n\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag === 'TABLE') {
|
||||
node.querySelectorAll('tr').forEach(function (row) {
|
||||
var cells = Array.from(row.querySelectorAll('td,th'))
|
||||
.map(function (c) { return (c.textContent || '').replace(/\s+/g, ' ').trim(); })
|
||||
.filter(Boolean);
|
||||
if (cells.length) out.push(cells.join(' | ') + '\n');
|
||||
});
|
||||
out.push('\n');
|
||||
return;
|
||||
}
|
||||
|
||||
var BLOCK = { DIV:1, SECTION:1, ARTICLE:1, FIGURE:1, FIGCAPTION:1,
|
||||
TD:1, TH:1, DT:1, DD:1, DETAILS:1, SUMMARY:1 };
|
||||
if (BLOCK[tag]) {
|
||||
var bBefore = out.length;
|
||||
node.childNodes.forEach(function (c) { walkNode(c, out); });
|
||||
if (out.length > bBefore) {
|
||||
var last = out[out.length - 1];
|
||||
if (last && !last.endsWith('\n')) out.push('\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
node.childNodes.forEach(function (c) { walkNode(c, out); });
|
||||
}
|
||||
|
||||
// ── Meta extraction ───────────────────────────────────────────────────────────
|
||||
function getMeta() {
|
||||
var desc =
|
||||
(document.querySelector('meta[name="description"]') || {}).content ||
|
||||
(document.querySelector('meta[property="og:description"]') || {}).content ||
|
||||
(document.querySelector('meta[name="twitter:description"]') || {}).content ||
|
||||
'';
|
||||
return { description: desc.trim() };
|
||||
}
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ollama Sidebar — Icon Generator</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #161616;
|
||||
color: #e4e4e4;
|
||||
padding: 32px 24px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 { font-size: 20px; margin-bottom: 8px; }
|
||||
p { color: #888; font-size: 13px; line-height: 1.6; margin-bottom: 24px; }
|
||||
code {
|
||||
background: #2a2a2a;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.icons-row {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.icon-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
canvas {
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.size-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.save-btn {
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 7px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.save-btn:hover { background: #6d28d9; }
|
||||
.save-all-btn {
|
||||
background: #5b21b6;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.save-all-btn:hover { background: #4c1d95; }
|
||||
.instructions {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 24px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.7;
|
||||
color: #999;
|
||||
}
|
||||
.instructions ol { padding-left: 18px; }
|
||||
.instructions li { margin-bottom: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Ollama Sidebar — Icon Generator</h1>
|
||||
<p>
|
||||
Click <strong>Save All Icons</strong> (or save each individually), then place the downloaded
|
||||
files inside the <code>icons/</code> folder before loading the extension.
|
||||
</p>
|
||||
|
||||
<div class="icons-row" id="icons-row"></div>
|
||||
|
||||
<button class="save-all-btn" onclick="saveAll()">Save All Icons</button>
|
||||
|
||||
<div class="instructions">
|
||||
<strong>Steps to use:</strong>
|
||||
<ol>
|
||||
<li>Click <em>Save All Icons</em> — your browser will download three PNG files.</li>
|
||||
<li>Move <code>icon16.png</code>, <code>icon48.png</code>, and <code>icon128.png</code> into the <code>icons/</code> folder.</li>
|
||||
<li>Load the extension: <em>Extensions → Manage Extensions → Load unpacked</em> (Chrome) or <em>Tools → Extensions → Install from Disk</em> (Orion).</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var SIZES = [16, 48, 128];
|
||||
var canvases = {};
|
||||
|
||||
function drawIcon(size) {
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
// Purple gradient background
|
||||
var grad = ctx.createLinearGradient(0, 0, size, size);
|
||||
grad.addColorStop(0, '#7c3aed');
|
||||
grad.addColorStop(1, '#4f46e5');
|
||||
|
||||
var r = Math.max(2, Math.round(size * 0.18));
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) {
|
||||
ctx.roundRect(0, 0, size, size, r);
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
ctx.moveTo(r, 0);
|
||||
ctx.lineTo(size - r, 0);
|
||||
ctx.arcTo(size, 0, size, r, r);
|
||||
ctx.lineTo(size, size - r);
|
||||
ctx.arcTo(size, size, size - r, size, r);
|
||||
ctx.lineTo(r, size);
|
||||
ctx.arcTo(0, size, 0, size - r, r);
|
||||
ctx.lineTo(0, r);
|
||||
ctx.arcTo(0, 0, r, 0, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
|
||||
// "O" letter centred
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.95)';
|
||||
var fontSize = Math.round(size * 0.56);
|
||||
ctx.font = 'bold ' + fontSize + 'px -apple-system, BlinkMacSystemFont, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('O', size / 2, size * 0.52);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function buildUI() {
|
||||
var row = document.getElementById('icons-row');
|
||||
SIZES.forEach(function (size) {
|
||||
var canvas = drawIcon(size);
|
||||
canvases[size] = canvas;
|
||||
|
||||
// Scale canvas visually so small ones are visible
|
||||
var displaySize = Math.max(size, 64);
|
||||
canvas.style.width = displaySize + 'px';
|
||||
canvas.style.height = displaySize + 'px';
|
||||
|
||||
var card = document.createElement('div');
|
||||
card.className = 'icon-card';
|
||||
|
||||
var label = document.createElement('div');
|
||||
label.className = 'size-label';
|
||||
label.textContent = size + 'x' + size;
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'save-btn';
|
||||
btn.textContent = 'Save';
|
||||
btn.onclick = (function (s) {
|
||||
return function () { saveIcon(s); };
|
||||
}(size));
|
||||
|
||||
card.appendChild(canvas);
|
||||
card.appendChild(label);
|
||||
card.appendChild(btn);
|
||||
row.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function saveIcon(size) {
|
||||
var canvas = canvases[size];
|
||||
var link = document.createElement('a');
|
||||
link.download = 'icon' + size + '.png';
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function saveAll() {
|
||||
SIZES.forEach(function (size, i) {
|
||||
// Slight delay between downloads to avoid browser blocking multiple saves
|
||||
setTimeout(function () { saveIcon(size); }, i * 200);
|
||||
});
|
||||
}
|
||||
|
||||
buildUI();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub Dark
|
||||
Description: Dark theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||
+3
-2
@@ -3,7 +3,8 @@
|
||||
"name": "Ollama Sidebar",
|
||||
"version": "1.0.0",
|
||||
"description": "Chat with local Ollama models from a browser sidebar",
|
||||
"permissions": ["storage", "sidePanel", "tabs", "activeTab", "scripting"],
|
||||
"options_page": "settings.html",
|
||||
"permissions": ["storage", "sidePanel", "tabs", "activeTab"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
@@ -33,7 +34,7 @@
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["marked.min.js", "highlight.min.js", "marked.min.css"],
|
||||
"resources": ["marked.min.js", "highlight.min.js", "hljs-github-dark.min.css", "pdf.min.js", "pdf.worker.min.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
<html>
|
||||
<head><title>404 Not Found</title></head>
|
||||
<body>
|
||||
<center><h1>404 Not Found</h1></center>
|
||||
<hr><center>nginx</center>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+22
File diff suppressed because one or more lines are too long
Vendored
+22
File diff suppressed because one or more lines are too long
+472
@@ -0,0 +1,472 @@
|
||||
/* ── Reset ───────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
/* ── Dark theme (default) ────────────────────────────────────── */
|
||||
:root {
|
||||
--bg0: #111114;
|
||||
--bg1: #1a1a1f;
|
||||
--bg2: #222228;
|
||||
--bg3: #2a2a32;
|
||||
--bg4: #32323c;
|
||||
--fg1: #e2e2e8;
|
||||
--fg2: #8888a0;
|
||||
--fg3: #55556a;
|
||||
--accent: #7c3aed;
|
||||
--acc-h: #6d28d9;
|
||||
--acc-d: rgba(124,58,237,.15);
|
||||
--border: #2e2e38;
|
||||
--focus: #7c3aed;
|
||||
--ok: #22c55e;
|
||||
--warn: #d97706;
|
||||
--err: #ef4444;
|
||||
--scroll: #3a3a48;
|
||||
}
|
||||
|
||||
/* ── Light theme ─────────────────────────────────────────────── */
|
||||
[data-theme="light"] {
|
||||
--bg0: #f0f2f5;
|
||||
--bg1: #ffffff;
|
||||
--bg2: #f8f8fb;
|
||||
--bg3: #eeeff4;
|
||||
--bg4: #e4e5ec;
|
||||
--fg1: #1a1a2e;
|
||||
--fg2: #55556e;
|
||||
--fg3: #9999b0;
|
||||
--border: #dddde8;
|
||||
--ok: #16a34a;
|
||||
--warn: #b45309;
|
||||
--err: #dc2626;
|
||||
--scroll: #ccccdc;
|
||||
}
|
||||
|
||||
/* ── Base ────────────────────────────────────────────────────── */
|
||||
html, body {
|
||||
background: var(--bg0);
|
||||
color: var(--fg1);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 620px;
|
||||
margin: 0 auto;
|
||||
padding: 0 28px 56px;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ───────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--scroll); border-radius: 3px; }
|
||||
|
||||
/* ── Header ──────────────────────────────────────────────────── */
|
||||
header {
|
||||
padding: 32px 0 22px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--fg1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--fg2);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ── Sections ────────────────────────────────────────────────── */
|
||||
section {
|
||||
padding: 26px 0;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.09em;
|
||||
color: var(--accent);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
section h3 {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: var(--fg2);
|
||||
margin-top: 24px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ── Field groups ────────────────────────────────────────────── */
|
||||
.field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.field-group:last-child { margin-bottom: 0; }
|
||||
|
||||
.field-group label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--fg2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-weight: 400;
|
||||
color: var(--fg3);
|
||||
}
|
||||
|
||||
.field-group.inline {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ── Inputs ──────────────────────────────────────────────────── */
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
background: var(--bg1);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg1);
|
||||
padding: 9px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.12s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--focus);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 88px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23666'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 11px center;
|
||||
padding-right: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select option { background: var(--bg2); color: var(--fg1); }
|
||||
|
||||
/* ── Input row (with icon button) ────────────────────────────── */
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.input-row input { flex: 1; }
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────── */
|
||||
.icon-btn {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg2);
|
||||
padding: 9px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
|
||||
.icon-btn:hover { background: var(--bg3); color: var(--fg1); }
|
||||
|
||||
.primary-btn {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 26px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, transform 0.1s;
|
||||
}
|
||||
|
||||
.primary-btn:hover { background: var(--acc-h); }
|
||||
.primary-btn:active { transform: scale(0.98); }
|
||||
|
||||
.secondary-btn {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg2);
|
||||
padding: 9px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background: var(--bg3);
|
||||
border-color: var(--accent);
|
||||
color: var(--fg1);
|
||||
}
|
||||
|
||||
/* ── Field actions row ───────────────────────────────────────── */
|
||||
.field-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* ── Status text ─────────────────────────────────────────────── */
|
||||
.status-text {
|
||||
font-size: 12.5px;
|
||||
color: var(--fg3);
|
||||
}
|
||||
|
||||
.status-text.ok { color: var(--ok); }
|
||||
.status-text.err { color: var(--err); }
|
||||
.status-text.hidden { display: none; }
|
||||
|
||||
/* ── Theme toggle ────────────────────────────────────────────── */
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.theme-btn {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg2);
|
||||
padding: 7px 18px;
|
||||
border-radius: 7px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
||||
}
|
||||
|
||||
.theme-btn.active {
|
||||
background: var(--acc-d);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.theme-btn:hover:not(.active) { background: var(--bg3); }
|
||||
|
||||
/* ── Params grid ─────────────────────────────────────────────── */
|
||||
.params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 200px 52px;
|
||||
gap: 10px 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
font-size: 13px;
|
||||
color: var(--fg2);
|
||||
}
|
||||
|
||||
.param-input {
|
||||
width: 100%;
|
||||
background: var(--bg1);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg1);
|
||||
padding: 7px 10px;
|
||||
border-radius: 7px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.12s;
|
||||
}
|
||||
|
||||
.param-input:focus { border-color: var(--focus); }
|
||||
|
||||
.param-val {
|
||||
font-size: 12px;
|
||||
color: var(--fg3);
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── Save row ────────────────────────────────────────────────── */
|
||||
.save-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 28px 0 0;
|
||||
}
|
||||
|
||||
/* ── Pull progress ───────────────────────────────────────────────────────────── */
|
||||
.pull-progress {
|
||||
font-size: 12px;
|
||||
color: var(--fg2);
|
||||
font-family: monospace;
|
||||
padding: 6px 0 2px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.pull-progress.hidden { display: none; }
|
||||
|
||||
/* ── Debug section ───────────────────────────────────────────────────────────── */
|
||||
#debug-section {
|
||||
padding: 26px 0 0;
|
||||
}
|
||||
|
||||
.debug-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
padding: 0;
|
||||
}
|
||||
.debug-summary::-webkit-details-marker { display: none; }
|
||||
|
||||
.debug-summary span:first-child {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.09em;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.debug-summary-chevron {
|
||||
font-size: 10px;
|
||||
color: var(--fg3);
|
||||
transition: transform .2s;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#debug-details[open] .debug-summary-chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.debug-body {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.debug-sysinfo {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.debug-sys-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr;
|
||||
gap: 5px 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.debug-sys-label { color: var(--fg2); font-weight: 600; }
|
||||
.debug-sys-val { color: var(--fg1); word-break: break-all; }
|
||||
.debug-ua { color: var(--fg3); font-size: 11px; }
|
||||
|
||||
.debug-connection {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.debug-log {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
max-height: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.debug-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.debug-row:last-child { border-bottom: none; }
|
||||
|
||||
.debug-ts {
|
||||
color: var(--fg3);
|
||||
font-size: 10.5px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.debug-badge {
|
||||
color: #fff;
|
||||
font-size: 9.5px;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: .04em;
|
||||
flex-shrink: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.debug-msg { color: var(--fg1); flex: 1; min-width: 0; }
|
||||
|
||||
.debug-autoclear {
|
||||
font-size: 11.5px;
|
||||
color: var(--fg3);
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.debug-detail {
|
||||
width: 100%;
|
||||
margin-top: 3px;
|
||||
padding: 5px 8px;
|
||||
background: var(--bg0);
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 10.5px;
|
||||
color: var(--fg2);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ollama Sidebar — Settings</title>
|
||||
<link rel="stylesheet" href="settings.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" data-theme="dark">
|
||||
|
||||
<header>
|
||||
<h1>Ollama Sidebar</h1>
|
||||
<p class="subtitle">Settings</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- ── Connection ──────────────────────────────────────────── -->
|
||||
<section>
|
||||
<h2>Connection</h2>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="input-url">Ollama Base URL</label>
|
||||
<input type="text" id="input-url" placeholder="http://localhost:11434" spellcheck="false">
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="input-token">Bearer Token <span class="hint">(optional)</span></label>
|
||||
<div class="input-row">
|
||||
<input type="password" id="input-token" placeholder="Leave empty if not required" spellcheck="false">
|
||||
<button id="btn-show-token" class="icon-btn" title="Show / hide token">⚬</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-actions">
|
||||
<button id="btn-test" class="secondary-btn">Test Connection</button>
|
||||
<span id="test-status" class="status-text"></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ── Models ──────────────────────────────────────────────── -->
|
||||
<section>
|
||||
<h2>Models</h2>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="select-title-model">Title Generation Model</label>
|
||||
<select id="select-title-model">
|
||||
<option value="">— select model —</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="select-default-model">Default Chat Model</label>
|
||||
<select id="select-default-model">
|
||||
<option value="">— select model —</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="select-embed-model">Embedding Model <span class="hint">(for semantic search — nomic-embed-text-v2-moe recommended)</span></label>
|
||||
<select id="select-embed-model">
|
||||
<option value="">— disable semantic RAG —</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h3>Pull a Model</h3>
|
||||
<div class="field-group">
|
||||
<label for="pull-model-input">Model name</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="pull-model-input" placeholder="e.g. llama3:8b, qwen3:8b" spellcheck="false">
|
||||
<button id="btn-pull" class="icon-btn" title="Pull model">⇓</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pull-progress" class="pull-progress hidden"></div>
|
||||
</section>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ── Chat Defaults ───────────────────────────────────────── -->
|
||||
<section>
|
||||
<h2>Chat Defaults</h2>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="input-system-prompt">System Prompt</label>
|
||||
<textarea id="input-system-prompt" rows="4" spellcheck="true"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="input-language">Response Language <span class="hint">(optional — leave blank for model default)</span></label>
|
||||
<input type="text" id="input-language" placeholder="e.g. English, Spanish, French…" spellcheck="false">
|
||||
</div>
|
||||
|
||||
<h3>Model Parameters</h3>
|
||||
<div id="params-grid" class="params-grid"></div>
|
||||
</section>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ── Appearance ──────────────────────────────────────────── -->
|
||||
<section>
|
||||
<h2>Appearance</h2>
|
||||
|
||||
<div class="field-group inline">
|
||||
<label>Theme</label>
|
||||
<div class="toggle-row">
|
||||
<button id="btn-theme-dark" class="theme-btn" data-theme="dark">Dark</button>
|
||||
<button id="btn-theme-light" class="theme-btn" data-theme="light">Light</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group inline">
|
||||
<label>Auto-scroll during generation</label>
|
||||
<div class="toggle-row">
|
||||
<button id="btn-autoscroll-on" class="theme-btn" data-val="true">On</button>
|
||||
<button id="btn-autoscroll-off" class="theme-btn" data-val="false">Off</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group inline">
|
||||
<label>Compact message density</label>
|
||||
<div class="toggle-row">
|
||||
<button id="btn-compact-on" class="theme-btn" data-val="true">On</button>
|
||||
<button id="btn-compact-off" class="theme-btn" data-val="false">Off</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="save-row">
|
||||
<button id="btn-save" class="primary-btn">Save Settings</button>
|
||||
<span id="save-status" class="status-text ok hidden">Saved ✓</span>
|
||||
</div>
|
||||
|
||||
<section id="debug-section">
|
||||
<details id="debug-details">
|
||||
<summary class="debug-summary">
|
||||
<span>Debug</span>
|
||||
<span class="debug-summary-chevron">▶</span>
|
||||
</summary>
|
||||
<div class="debug-body">
|
||||
<div id="debug-sysinfo" class="debug-sysinfo"></div>
|
||||
<div class="field-actions" style="margin-bottom:12px;">
|
||||
<button id="btn-debug-clear" class="secondary-btn">Clear Log</button>
|
||||
<button id="btn-debug-export" class="secondary-btn">Copy Log</button>
|
||||
<span id="debug-log-count" class="status-text"></span>
|
||||
<span id="debug-autoclear" class="status-text debug-autoclear"></span>
|
||||
</div>
|
||||
<div id="debug-log" class="debug-log"></div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
<script src="settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+513
@@ -0,0 +1,513 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
var DEF_PARAMS = {
|
||||
temperature: 0.7,
|
||||
top_p: 0.9,
|
||||
top_k: 40,
|
||||
repeat_penalty: 1.1,
|
||||
seed: -1,
|
||||
num_ctx: 4096,
|
||||
num_predict: -1
|
||||
};
|
||||
|
||||
var PARAM_META = [
|
||||
{ key: 'temperature', label: 'Temperature', min: 0, max: 2, step: 0.05 },
|
||||
{ key: 'top_p', label: 'Top-P', min: 0, max: 1, step: 0.05 },
|
||||
{ key: 'top_k', label: 'Top-K', min: 1, max: 200, step: 1 },
|
||||
{ key: 'repeat_penalty', label: 'Repeat Penalty', min: 0.5, max: 2, step: 0.05 },
|
||||
{ key: 'seed', label: 'Seed (-1 = rand)', min: -1, max: 999999, step: 1 },
|
||||
{ key: 'num_ctx', label: 'Context (tokens)', min: 512, max: 131072, step: 512 },
|
||||
{ key: 'num_predict', label: 'Max Tokens (-1=∞)', min: -1, max: 8192, step: 1 }
|
||||
];
|
||||
|
||||
var DEFAULT_SYSTEM_PROMPT =
|
||||
'Be concise and detailed. Give the most information in the fewest words unless the topic requires a longer explanation.';
|
||||
|
||||
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
var app = document.getElementById('app');
|
||||
var inputUrl = document.getElementById('input-url');
|
||||
var inputToken = document.getElementById('input-token');
|
||||
var btnShowToken = document.getElementById('btn-show-token');
|
||||
var btnTest = document.getElementById('btn-test');
|
||||
var testStatus = document.getElementById('test-status');
|
||||
var selectTitleModel = document.getElementById('select-title-model');
|
||||
var selectDefaultModel = document.getElementById('select-default-model');
|
||||
var selectEmbedModel = document.getElementById('select-embed-model');
|
||||
var inputSystemPrompt = document.getElementById('input-system-prompt');
|
||||
var inputLanguage = document.getElementById('input-language');
|
||||
var paramsGrid = document.getElementById('params-grid');
|
||||
var btnThemeDark = document.getElementById('btn-theme-dark');
|
||||
var btnThemeLight = document.getElementById('btn-theme-light');
|
||||
var btnAutoscrollOn = document.getElementById('btn-autoscroll-on');
|
||||
var btnAutoscrollOff = document.getElementById('btn-autoscroll-off');
|
||||
var btnCompactOn = document.getElementById('btn-compact-on');
|
||||
var btnCompactOff = document.getElementById('btn-compact-off');
|
||||
var btnSave = document.getElementById('btn-save');
|
||||
var saveStatus = document.getElementById('save-status');
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
var S = { theme: 'dark', autoScroll: true, compact: false, models: [] };
|
||||
|
||||
// ── Utilities ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatBytes(b) {
|
||||
if (b >= 1e9) return (b / 1e9).toFixed(1) + ' GB';
|
||||
if (b >= 1e6) return (b / 1e6).toFixed(0) + ' MB';
|
||||
return b + ' B';
|
||||
}
|
||||
|
||||
function getHeaders() {
|
||||
var h = { 'Content-Type': 'application/json' };
|
||||
var tok = inputToken.value.trim();
|
||||
if (tok) h['Authorization'] = 'Bearer ' + tok;
|
||||
return h;
|
||||
}
|
||||
|
||||
// ── Theme ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function setTheme(t) {
|
||||
S.theme = t;
|
||||
app.setAttribute('data-theme', t);
|
||||
btnThemeDark.classList.toggle('active', t === 'dark');
|
||||
btnThemeLight.classList.toggle('active', t === 'light');
|
||||
}
|
||||
|
||||
function setAutoScroll(on) {
|
||||
S.autoScroll = on;
|
||||
btnAutoscrollOn.classList.toggle('active', on);
|
||||
btnAutoscrollOff.classList.toggle('active', !on);
|
||||
}
|
||||
|
||||
function setCompact(on) {
|
||||
S.compact = on;
|
||||
btnCompactOn.classList.toggle('active', on);
|
||||
btnCompactOff.classList.toggle('active', !on);
|
||||
}
|
||||
|
||||
// ── Model dropdowns ───────────────────────────────────────────────────────────
|
||||
|
||||
function populateModelSelects(models) {
|
||||
S.models = models;
|
||||
var titleVal = selectTitleModel.value;
|
||||
var defaultVal = selectDefaultModel.value;
|
||||
var embedVal = selectEmbedModel.value;
|
||||
|
||||
[selectTitleModel, selectDefaultModel].forEach(function (sel) {
|
||||
sel.innerHTML = '<option value="">— select model —</option>';
|
||||
models.forEach(function (m) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = m.name;
|
||||
var lbl = m.name;
|
||||
if (m.paramSize) lbl += ' (' + m.paramSize + ')';
|
||||
if (m.sizeLabel) lbl += ' · ' + m.sizeLabel;
|
||||
opt.textContent = lbl;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
});
|
||||
|
||||
selectEmbedModel.innerHTML = '<option value="">— disable semantic RAG —</option>';
|
||||
models.forEach(function (m) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = m.name;
|
||||
var lbl = m.name;
|
||||
if (m.paramSize) lbl += ' (' + m.paramSize + ')';
|
||||
if (m.sizeLabel) lbl += ' · ' + m.sizeLabel;
|
||||
opt.textContent = lbl;
|
||||
selectEmbedModel.appendChild(opt);
|
||||
});
|
||||
|
||||
selectTitleModel.value = titleVal;
|
||||
selectDefaultModel.value = defaultVal;
|
||||
selectEmbedModel.value = embedVal;
|
||||
}
|
||||
|
||||
// ── Params grid ───────────────────────────────────────────────────────────────
|
||||
|
||||
function buildParamsGrid(stored) {
|
||||
paramsGrid.innerHTML = '';
|
||||
PARAM_META.forEach(function (pm) {
|
||||
var val = (stored[pm.key] !== undefined) ? stored[pm.key] : DEF_PARAMS[pm.key];
|
||||
|
||||
var label = document.createElement('label');
|
||||
label.className = 'param-label';
|
||||
label.htmlFor = 'param-' + pm.key;
|
||||
label.textContent = pm.label;
|
||||
|
||||
var input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.id = 'param-' + pm.key;
|
||||
input.className = 'param-input';
|
||||
input.value = val;
|
||||
input.min = pm.min;
|
||||
input.max = pm.max;
|
||||
input.step = pm.step;
|
||||
|
||||
var valDisplay = document.createElement('span');
|
||||
valDisplay.className = 'param-val';
|
||||
valDisplay.textContent = val;
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
valDisplay.textContent = this.value;
|
||||
});
|
||||
|
||||
paramsGrid.appendChild(label);
|
||||
paramsGrid.appendChild(input);
|
||||
paramsGrid.appendChild(valDisplay);
|
||||
});
|
||||
}
|
||||
|
||||
function getParamsFromGrid() {
|
||||
var result = {};
|
||||
PARAM_META.forEach(function (pm) {
|
||||
var inp = document.getElementById('param-' + pm.key);
|
||||
if (inp) result[pm.key] = parseFloat(inp.value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Network ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function fetchModels(cb) {
|
||||
var baseUrl = inputUrl.value.trim().replace(/\/+$/, '') || 'http://localhost:11434';
|
||||
chrome.runtime.sendMessage(
|
||||
{ action: 'OLLAMA_GET', url: baseUrl + '/api/tags', headers: getHeaders() },
|
||||
function (response) {
|
||||
if (chrome.runtime.lastError || !response || !response.ok) {
|
||||
if (cb) cb([]);
|
||||
return;
|
||||
}
|
||||
var models = (response.data.models || []).map(function (m) {
|
||||
return {
|
||||
name: m.name,
|
||||
paramSize: (m.details && m.details.parameter_size) || '',
|
||||
sizeLabel: m.size ? formatBytes(m.size) : ''
|
||||
};
|
||||
});
|
||||
populateModelSelects(models);
|
||||
if (cb) cb(models);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ── Pull model ────────────────────────────────────────────────────────────────
|
||||
|
||||
function pullModel(name) {
|
||||
if (!name) return;
|
||||
var progressEl = document.getElementById('pull-progress');
|
||||
progressEl.classList.remove('hidden');
|
||||
progressEl.textContent = 'Starting pull…';
|
||||
|
||||
var baseUrl = inputUrl.value.trim().replace(/\/+$/, '') || 'http://localhost:11434';
|
||||
var requestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||||
var lastPullStatus = '';
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
action: 'OLLAMA_FETCH',
|
||||
url: baseUrl + '/api/pull',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ name: name, stream: true }),
|
||||
requestId: requestId
|
||||
});
|
||||
|
||||
var pullListener = function (msg) {
|
||||
if (msg.requestId !== requestId) return;
|
||||
if (msg.action === 'STREAM_CHUNK' && msg.text) {
|
||||
try {
|
||||
var parsed = JSON.parse(msg.text);
|
||||
var status = parsed.status || '';
|
||||
var completed = parsed.completed || 0;
|
||||
var total = parsed.total || 0;
|
||||
var pct = total > 0 ? ' (' + Math.round(completed / total * 100) + '%)' : '';
|
||||
progressEl.textContent = status + pct;
|
||||
lastPullStatus = status || lastPullStatus;
|
||||
} catch (_) {}
|
||||
}
|
||||
if (msg.action === 'STREAM_DONE') {
|
||||
chrome.runtime.onMessage.removeListener(pullListener);
|
||||
progressEl.textContent = 'Pull complete — ' + name;
|
||||
setTimeout(function () { progressEl.classList.add('hidden'); }, 2000);
|
||||
fetchModels();
|
||||
}
|
||||
if (msg.action === 'STREAM_ERROR') {
|
||||
chrome.runtime.onMessage.removeListener(pullListener);
|
||||
progressEl.textContent = 'Error: ' + (msg.error || 'pull failed');
|
||||
}
|
||||
};
|
||||
chrome.runtime.onMessage.addListener(pullListener);
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
function showSaved() {
|
||||
saveStatus.classList.remove('hidden');
|
||||
setTimeout(function () { saveStatus.classList.add('hidden'); }, 2000);
|
||||
}
|
||||
|
||||
// ── Load ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
function loadSettings() {
|
||||
chrome.storage.local.get([
|
||||
'ollamaSidebarUrl', 'ollamaSidebarToken',
|
||||
'ollamaSidebarTitleModel', 'ollamaSidebarDefaultModel',
|
||||
'ollamaSidebarEmbedModel',
|
||||
'ollamaSidebarSystemPrompt', 'ollamaSidebarTheme',
|
||||
'ollamaSidebarParams', 'ollamaSidebarLanguage',
|
||||
'ollamaSidebarAutoScroll', 'ollamaSidebarCompact'
|
||||
], function (r) {
|
||||
inputUrl.value = r.ollamaSidebarUrl || 'http://localhost:11434';
|
||||
inputToken.value = r.ollamaSidebarToken || '';
|
||||
inputSystemPrompt.value = r.ollamaSidebarSystemPrompt || DEFAULT_SYSTEM_PROMPT;
|
||||
inputLanguage.value = r.ollamaSidebarLanguage || '';
|
||||
setTheme(r.ollamaSidebarTheme || 'dark');
|
||||
setAutoScroll(r.ollamaSidebarAutoScroll !== false);
|
||||
setCompact(r.ollamaSidebarCompact || false);
|
||||
buildParamsGrid(r.ollamaSidebarParams || {});
|
||||
|
||||
fetchModels(function () {
|
||||
selectTitleModel.value = r.ollamaSidebarTitleModel || '';
|
||||
selectDefaultModel.value = r.ollamaSidebarDefaultModel || '';
|
||||
selectEmbedModel.value = r.ollamaSidebarEmbedModel || '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Event handlers ────────────────────────────────────────────────────────────
|
||||
|
||||
btnShowToken.addEventListener('click', function () {
|
||||
inputToken.type = inputToken.type === 'password' ? 'text' : 'password';
|
||||
});
|
||||
|
||||
btnTest.addEventListener('click', function () {
|
||||
var baseUrl = inputUrl.value.trim().replace(/\/+$/, '') || 'http://localhost:11434';
|
||||
testStatus.textContent = 'Testing…';
|
||||
testStatus.className = 'status-text';
|
||||
chrome.runtime.sendMessage(
|
||||
{ action: 'OLLAMA_GET', url: baseUrl + '/api/tags', headers: getHeaders() },
|
||||
function (response) {
|
||||
if (chrome.runtime.lastError || !response) {
|
||||
var e = chrome.runtime.lastError ? chrome.runtime.lastError.message : 'No response';
|
||||
testStatus.textContent = 'Failed (' + e + ')';
|
||||
testStatus.className = 'status-text err';
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
testStatus.textContent = 'Failed (' + (response.status || 'error') + ')';
|
||||
testStatus.className = 'status-text err';
|
||||
return;
|
||||
}
|
||||
var models = (response.data.models || []).map(function (m) {
|
||||
return {
|
||||
name: m.name,
|
||||
paramSize: (m.details && m.details.parameter_size) || '',
|
||||
sizeLabel: m.size ? formatBytes(m.size) : ''
|
||||
};
|
||||
});
|
||||
populateModelSelects(models);
|
||||
testStatus.textContent = 'Connected — ' + models.length + ' model(s)';
|
||||
testStatus.className = 'status-text ok';
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
btnThemeDark.addEventListener('click', function () { setTheme('dark'); });
|
||||
btnThemeLight.addEventListener('click', function () { setTheme('light'); });
|
||||
|
||||
btnAutoscrollOn.addEventListener('click', function () { setAutoScroll(true); });
|
||||
btnAutoscrollOff.addEventListener('click', function () { setAutoScroll(false); });
|
||||
|
||||
btnCompactOn.addEventListener('click', function () {
|
||||
setCompact(true);
|
||||
chrome.storage.local.set({ ollamaSidebarCompact: true });
|
||||
});
|
||||
btnCompactOff.addEventListener('click', function () {
|
||||
setCompact(false);
|
||||
chrome.storage.local.set({ ollamaSidebarCompact: false });
|
||||
});
|
||||
|
||||
btnSave.addEventListener('click', function () {
|
||||
var url = inputUrl.value.trim().replace(/\/+$/, '') || 'http://localhost:11434';
|
||||
chrome.storage.local.set({
|
||||
ollamaSidebarUrl: url,
|
||||
ollamaSidebarToken: inputToken.value,
|
||||
ollamaSidebarTitleModel: selectTitleModel.value,
|
||||
ollamaSidebarDefaultModel: selectDefaultModel.value,
|
||||
ollamaSidebarEmbedModel: selectEmbedModel.value,
|
||||
ollamaSidebarSystemPrompt: inputSystemPrompt.value,
|
||||
ollamaSidebarLanguage: inputLanguage.value.trim(),
|
||||
ollamaSidebarTheme: S.theme,
|
||||
ollamaSidebarAutoScroll: S.autoScroll,
|
||||
ollamaSidebarCompact: S.compact,
|
||||
ollamaSidebarParams: getParamsFromGrid()
|
||||
}, function () {
|
||||
showSaved();
|
||||
renderSysinfo();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('btn-pull').addEventListener('click', function () {
|
||||
pullModel(document.getElementById('pull-model-input').value.trim());
|
||||
});
|
||||
|
||||
document.getElementById('pull-model-input').addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') pullModel(this.value.trim());
|
||||
});
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
loadSettings();
|
||||
|
||||
// ── Debug panel ───────────────────────────────────────────────────────────────
|
||||
|
||||
var DBG_KEY = 'ollama_debug_log';
|
||||
var AUTOCLEAR_MS = 30 * 60 * 1000;
|
||||
var debugLog = document.getElementById('debug-log');
|
||||
var debugSysinfo = document.getElementById('debug-sysinfo');
|
||||
var debugCount = document.getElementById('debug-log-count');
|
||||
var debugAutoclear = document.getElementById('debug-autoclear');
|
||||
|
||||
var TYPE_COLORS = {
|
||||
CLICK: '#7c3aed',
|
||||
FILE: '#2563eb',
|
||||
OLLAMA: '#059669',
|
||||
STREAM: '#0891b2',
|
||||
PERSONA: '#db2777',
|
||||
MODEL: '#d97706',
|
||||
ERROR: '#dc2626',
|
||||
INFO: '#6b7280',
|
||||
SESSION: '#7c3aed'
|
||||
};
|
||||
|
||||
function renderSysinfo() {
|
||||
chrome.storage.local.get(['ollamaSidebarUrl', 'ollamaSidebarToken'], function (r) {
|
||||
var url = r.ollamaSidebarUrl || 'http://localhost:11434';
|
||||
var ua = navigator.userAgent;
|
||||
var browser = 'Unknown';
|
||||
if (/OPR\//.test(ua)) browser = 'Opera';
|
||||
else if (/Orion/.test(ua)) browser = 'Orion';
|
||||
else if (/Edg\//.test(ua)) browser = 'Edge';
|
||||
else if (/Chrome\//.test(ua)) browser = 'Chrome';
|
||||
else if (/Safari\//.test(ua)) browser = 'Safari';
|
||||
else if (/Firefox\//.test(ua)) browser = 'Firefox';
|
||||
|
||||
var supportsFilePicker = typeof window.showOpenFilePicker !== 'undefined';
|
||||
|
||||
debugSysinfo.innerHTML =
|
||||
'<div class="debug-sys-grid">' +
|
||||
'<span class="debug-sys-label">Browser</span><span class="debug-sys-val">' + browser + '</span>' +
|
||||
'<span class="debug-sys-label">User Agent</span><span class="debug-sys-val debug-ua">' + ua + '</span>' +
|
||||
'<span class="debug-sys-label">Ollama URL</span><span class="debug-sys-val">' + url + '</span>' +
|
||||
'<span class="debug-sys-label">showOpenFilePicker</span><span class="debug-sys-val" style="color:' + (supportsFilePicker ? 'var(--ok)' : 'var(--err)') + '">' + (supportsFilePicker ? 'supported' : 'NOT supported') + '</span>' +
|
||||
'</div>';
|
||||
|
||||
var tok = r.ollamaSidebarToken || '';
|
||||
var hdrs = { 'Content-Type': 'application/json' };
|
||||
if (tok) hdrs['Authorization'] = 'Bearer ' + tok;
|
||||
chrome.runtime.sendMessage({ action: 'OLLAMA_GET', url: url + '/api/tags', headers: hdrs }, function (res) {
|
||||
var statusEl = debugSysinfo.querySelector('.debug-connection') || document.createElement('div');
|
||||
statusEl.className = 'debug-connection';
|
||||
if (res && res.ok) {
|
||||
var modelNames = (res.data.models || []).map(function (m) { return m.name; }).join(', ');
|
||||
statusEl.innerHTML = '<span style="color:var(--ok)">● Connected</span> — ' +
|
||||
(res.data.models || []).length + ' model(s): ' + (modelNames || '(none)');
|
||||
} else {
|
||||
statusEl.innerHTML = '<span style="color:var(--err)">● Disconnected</span> — ' + (res ? res.error : 'no response');
|
||||
}
|
||||
if (!debugSysinfo.contains(statusEl)) debugSysinfo.appendChild(statusEl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateAutoclearLabel(entries) {
|
||||
if (!debugAutoclear) return;
|
||||
if (!entries || !entries.length) {
|
||||
debugAutoclear.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
var oldest = entries.reduce(function (min, e) {
|
||||
return e.ts < min ? e.ts : min;
|
||||
}, entries[0].ts);
|
||||
|
||||
var expiresAt = oldest + AUTOCLEAR_MS;
|
||||
var remaining = expiresAt - Date.now();
|
||||
|
||||
if (remaining <= 0) {
|
||||
debugAutoclear.textContent = 'Oldest entry clearing on next log write';
|
||||
debugAutoclear.style.color = 'var(--err)';
|
||||
return;
|
||||
}
|
||||
|
||||
var totalSecs = Math.ceil(remaining / 1000);
|
||||
var mins = Math.floor(totalSecs / 60);
|
||||
var secs = totalSecs % 60;
|
||||
var label = mins > 0
|
||||
? 'Oldest clears in ' + mins + 'm ' + secs + 's'
|
||||
: 'Oldest clears in ' + secs + 's';
|
||||
|
||||
debugAutoclear.textContent = label;
|
||||
debugAutoclear.style.color = mins < 5 ? 'var(--warn)' : 'var(--fg3)';
|
||||
}
|
||||
|
||||
var _lastLogEntries = [];
|
||||
|
||||
function renderLog(entries) {
|
||||
_lastLogEntries = entries;
|
||||
debugCount.textContent = entries.length + ' entries';
|
||||
updateAutoclearLabel(entries);
|
||||
if (!entries.length) {
|
||||
debugLog.innerHTML = '<div style="padding:12px;color:var(--fg3);font-size:12px;">No log entries yet. Interact with the sidebar to generate logs.</div>';
|
||||
return;
|
||||
}
|
||||
var rows = entries.slice().reverse().map(function (e) {
|
||||
var color = TYPE_COLORS[e.type] || '#6b7280';
|
||||
var time = new Date(e.ts).toLocaleTimeString();
|
||||
var detail = e.detail ? '<div class="debug-detail">' + JSON.stringify(e.detail, null, 2) + '</div>' : '';
|
||||
return '<div class="debug-row">' +
|
||||
'<span class="debug-ts">' + time + '</span>' +
|
||||
'<span class="debug-badge" style="background:' + color + '">' + e.type + '</span>' +
|
||||
'<span class="debug-msg">' + e.msg + '</span>' +
|
||||
detail +
|
||||
'</div>';
|
||||
}).join('');
|
||||
debugLog.innerHTML = rows;
|
||||
}
|
||||
|
||||
function loadLog() {
|
||||
chrome.storage.local.get(DBG_KEY, function (r) {
|
||||
renderLog(r[DBG_KEY] || []);
|
||||
});
|
||||
}
|
||||
|
||||
chrome.storage.onChanged.addListener(function (changes, area) {
|
||||
if (area === 'local' && changes[DBG_KEY]) {
|
||||
renderLog(changes[DBG_KEY].newValue || []);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-debug-clear').addEventListener('click', function () {
|
||||
chrome.storage.local.set({ [DBG_KEY]: [] }, loadLog);
|
||||
});
|
||||
|
||||
document.getElementById('btn-debug-export').addEventListener('click', function () {
|
||||
chrome.storage.local.get(DBG_KEY, function (r) {
|
||||
var text = JSON.stringify(r[DBG_KEY] || [], null, 2);
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
document.getElementById('btn-debug-export').textContent = 'Copied ✓';
|
||||
setTimeout(function () { document.getElementById('btn-debug-export').textContent = 'Copy Log'; }, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(function () {
|
||||
updateAutoclearLabel(_lastLogEntries);
|
||||
}, 30000);
|
||||
|
||||
renderSysinfo();
|
||||
loadLog();
|
||||
|
||||
})();
|
||||
+565
-295
File diff suppressed because it is too large
Load Diff
+120
-80
@@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ollama Sidebar</title>
|
||||
<link rel="stylesheet" href="sidepanel.css">
|
||||
<link rel="stylesheet" href="hljs-github-dark.min.css">
|
||||
<script src="marked.min.js"></script>
|
||||
<script src="highlight.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" data-theme="dark">
|
||||
@@ -12,76 +15,39 @@
|
||||
<!-- ── Header ─────────────────────────────────────────────── -->
|
||||
<header>
|
||||
<div class="h-left">
|
||||
<button id="btn-sessions" class="icon-btn" title="Conversations">☰</button>
|
||||
<button id="btn-sessions" class="icon-btn" title="Chat history">💬</button>
|
||||
<span class="app-title">Ollama</span>
|
||||
<span id="status-dot" class="status-dot" title="Checking…"></span>
|
||||
</div>
|
||||
<div class="h-right">
|
||||
<button id="btn-theme" class="icon-btn" title="Toggle theme">◐</button>
|
||||
<div class="status-wrap">
|
||||
<span id="status-dot" class="status-dot" title="Checking…"></span>
|
||||
<div id="stats-panel" class="stats-panel hidden"></div>
|
||||
</div>
|
||||
<button id="btn-compact" class="icon-btn" title="Compact mode">≡</button>
|
||||
<button id="btn-help" class="icon-btn" title="Keyboard shortcuts (Ctrl+?)">?</button>
|
||||
<button id="btn-settings" class="icon-btn" title="Settings">⚙</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── Settings panel ─────────────────────────────────────── -->
|
||||
<div id="settings-panel" class="drop-panel hidden">
|
||||
<div class="field-group">
|
||||
<label class="field-lbl">Ollama URL</label>
|
||||
<input type="text" id="input-url" placeholder="http://localhost:11434" spellcheck="false">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-lbl">Bearer Token <span class="hint">(optional)</span></label>
|
||||
<div class="input-row-inline">
|
||||
<input type="password" id="input-token" placeholder="Leave empty if not required" spellcheck="false">
|
||||
<button id="btn-show-token" class="icon-btn sm" title="Show/hide">⚬</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-save" class="primary-btn">Save & Connect</button>
|
||||
<div id="settings-status" class="status-line"></div>
|
||||
|
||||
<!-- System prompt -->
|
||||
<div id="system-prompt-panel" class="collapsible">
|
||||
<div class="coll-hdr">
|
||||
<span>System Prompt</span>
|
||||
<button id="btn-close-system" class="icon-btn sm">✕</button>
|
||||
</div>
|
||||
<textarea id="input-system-prompt"
|
||||
placeholder="Instructions for the model this session…"
|
||||
rows="3"
|
||||
spellcheck="true"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Model parameters -->
|
||||
<div id="params-panel" class="collapsible">
|
||||
<div class="coll-hdr">
|
||||
<span>Parameters</span>
|
||||
<button id="btn-close-params" class="icon-btn sm">✕</button>
|
||||
</div>
|
||||
<div id="params-grid" class="params-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Model selector -->
|
||||
<div class="field-group">
|
||||
<label class="field-lbl">Model</label>
|
||||
<select id="select-model"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Sessions panel ─────────────────────────────────────── -->
|
||||
<div id="sessions-panel" class="drop-panel hidden">
|
||||
<div class="panel-hdr">
|
||||
<span class="panel-title">Conversations</span>
|
||||
<button id="btn-new-session" class="pill-btn">+ New</button>
|
||||
</div>
|
||||
<input type="text" id="session-search" class="session-search" placeholder="Search conversations…" spellcheck="false">
|
||||
<div id="sessions-list" class="sessions-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── Session bar ─────────────────────────────────────────── -->
|
||||
<div class="session-bar">
|
||||
<span id="session-name-display" class="session-name" title="Click to rename" id="session-name-display"></span>
|
||||
<span id="session-name-display" class="session-name" title="Double-click to rename"></span>
|
||||
<div class="session-bar-actions">
|
||||
<button id="btn-rename-session" class="icon-btn sm" title="Rename">✎</button>
|
||||
<button id="btn-export-session" class="icon-btn sm" title="Export as Markdown">⇓</button>
|
||||
<button id="btn-delete-session" class="icon-btn sm danger" title="Delete session">✕</button>
|
||||
<select id="model-select" class="model-select-bar" title="Active model"></select>
|
||||
<select id="model-select-b" class="model-select-bar hidden" title="Second model (compare)"></select>
|
||||
<button id="btn-persona" class="icon-btn sm" title="Switch persona">🎭</button>
|
||||
<button id="btn-export-session" class="icon-btn sm" title="Export chat as Markdown">📥</button>
|
||||
<button id="btn-delete-session" class="icon-btn sm danger" title="Delete chat">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,35 +59,27 @@
|
||||
<!-- ── Input area ──────────────────────────────────────────── -->
|
||||
<div class="input-area">
|
||||
|
||||
<!-- File chips -->
|
||||
<!-- Row 1: Context chips (shown when active) -->
|
||||
<div id="file-chips-area" class="file-chips-area hidden">
|
||||
<div id="file-chips" class="file-chips"></div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<button id="btn-page-context" class="toolbar-btn" title="Toggle page context">
|
||||
<span class="tbi">▢</span><span id="page-ctx-lbl">Page</span>
|
||||
</button>
|
||||
<button id="btn-attach-file" class="toolbar-btn" title="Attach file (.txt .md .pdf .csv)">
|
||||
<span class="tbi">📎</span>
|
||||
</button>
|
||||
<button id="btn-templates-open" class="toolbar-btn" title="Prompt templates">
|
||||
<span class="tbi">/</span>
|
||||
</button>
|
||||
<div class="tb-spacer"></div>
|
||||
<button id="btn-clear" class="toolbar-btn" title="Clear conversation">
|
||||
<span class="tbi">✕</span> Clear
|
||||
</button>
|
||||
<!-- Context window bar -->
|
||||
<div id="ctx-bar-wrap" class="ctx-bar-wrap-full" title="Context window usage">
|
||||
<span class="ctx-bar-label">Context</span>
|
||||
<div class="ctx-bar-track">
|
||||
<div id="ctx-bar" class="ctx-bar"></div>
|
||||
</div>
|
||||
<span id="ctx-bar-pct" class="ctx-bar-pct">0%</span>
|
||||
</div>
|
||||
|
||||
<!-- Message input -->
|
||||
<!-- Row 2: Message textarea -->
|
||||
<textarea id="input-message"
|
||||
placeholder="Type a message… (Enter to send, Shift+Enter for newline, / for templates)"
|
||||
rows="3"
|
||||
placeholder="Message… (Enter to send, Shift+Enter for newline, / for templates)"
|
||||
rows="2"
|
||||
spellcheck="true"></textarea>
|
||||
|
||||
<!-- Template autocomplete dropdown (shown when / is typed) -->
|
||||
<!-- Template autocomplete dropdown -->
|
||||
<div id="template-dropdown" class="tmpl-dropdown hidden">
|
||||
<div class="tmpl-dd-header">
|
||||
Templates
|
||||
@@ -130,11 +88,22 @@
|
||||
<div id="tmpl-dd-list" class="tmpl-dd-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Send row -->
|
||||
<!-- Row 3: Controls -->
|
||||
<div class="send-row">
|
||||
<button id="btn-stop" class="stop-btn hidden" title="Stop generation">■</button>
|
||||
<span id="model-display" class="model-display" title="Active model"></span>
|
||||
<button id="btn-send" class="send-btn" title="Send (Enter)">►</button>
|
||||
<button id="btn-compare" class="icon-btn sm compare-btn" title="Compare two models (select a second model to begin)">
|
||||
<span class="tbi">⇔</span><span class="tbi" style="font-size:10px;">Compare</span>
|
||||
</button>
|
||||
<button id="btn-templates-open" class="icon-btn sm" title="Prompt templates (/)">
|
||||
<span class="tbi">/</span>
|
||||
</button>
|
||||
<button id="btn-page-context" class="icon-btn sm ctx-btn" title="Add current page as context">
|
||||
<span class="tbi">🌐</span><span id="page-ctx-lbl">Add page</span>
|
||||
</button>
|
||||
<button id="btn-attach-file" class="icon-btn sm" title="Attach file or image">
|
||||
<span class="tbi">📎</span>
|
||||
</button>
|
||||
<div class="tb-spacer"></div>
|
||||
<button id="btn-send" class="send-btn" title="Send message (Enter)">►</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /input-area -->
|
||||
@@ -155,13 +124,84 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden file input -->
|
||||
<input type="file" id="file-input" accept=".txt,.md,.pdf,.csv" multiple hidden>
|
||||
<!-- ── Persona manager overlay ────────────────────────────── -->
|
||||
<div id="persona-overlay" class="overlay hidden">
|
||||
<div class="overlay-panel">
|
||||
<div class="overlay-hdr">
|
||||
<span>Personas</span>
|
||||
<button id="btn-persona-overlay-close" class="icon-btn">✕</button>
|
||||
</div>
|
||||
<div id="persona-list" class="tmpl-list"></div>
|
||||
<div class="tmpl-add-form">
|
||||
<input id="persona-new-name" type="text" placeholder="Persona name…" maxlength="60">
|
||||
<textarea id="persona-new-prompt" placeholder="System prompt…" rows="4"></textarea>
|
||||
<button id="btn-persona-add" class="primary-btn">Add Persona</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Export preview overlay ──────────────────────────────── -->
|
||||
<div id="export-overlay" class="overlay hidden">
|
||||
<div class="overlay-panel">
|
||||
<div class="overlay-hdr">
|
||||
<span id="export-overlay-title">Export</span>
|
||||
<div style="display:flex;gap:6px;align-items:center;">
|
||||
<button id="btn-export-copy" class="pill-btn">Copy</button>
|
||||
<button id="btn-export-close" class="icon-btn">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="export-content" class="export-pre"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ── Drag and drop overlay ───────────────────────────────── -->
|
||||
<div id="drag-overlay" class="drag-overlay hidden">
|
||||
<div class="drag-overlay-inner">
|
||||
<div class="drag-icon">📎</div>
|
||||
<div class="drag-label">Drop files to attach</div>
|
||||
<div class="drag-sub">.txt · .md · .pdf · .csv · .jpg · .png · .webp</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Keyboard shortcuts overlay ──────────────────────────── -->
|
||||
<div id="help-overlay" class="overlay hidden">
|
||||
<div class="overlay-panel help-panel">
|
||||
<div class="overlay-hdr">
|
||||
<span>Keyboard Shortcuts</span>
|
||||
<button id="btn-help-close" class="icon-btn">✕</button>
|
||||
</div>
|
||||
<div class="help-body">
|
||||
<div class="help-group">
|
||||
<div class="help-group-label">Chat</div>
|
||||
<div class="help-row"><kbd>Enter</kbd><span>Send message</span></div>
|
||||
<div class="help-row"><kbd>Shift</kbd><kbd>Enter</kbd><span>New line</span></div>
|
||||
<div class="help-row"><kbd>Ctrl</kbd><kbd>L</kbd><span>Clear conversation (press twice)</span></div>
|
||||
</div>
|
||||
<div class="help-group">
|
||||
<div class="help-group-label">Navigation</div>
|
||||
<div class="help-row"><kbd>Ctrl</kbd><kbd>K</kbd><span>New chat</span></div>
|
||||
<div class="help-row"><kbd>Ctrl</kbd><kbd>,</kbd><span>Open settings</span></div>
|
||||
<div class="help-row"><kbd>Ctrl</kbd><kbd>/</kbd><span>Open templates</span></div>
|
||||
<div class="help-row"><kbd>Ctrl</kbd><kbd>?</kbd><span>Show this help</span></div>
|
||||
<div class="help-row"><kbd>Esc</kbd><span>Close panels and dropdowns</span></div>
|
||||
</div>
|
||||
<div class="help-group">
|
||||
<div class="help-group-label">Files</div>
|
||||
<div class="help-row"><span class="help-drag">Drag & drop</span><span>Attach files or images</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /#app -->
|
||||
<!-- Markdown renderer -->
|
||||
<script src="marked.min.js"></script>
|
||||
<script src="highlight.min.js"></script>
|
||||
<script src="pdf.min.js"></script>
|
||||
<script>
|
||||
if (typeof pdfjsLib !== 'undefined') {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc =
|
||||
chrome.runtime.getURL('pdf.worker.min.js');
|
||||
}
|
||||
</script>
|
||||
<script src="sidepanel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1445
-507
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user