Compare commits
3 Commits
b5e5b302f2
...
31c99e7479
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31c99e7479 | ||
|
|
f4af63afec | ||
|
|
31b1de1b0d |
@@ -40,14 +40,6 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-web-
|
${{ runner.os }}-node-web-
|
||||||
|
|
||||||
- name: Cache Node modules (extension)
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: extension/node_modules
|
|
||||||
key: ${{ runner.os }}-node-ext-${{ hashFiles('extension/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-ext-
|
|
||||||
|
|
||||||
- name: Download Go dependencies
|
- name: Download Go dependencies
|
||||||
run: go mod download
|
run: go mod download
|
||||||
|
|
||||||
@@ -62,7 +54,6 @@ jobs:
|
|||||||
cd extension
|
cd extension
|
||||||
npm ci
|
npm ci
|
||||||
npx wxt zip
|
npx wxt zip
|
||||||
npx wxt zip --browser firefox
|
|
||||||
mkdir -p ../dist
|
mkdir -p ../dist
|
||||||
mv .output/muyue-extension-*.zip ../dist/
|
mv .output/muyue-extension-*.zip ../dist/
|
||||||
|
|
||||||
|
|||||||
@@ -40,14 +40,6 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-web-
|
${{ runner.os }}-node-web-
|
||||||
|
|
||||||
- name: Cache Node modules (extension)
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: extension/node_modules
|
|
||||||
key: ${{ runner.os }}-node-ext-${{ hashFiles('extension/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-ext-
|
|
||||||
|
|
||||||
- name: Download dependencies
|
- name: Download dependencies
|
||||||
run: go mod download
|
run: go mod download
|
||||||
|
|
||||||
@@ -62,7 +54,6 @@ jobs:
|
|||||||
cd extension
|
cd extension
|
||||||
npm ci
|
npm ci
|
||||||
npx wxt zip
|
npx wxt zip
|
||||||
npx wxt zip --browser firefox
|
|
||||||
mkdir -p ../dist
|
mkdir -p ../dist
|
||||||
mv .output/muyue-extension-*.zip ../dist/
|
mv .output/muyue-extension-*.zip ../dist/
|
||||||
|
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -9,7 +9,7 @@ WEB_DIR = web
|
|||||||
|
|
||||||
EXT_DIR = extension
|
EXT_DIR = extension
|
||||||
|
|
||||||
.PHONY: build install clean test test-short run scan fmt lint build-all deps vet frontend dev-desktop ext ext-chrome ext-firefox ext-zip
|
.PHONY: build install clean test test-short run scan fmt lint build-all deps vet frontend dev-desktop ext ext-zip
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
cd $(WEB_DIR) && $(NPM) ci && $(NPM) run build
|
cd $(WEB_DIR) && $(NPM) ci && $(NPM) run build
|
||||||
@@ -66,16 +66,10 @@ build-all: frontend
|
|||||||
GOOS=windows GOARCH=arm64 $(GO) build -o dist/$(BINARY)-windows-arm64.exe ./cmd/muyue/
|
GOOS=windows GOARCH=arm64 $(GO) build -o dist/$(BINARY)-windows-arm64.exe ./cmd/muyue/
|
||||||
|
|
||||||
ext:
|
ext:
|
||||||
cd $(EXT_DIR) && $(NPM) ci && $(NPM) run build && $(NPM) run build:firefox
|
|
||||||
|
|
||||||
ext-chrome:
|
|
||||||
cd $(EXT_DIR) && $(NPM) ci && $(NPM) run build
|
cd $(EXT_DIR) && $(NPM) ci && $(NPM) run build
|
||||||
|
|
||||||
ext-firefox:
|
|
||||||
cd $(EXT_DIR) && $(NPM) ci && $(NPM) run build:firefox
|
|
||||||
|
|
||||||
ext-zip:
|
ext-zip:
|
||||||
cd $(EXT_DIR) && $(NPM) ci && $(NPM) run zip && $(NPM) run zip:firefox
|
cd $(EXT_DIR) && $(NPM) ci && $(NPM) run zip
|
||||||
|
|
||||||
deps:
|
deps:
|
||||||
$(GO) mod tidy
|
$(GO) mod tidy
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "muyue-extension",
|
"name": "muyue-extension",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
"build": "wxt build",
|
"build": "wxt build",
|
||||||
"build:firefox": "wxt build --browser firefox",
|
"zip": "wxt zip"
|
||||||
"zip": "wxt zip",
|
|
||||||
"zip:firefox": "wxt zip --browser firefox"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wxt": "^0.20"
|
"wxt": "^0.20"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=320" />
|
<meta name="viewport" content="width=320" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="panel">
|
<div class="panel" style="width:320px">
|
||||||
<header>
|
<header>
|
||||||
<img src="/icon/32.png" alt="Muyue" />
|
<img src="/icon/32.png" alt="Muyue" />
|
||||||
<h1>Muyue</h1>
|
<h1>Muyue</h1>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
Open Dashboard
|
Open Dashboard
|
||||||
</a>
|
</a>
|
||||||
<button id="btn-sidepanel" class="btn">
|
<button id="btn-sidepanel" class="btn">
|
||||||
Open Side Panel
|
Open Chat Panel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<span>Muyue</span> browser extension v0.1.0
|
<span>Muyue</span> extension v0.9.0
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,50 +2,83 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=100%" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<header>
|
<header>
|
||||||
<img src="/icon/32.png" alt="Muyue" />
|
<img src="/icon/32.png" alt="Muyue" />
|
||||||
<h1>Muyue Side Panel</h1>
|
<h1>Muyue</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="status-card">
|
<nav class="tabs">
|
||||||
<div class="status-row">
|
<button class="tab active" data-tab="config">Configuration</button>
|
||||||
<span class="status-label">Server</span>
|
<button class="tab" data-tab="chat">Chat</button>
|
||||||
<span class="status-value" id="server-status">
|
</nav>
|
||||||
<span class="dot dot-yellow"></span>Checking…
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-row">
|
|
||||||
<span class="status-label">Active sessions</span>
|
|
||||||
<span class="status-value" id="session-count">—</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-row">
|
|
||||||
<span class="status-label">Console errors</span>
|
|
||||||
<span class="status-value" id="error-count">0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="sessions-list"></div>
|
<section id="tab-config" class="tab-content active">
|
||||||
|
<div class="status-card">
|
||||||
<div class="actions">
|
<div class="status-row">
|
||||||
<a id="btn-dashboard" href="#" class="btn btn-primary" target="_blank">
|
<span class="status-label">Server</span>
|
||||||
Open Dashboard
|
<span class="status-value" id="server-status">
|
||||||
</a>
|
<span class="dot dot-yellow"></span>Checking…
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
<div class="settings-section">
|
<div class="status-row">
|
||||||
<label>Server URL</label>
|
<span class="status-label">Active sessions</span>
|
||||||
<div class="input-row">
|
<span class="status-value" id="session-count">—</span>
|
||||||
<input type="text" id="server-url" placeholder="http://127.0.0.1:8080" />
|
</div>
|
||||||
<button id="btn-save-url">Save</button>
|
<div class="status-row">
|
||||||
|
<span class="status-label">Console errors</span>
|
||||||
|
<span class="status-value" id="error-count">0</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div id="sessions-list"></div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<a id="btn-dashboard" href="#" class="btn btn-primary" target="_blank">
|
||||||
|
Open Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<label>Server URL</label>
|
||||||
|
<div class="input-row">
|
||||||
|
<input type="text" id="server-url" placeholder="http://127.0.0.1:8080" />
|
||||||
|
<button id="btn-save-url">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="tab-chat" class="tab-content">
|
||||||
|
<div id="chat-offline" class="chat-offline">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--text-secondary)" stroke-width="1.5">
|
||||||
|
<circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>
|
||||||
|
</svg>
|
||||||
|
<span>Server offline</span>
|
||||||
|
</div>
|
||||||
|
<div id="chat-area" class="chat-area" style="display:none">
|
||||||
|
<div id="chat-feed" class="chat-feed"></div>
|
||||||
|
<div id="chat-streaming" class="chat-streaming" style="display:none"></div>
|
||||||
|
<div class="chat-input-row">
|
||||||
|
<textarea id="chat-input" placeholder="Send a message…" rows="1"></textarea>
|
||||||
|
<button id="chat-send" class="chat-send-btn">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="chat-stop" class="chat-stop-btn" style="display:none">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<span>Muyue</span> browser extension v0.1.0
|
<span>Muyue</span> extension v0.9.0
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
import '../../styles/panel.css';
|
import '../../styles/panel.css';
|
||||||
import { getServerUrl, setServerUrl, fetchSessions } from '../../lib/config';
|
import { getServerUrl, setServerUrl, fetchSessions, checkServerHealth } from '../../lib/config';
|
||||||
|
import { getChatHistory, sendChat, clearChat } from '../../lib/api';
|
||||||
|
|
||||||
const $serverStatus = document.getElementById('server-status');
|
const $ = (s) => document.querySelector(s);
|
||||||
const $sessionCount = document.getElementById('session-count');
|
const $$ = (s) => document.querySelectorAll(s);
|
||||||
const $errorCount = document.getElementById('error-count');
|
|
||||||
const $sessionsList = document.getElementById('sessions-list');
|
const $serverStatus = $('#server-status');
|
||||||
const $btnDashboard = document.getElementById('btn-dashboard');
|
const $sessionCount = $('#session-count');
|
||||||
const $serverUrl = document.getElementById('server-url');
|
const $errorCount = $('#error-count');
|
||||||
const $btnSaveUrl = document.getElementById('btn-save-url');
|
const $sessionsList = $('#sessions-list');
|
||||||
|
const $btnDashboard = $('#btn-dashboard');
|
||||||
|
const $serverUrl = $('#server-url');
|
||||||
|
const $btnSaveUrl = $('#btn-save-url');
|
||||||
|
const $chatOffline = $('#chat-offline');
|
||||||
|
const $chatArea = $('#chat-area');
|
||||||
|
const $chatFeed = $('#chat-feed');
|
||||||
|
const $chatStreaming = $('#chat-streaming');
|
||||||
|
const $chatInput = $('#chat-input');
|
||||||
|
const $chatSend = $('#chat-send');
|
||||||
|
const $chatStop = $('#chat-stop');
|
||||||
|
|
||||||
|
let serverOnline = false;
|
||||||
|
let messages = [];
|
||||||
|
let loading = false;
|
||||||
|
let abortController = null;
|
||||||
|
let currentStreamingEl = null;
|
||||||
|
|
||||||
function dot(color) {
|
function dot(color) {
|
||||||
return `<span class="dot dot-${color}"></span>`;
|
return `<span class="dot dot-${color}"></span>`;
|
||||||
@@ -18,7 +35,6 @@ function renderSessions(sessions) {
|
|||||||
$sessionsList.innerHTML = '';
|
$sessionsList.innerHTML = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sessionsList.innerHTML = `
|
$sessionsList.innerHTML = `
|
||||||
<div class="status-card" style="margin-top:12px">
|
<div class="status-card" style="margin-top:12px">
|
||||||
<div style="font-size:11px;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px">
|
<div style="font-size:11px;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px">
|
||||||
@@ -38,28 +54,360 @@ function renderSessions(sessions) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
function formatText(text) {
|
||||||
const url = await getServerUrl();
|
let html = text
|
||||||
$serverUrl.value = url;
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
$btnDashboard.href = url;
|
html = html
|
||||||
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
||||||
|
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
|
||||||
|
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
|
||||||
|
.replace(/^# (.+)$/gm, '<h2>$1</h2>')
|
||||||
|
.replace(/^\s*[-*] (.+)$/gm, '<div class="chat-bullet">• $1</div>')
|
||||||
|
.replace(/^\s*(\d+)[.)] (.+)$/gm, '<div class="chat-step"><span class="chat-step-num">$1</span> $2</div>')
|
||||||
|
.replace(/\n/g, '<br/>');
|
||||||
|
html = html
|
||||||
|
.replace(/<br\/>\s*<br\/>/g, '<br/>')
|
||||||
|
.replace(/<br\/>\s*(<h[234]|<div class="chat-)/g, '$1')
|
||||||
|
.replace(/(<\/h[234]|<\/div>)\s*<br\/>/g, '$1');
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
function renderContent(text) {
|
||||||
const sessions = await fetchSessions();
|
const parts = [];
|
||||||
$serverStatus.innerHTML = `${dot('green')} Online`;
|
const codeBlockRegex = /(```[\s\S]*?```)/g;
|
||||||
$sessionCount.textContent = sessions.length;
|
let match;
|
||||||
renderSessions(sessions);
|
let lastIndex = 0;
|
||||||
} catch {
|
while ((match = codeBlockRegex.exec(text)) !== null) {
|
||||||
$serverStatus.innerHTML = `${dot('red')} Offline`;
|
if (match.index > lastIndex) {
|
||||||
$sessionCount.textContent = '—';
|
parts.push({ type: 'text', content: text.slice(lastIndex, match.index) });
|
||||||
$sessionsList.innerHTML = '';
|
}
|
||||||
|
const full = match[1];
|
||||||
|
const firstNewline = full.indexOf('\n');
|
||||||
|
const lang = firstNewline > -1 ? full.slice(3, firstNewline).trim() : '';
|
||||||
|
const code = firstNewline > -1 ? full.slice(firstNewline + 1, -3) : full.slice(3, -3);
|
||||||
|
parts.push({ type: 'code', lang, content: code });
|
||||||
|
lastIndex = match.index + full.length;
|
||||||
|
}
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
parts.push({ type: 'text', content: text.slice(lastIndex) });
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMessageEl(msg) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = `chat-msg ${msg.role}`;
|
||||||
|
|
||||||
|
if (msg.role === 'system') {
|
||||||
|
el.innerHTML = `<div class="chat-system-dot"></div><div class="chat-system-text">${escapeHtml(msg.content)}</div>`;
|
||||||
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.sendMessage({ type: 'get_state' }, (state) => {
|
const isUser = msg.role === 'user';
|
||||||
if (chrome.runtime.lastError || !state) return;
|
const avatar = isUser ? '★' : '◆';
|
||||||
$errorCount.textContent = state.errorCount || 0;
|
const label = isUser ? 'CDT' : 'GEN';
|
||||||
|
|
||||||
|
let displayContent = msg.content;
|
||||||
|
let parsedToolCalls = null;
|
||||||
|
let parsedSegments = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(msg.content);
|
||||||
|
if (parsed && Array.isArray(parsed.segments)) {
|
||||||
|
parsedSegments = parsed.segments;
|
||||||
|
displayContent = parsed.content || '';
|
||||||
|
} else if (parsed && Array.isArray(parsed.tool_calls)) {
|
||||||
|
parsedToolCalls = parsed.tool_calls;
|
||||||
|
displayContent = parsed.content || '';
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const cleanContent = displayContent.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '');
|
||||||
|
|
||||||
|
let bodyHtml = '';
|
||||||
|
if (parsedSegments) {
|
||||||
|
bodyHtml = parsedSegments.map((seg) => {
|
||||||
|
if (seg.type === 'text' && seg.content) {
|
||||||
|
const c = seg.content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '');
|
||||||
|
if (!c) return '';
|
||||||
|
return renderContent(c).map((p) => {
|
||||||
|
if (p.type === 'code') {
|
||||||
|
return `<div class="chat-code-block"><div class="chat-code-header"><span class="chat-code-lang">${p.lang || ''}</span><button class="chat-copy-btn" data-code="${encodeURIComponent(p.content)}">Copy</button></div><pre><code>${escapeHtml(p.content)}</code></pre></div>`;
|
||||||
|
}
|
||||||
|
return `<span>${formatText(p.content)}</span>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
if (seg.type === 'tool') {
|
||||||
|
const name = seg.call?.name || 'tool';
|
||||||
|
const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', list_files: '📁', search_files: '🔍', web_fetch: '🌐' }[name] || '🔧';
|
||||||
|
const done = seg.result;
|
||||||
|
const isErr = done && done.is_error;
|
||||||
|
const preview = (() => {
|
||||||
|
try {
|
||||||
|
const args = typeof seg.call.args === 'string' ? JSON.parse(seg.call.args) : seg.call.args;
|
||||||
|
return args.command || args.task || args.path || args.url || JSON.stringify(args).slice(0, 60);
|
||||||
|
} catch { return ''; }
|
||||||
|
})();
|
||||||
|
const resultText = done ? (done.content || '').slice(0, 500) : '';
|
||||||
|
return `<div class="chat-tool ${done ? 'done' : 'running'} ${isErr ? 'error' : ''}"><div class="chat-tool-header"><span class="chat-tool-icon">${icon}</span><span>${name}</span>${done ? `<span class="chat-tool-status ${isErr ? 'err' : 'ok'}">${isErr ? '✗' : '✓'}</span>` : '<span class="chat-dots"><span></span><span></span><span></span></span>'}</div>${preview ? `<div class="chat-tool-args">${escapeHtml(preview)}</div>` : ''}${resultText ? `<pre class="chat-tool-result">${escapeHtml(resultText)}</pre>` : ''}</div>`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}).join('');
|
||||||
|
} else {
|
||||||
|
if (cleanContent) {
|
||||||
|
bodyHtml = renderContent(cleanContent).map((p) => {
|
||||||
|
if (p.type === 'code') {
|
||||||
|
return `<div class="chat-code-block"><div class="chat-code-header"><span class="chat-code-lang">${p.lang || ''}</span><button class="chat-copy-btn" data-code="${encodeURIComponent(p.content)}">Copy</button></div><pre><code>${escapeHtml(p.content)}</code></pre></div>`;
|
||||||
|
}
|
||||||
|
return `<span>${formatText(p.content)}</span>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
if (parsedToolCalls && parsedToolCalls.length > 0) {
|
||||||
|
bodyHtml = parsedToolCalls.map((tc) => {
|
||||||
|
const name = tc.name || 'tool';
|
||||||
|
const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', web_fetch: '🌐' }[name] || '🔧';
|
||||||
|
return `<div class="chat-tool done"><div class="chat-tool-header"><span class="chat-tool-icon">${icon}</span><span>${name}</span><span class="chat-tool-status ok">✓</span></div></div>`;
|
||||||
|
}).join('') + bodyHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bodyHtml) bodyHtml = '<span class="chat-dots"><span></span><span></span><span></span></span>';
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="chat-avatar ${isUser ? 'user' : 'ai'}">${avatar}</div>
|
||||||
|
<div class="chat-body">
|
||||||
|
<div class="chat-header"><span class="chat-badge" style="color:${isUser ? '#FFD740' : '#FF9100'};border-color:${isUser ? '#FFD740' : '#FF9100'}">${label}</span></div>
|
||||||
|
<div class="chat-content">${bodyHtml}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMessages() {
|
||||||
|
$chatFeed.innerHTML = '';
|
||||||
|
messages.forEach((msg) => {
|
||||||
|
$chatFeed.appendChild(createMessageEl(msg));
|
||||||
|
});
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
$chatFeed.scrollTop = $chatFeed.scrollHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
$$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tabName));
|
||||||
|
$$('.tab-content').forEach((s) => s.classList.toggle('active', s.id === `tab-${tabName}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChatVisibility() {
|
||||||
|
if (serverOnline) {
|
||||||
|
$chatOffline.style.display = 'none';
|
||||||
|
$chatArea.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
$chatOffline.style.display = 'flex';
|
||||||
|
$chatArea.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChatHistory() {
|
||||||
|
try {
|
||||||
|
const data = await getChatHistory();
|
||||||
|
if (data.messages && data.messages.length > 0) {
|
||||||
|
messages = data.messages;
|
||||||
|
} else {
|
||||||
|
messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }];
|
||||||
|
}
|
||||||
|
renderMessages();
|
||||||
|
} catch {
|
||||||
|
messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }];
|
||||||
|
renderMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSend() {
|
||||||
|
const text = $chatInput.value.trim();
|
||||||
|
if (!text || loading) return;
|
||||||
|
|
||||||
|
if (text === '/clear') {
|
||||||
|
try { await clearChat(); } catch {}
|
||||||
|
messages = [{ id: 'clear-' + Date.now(), role: 'system', content: 'Conversation cleared.' }];
|
||||||
|
renderMessages();
|
||||||
|
$chatInput.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chatInput.value = '';
|
||||||
|
$chatInput.style.height = 'auto';
|
||||||
|
|
||||||
|
const userMsg = { id: Date.now().toString(), role: 'user', content: text };
|
||||||
|
messages.push(userMsg);
|
||||||
|
$chatFeed.appendChild(createMessageEl(userMsg));
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
$chatSend.style.display = 'none';
|
||||||
|
$chatStop.style.display = 'flex';
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
abortController = controller;
|
||||||
|
|
||||||
|
let segments = [];
|
||||||
|
let thinking = '';
|
||||||
|
let textStartIdx = 0;
|
||||||
|
let streamText = '';
|
||||||
|
|
||||||
|
const updateLastText = (text) => {
|
||||||
|
if (!text) return;
|
||||||
|
const last = segments.length > 0 ? segments[segments.length - 1] : null;
|
||||||
|
if (last && last.type === 'text') {
|
||||||
|
last.content = text;
|
||||||
|
} else {
|
||||||
|
segments.push({ type: 'text', content: text });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
currentStreamingEl = document.createElement('div');
|
||||||
|
currentStreamingEl.className = 'chat-msg assistant streaming';
|
||||||
|
$chatFeed.appendChild(currentStreamingEl);
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const finalContent = await sendChat(text, true, (partial, event) => {
|
||||||
|
if (event && (event.thinking !== undefined || event.thinking_start || event.thinking_end)) {
|
||||||
|
if (event.thinking !== undefined) thinking += event.thinking;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event && event.tool_call) {
|
||||||
|
updateLastText(partial.slice(textStartIdx));
|
||||||
|
textStartIdx = partial.length;
|
||||||
|
segments.push({ type: 'tool', call: event.tool_call, result: null });
|
||||||
|
} else if (event && event.tool_result) {
|
||||||
|
const segIdx = segments.findIndex((s) => s.type === 'tool' && s.call && s.call.tool_call_id === event.tool_result.tool_call_id);
|
||||||
|
if (segIdx >= 0) segments[segIdx].result = event.tool_result;
|
||||||
|
} else {
|
||||||
|
updateLastText(partial.slice(textStartIdx));
|
||||||
|
}
|
||||||
|
streamText = partial;
|
||||||
|
|
||||||
|
const allText = segments.filter((s) => s.type === 'text').map((s) => s.content).join('');
|
||||||
|
const toolSegs = segments.filter((s) => s.type === 'tool');
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
if (thinking) {
|
||||||
|
html += `<div class="chat-thinking"><span class="chat-thinking-icon">⏱</span> Thinking…</div>`;
|
||||||
|
}
|
||||||
|
segments.forEach((seg) => {
|
||||||
|
if (seg.type === 'text' && seg.content) {
|
||||||
|
const c = seg.content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '');
|
||||||
|
if (c) html += `<div class="chat-content">${formatText(c)}</div>`;
|
||||||
|
}
|
||||||
|
if (seg.type === 'tool') {
|
||||||
|
const name = seg.call?.name || 'tool';
|
||||||
|
const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', web_fetch: '🌐' }[name] || '🔧';
|
||||||
|
const done = seg.result;
|
||||||
|
const isErr = done && done.is_error;
|
||||||
|
const preview = (() => {
|
||||||
|
try {
|
||||||
|
const args = typeof seg.call.args === 'string' ? JSON.parse(seg.call.args) : seg.call.args;
|
||||||
|
return args.command || args.task || args.path || JSON.stringify(args).slice(0, 60);
|
||||||
|
} catch { return ''; }
|
||||||
|
})();
|
||||||
|
html += `<div class="chat-tool ${done ? 'done' : 'running'} ${isErr ? 'error' : ''}"><div class="chat-tool-header"><span class="chat-tool-icon">${icon}</span><span>${name}</span>${done ? `<span class="chat-tool-status ${isErr ? 'err' : 'ok'}">${isErr ? '✗' : '✓'}</span>` : '<span class="chat-dots"><span></span><span></span><span></span></span>'}</div>${preview ? `<div class="chat-tool-args">${escapeHtml(preview)}</div>` : ''}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
html = '<span class="chat-dots"><span></span><span></span><span></span></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStreamingEl.innerHTML = `
|
||||||
|
<div class="chat-avatar ai">◆</div>
|
||||||
|
<div class="chat-body">
|
||||||
|
<div class="chat-header"><span class="chat-badge" style="color:#FF9100;border-color:#FF9100">GEN</span></div>
|
||||||
|
${html}
|
||||||
|
<span class="chat-cursor"></span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
scrollToBottom();
|
||||||
|
}, controller.signal);
|
||||||
|
|
||||||
|
if (currentStreamingEl && currentStreamingEl.parentNode) {
|
||||||
|
currentStreamingEl.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const allText = segments.filter((s) => s.type === 'text').map((s) => s.content).join('');
|
||||||
|
const toolSegs = segments.filter((s) => s.type === 'tool');
|
||||||
|
const aiMsg = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: toolSegs.length > 0 ? JSON.stringify({
|
||||||
|
segments: segments.map((s) => s.type === 'text'
|
||||||
|
? { type: 'text', content: s.content }
|
||||||
|
: { type: 'tool', call: s.call, result: s.result ? { content: s.result.content || '', is_error: s.result.is_error || false, tool_call_id: s.call?.tool_call_id } : null }),
|
||||||
|
content: allText,
|
||||||
|
}) : (allText || finalContent),
|
||||||
|
};
|
||||||
|
messages.push(aiMsg);
|
||||||
|
$chatFeed.appendChild(createMessageEl(aiMsg));
|
||||||
|
scrollToBottom();
|
||||||
|
} catch (err) {
|
||||||
|
if (currentStreamingEl && currentStreamingEl.parentNode) {
|
||||||
|
currentStreamingEl.remove();
|
||||||
|
}
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
const errMsg = { id: (Date.now() + 1).toString(), role: 'system', content: `Error: ${err.message}` };
|
||||||
|
messages.push(errMsg);
|
||||||
|
$chatFeed.appendChild(createMessageEl(errMsg));
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
abortController = null;
|
||||||
|
currentStreamingEl = null;
|
||||||
|
$chatSend.style.display = 'flex';
|
||||||
|
$chatStop.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$$('.tab').forEach((tab) => {
|
||||||
|
tab.addEventListener('click', () => switchTab(tab.dataset.tab));
|
||||||
|
});
|
||||||
|
|
||||||
|
$chatInput.addEventListener('input', () => {
|
||||||
|
$chatInput.style.height = 'auto';
|
||||||
|
$chatInput.style.height = Math.min($chatInput.scrollHeight, 100) + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
$chatInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$chatSend.addEventListener('click', handleSend);
|
||||||
|
$chatStop.addEventListener('click', () => {
|
||||||
|
if (abortController) abortController.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
$chatFeed.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('.chat-copy-btn');
|
||||||
|
if (btn) {
|
||||||
|
navigator.clipboard.writeText(decodeURIComponent(btn.dataset.code));
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.textContent = 'Copied!';
|
||||||
|
setTimeout(() => { btn.textContent = orig; }, 1200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$btnSaveUrl.addEventListener('click', async () => {
|
$btnSaveUrl.addEventListener('click', async () => {
|
||||||
const url = $serverUrl.value.trim().replace(/\/$/, '');
|
const url = $serverUrl.value.trim().replace(/\/$/, '');
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -68,5 +416,32 @@ $btnSaveUrl.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
const url = await getServerUrl();
|
||||||
|
$serverUrl.value = url;
|
||||||
|
$btnDashboard.href = url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessions = await fetchSessions();
|
||||||
|
serverOnline = true;
|
||||||
|
$serverStatus.innerHTML = `${dot('green')} Online`;
|
||||||
|
$sessionCount.textContent = sessions.length;
|
||||||
|
renderSessions(sessions);
|
||||||
|
} catch {
|
||||||
|
serverOnline = false;
|
||||||
|
$serverStatus.innerHTML = `${dot('red')} Offline`;
|
||||||
|
$sessionCount.textContent = '—';
|
||||||
|
$sessionsList.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChatVisibility();
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({ type: 'get_state' }, (state) => {
|
||||||
|
if (chrome.runtime.lastError || !state) return;
|
||||||
|
$errorCount.textContent = state.errorCount || 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
setInterval(refresh, 5000);
|
loadChatHistory();
|
||||||
|
setInterval(refresh, 10000);
|
||||||
|
|||||||
77
extension/src/lib/api.js
Normal file
77
extension/src/lib/api.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { getServerUrl } from './config';
|
||||||
|
|
||||||
|
async function request(path, options = {}) {
|
||||||
|
const base = await getServerUrl();
|
||||||
|
const res = await fetch(`${base}/api${path}`, {
|
||||||
|
...options,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error(err.error || res.statusText);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChatHistory() {
|
||||||
|
return request('/chat/history');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearChat() {
|
||||||
|
return request('/chat/clear', { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function summarizeChat() {
|
||||||
|
return request('/chat/summarize', { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendChat(message, stream = true, onChunk, signal) {
|
||||||
|
const base = await getServerUrl();
|
||||||
|
|
||||||
|
if (!stream) {
|
||||||
|
return request('/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ message, stream: false }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fetch(`${base}/api/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ message, stream: true }),
|
||||||
|
signal,
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
reject(new Error(err.error || res.statusText));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let full = '';
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
const text = decoder.decode(value, { stream: true });
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
if (!line.startsWith('data: ')) continue;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(line.slice(6));
|
||||||
|
if (data.error) { reject(new Error(data.error)); return; }
|
||||||
|
if (data.done) { resolve(full); return; }
|
||||||
|
if (data.content) {
|
||||||
|
full += data.content;
|
||||||
|
if (onChunk) onChunk(full, data);
|
||||||
|
} else if (data.thinking !== undefined || data.thinking_end) {
|
||||||
|
if (onChunk) onChunk(full, data);
|
||||||
|
} else if (data.tool_call || data.tool_result) {
|
||||||
|
if (onChunk) onChunk(full, data);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(full);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -30,17 +30,19 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
width: 320px;
|
display: flex;
|
||||||
padding: 16px;
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
.panel > header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 16px;
|
padding: 12px 16px;
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
header img {
|
header img {
|
||||||
@@ -54,6 +56,47 @@ header h1 {
|
|||||||
letter-spacing: -0.3px;
|
letter-spacing: -0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.status-card {
|
.status-card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -189,12 +232,13 @@ header h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 12px;
|
margin-top: auto;
|
||||||
padding-top: 10px;
|
padding: 10px 16px;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer span {
|
.footer span {
|
||||||
@@ -209,3 +253,351 @@ header h1 {
|
|||||||
.loading {
|
.loading {
|
||||||
animation: pulse 1.5s ease-in-out infinite;
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-offline {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-feed {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-msg {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-msg.system {
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-system-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-system-text {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-avatar {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-avatar.user {
|
||||||
|
background: rgba(255, 215, 64, 0.15);
|
||||||
|
color: #FFD740;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-avatar.ai {
|
||||||
|
background: rgba(255, 145, 0, 0.15);
|
||||||
|
color: #FF9100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content h2 { font-size: 14px; margin: 8px 0 4px; }
|
||||||
|
.chat-content h3 { font-size: 13px; margin: 6px 0 3px; }
|
||||||
|
.chat-content h4 { font-size: 12px; margin: 4px 0 2px; }
|
||||||
|
.chat-content strong { color: #fff; }
|
||||||
|
.chat-bullet, .chat-step {
|
||||||
|
padding-left: 4px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
.chat-step-num {
|
||||||
|
display: inline-block;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-dim);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 18px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.inline-code {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-code-block {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 6px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-code-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-code-lang {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-copy-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-copy-btn:hover {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-code-block pre {
|
||||||
|
padding: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-code-block code {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-tool {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin: 6px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-tool.error {
|
||||||
|
border-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-tool-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-tool-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-tool-status {
|
||||||
|
margin-left: auto;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-tool-status.ok { color: var(--green); }
|
||||||
|
.chat-tool-status.err { color: var(--red); }
|
||||||
|
|
||||||
|
.chat-tool-args {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-tool-result {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-icon {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chatDotPulse {
|
||||||
|
0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
|
||||||
|
40% { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-dots {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-dots span {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-secondary);
|
||||||
|
animation: chatDotPulse 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.chat-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes cursorBlink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2px;
|
||||||
|
height: 14px;
|
||||||
|
background: var(--accent);
|
||||||
|
margin-left: 2px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
animation: cursorBlink 0.8s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 0 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-row textarea {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 13px;
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
max-height: 100px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-row textarea:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-send-btn, .chat-stop-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-send-btn:hover, .chat-stop-btn:hover {
|
||||||
|
box-shadow: 0 0 12px var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-stop-btn {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-color: var(--border);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-msg.user .chat-content {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 0 8px 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-msg.assistant .chat-content {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,19 +2,17 @@ import { defineConfig } from 'wxt';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
srcDir: 'src',
|
srcDir: 'src',
|
||||||
suppressWarnings: {
|
manifest: {
|
||||||
firefoxDataCollection: true,
|
|
||||||
},
|
|
||||||
manifest: ({ browser }) => ({
|
|
||||||
name: 'Muyue',
|
name: 'Muyue',
|
||||||
description: 'AI-powered browser testing & automation — connected to your Muyue desktop app',
|
description: 'AI-powered browser testing & automation — connected to your Muyue desktop app',
|
||||||
permissions: [
|
permissions: [
|
||||||
'storage',
|
'storage',
|
||||||
'activeTab',
|
'activeTab',
|
||||||
'tabs',
|
'tabs',
|
||||||
...(browser === 'chrome' ? ['sidePanel'] : []),
|
'sidePanel',
|
||||||
'scripting',
|
'scripting',
|
||||||
'notifications',
|
'notifications',
|
||||||
|
'alarms',
|
||||||
],
|
],
|
||||||
host_permissions: ['http://127.0.0.1:*/*', 'http://localhost:*/*'],
|
host_permissions: ['http://127.0.0.1:*/*', 'http://localhost:*/*'],
|
||||||
action: {
|
action: {
|
||||||
@@ -26,5 +24,5 @@ export default defineConfig({
|
|||||||
side_panel: {
|
side_panel: {
|
||||||
default_path: 'sidepanel.html',
|
default_path: 'sidepanel.html',
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
Name = "muyue"
|
Name = "muyue"
|
||||||
Version = "0.8.0"
|
Version = "0.9.0"
|
||||||
Author = "La Légion de Muyue"
|
Author = "La Légion de Muyue"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user