Compare commits
4 Commits
v0.9.5-bet
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7c086cec | ||
|
|
b40bc291ba | ||
|
|
1216c80118 | ||
|
|
eda493856a |
59
CHANGELOG.md
59
CHANGELOG.md
@@ -4,6 +4,65 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## v0.9.6
|
||||
|
||||
### Changes since v0.9.5
|
||||
|
||||
- fix(ext): use <all_urls> host_permissions to allow injection on any page (b40bc29)
|
||||
- chore: bump version to v0.9.6 (1216c80)
|
||||
- feat(ext): add session inject button, auto-inject toggle, and live Studio feed in sidepanel, bump v0.9.6 (eda4938)
|
||||
|
||||
### Downloads
|
||||
|
||||
| Platform | File |
|
||||
|----------|------|
|
||||
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-linux-amd64.tar.gz) |
|
||||
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-linux-arm64.tar.gz) |
|
||||
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-darwin-amd64.tar.gz) |
|
||||
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-darwin-arm64.tar.gz) |
|
||||
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-windows-amd64.zip) |
|
||||
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-windows-arm64.zip) |
|
||||
|
||||
The binary includes both CLI and Desktop modes.
|
||||
Run `muyue` for TUI, `muyue desktop` for web UI.
|
||||
|
||||
### Install
|
||||
|
||||
**Linux (x86_64)**
|
||||
```bash
|
||||
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-linux-amd64.tar.gz | tar xz
|
||||
chmod +x muyue-linux-amd64
|
||||
sudo mv muyue-linux-amd64 /usr/local/bin/muyue
|
||||
```
|
||||
|
||||
**macOS (Apple Silicon)**
|
||||
```bash
|
||||
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-darwin-arm64.tar.gz | tar xz
|
||||
chmod +x muyue-darwin-arm64
|
||||
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
|
||||
```
|
||||
|
||||
**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer + commande `muyue` dans la session courante :
|
||||
```powershell
|
||||
Get-Process muyue, muyue-windows-amd64 -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 500
|
||||
$dest = "$env:LOCALAPPDATA\Muyue"; New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||||
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip"
|
||||
Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force
|
||||
& "$dest\muyue-windows-amd64.exe" install-shortcuts
|
||||
$env:Path += ";$dest"
|
||||
```
|
||||
|
||||
Le 1ʳᵉ ligne tue toute instance Muyue déjà lancée (sinon Windows refuse d'écraser le `.exe` verrouillé et l'install échoue silencieusement). Si vous mettez à jour depuis une version précédente, c'est obligatoire.
|
||||
|
||||
|
||||
## v0.9.6
|
||||
|
||||
### Changes since v0.9.5
|
||||
|
||||
- feat(ext): add session inject button and auto-inject toggle in sidepanel Configuration tab
|
||||
- feat(ext): replace interactive Chat tab with live read-only Studio feed mirror
|
||||
- chore(ext): bump extension to v0.9.6
|
||||
|
||||
## v0.9.5
|
||||
|
||||
### Changes since v0.9.4
|
||||
|
||||
4
extension/package-lock.json
generated
4
extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "muyue-extension",
|
||||
"version": "0.1.0",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "muyue-extension",
|
||||
"version": "0.1.0",
|
||||
"version": "0.9.0",
|
||||
"dependencies": {
|
||||
"wxt": "^0.20"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "muyue-extension",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,7 +3,9 @@ import { dispatch } from '../lib/page-rpc';
|
||||
export default defineContentScript({
|
||||
matches: ['http://*/*', 'https://*/*'],
|
||||
runAt: 'document_idle',
|
||||
main() {
|
||||
async main() {
|
||||
const autoInjectResult = await chrome.storage.local.get('muyue_auto_inject');
|
||||
if (!autoInjectResult.muyue_auto_inject) return;
|
||||
if (window.__muyueExtension) return;
|
||||
window.__muyueExtension = true;
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>Muyue</span> extension v0.9.0
|
||||
<span>Muyue</span> extension v0.9.6
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -40,6 +40,26 @@
|
||||
<a id="btn-dashboard" href="#" class="btn btn-primary" target="_blank">
|
||||
Open Dashboard
|
||||
</a>
|
||||
<button id="btn-inject-session" class="btn">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
|
||||
</svg>
|
||||
Injecter le script de session
|
||||
</button>
|
||||
<div class="inject-status" id="inject-status" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-section">
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label">Auto-injection</span>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="toggle-auto-inject" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="toggle-desc">
|
||||
Injecte automatiquement le script de session dans chaque page visitée
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
@@ -59,28 +79,16 @@
|
||||
<span>Server offline</span>
|
||||
</div>
|
||||
<div id="chat-area" class="studio-feed-layout" style="display:none">
|
||||
<div id="chat-feed" class="studio-feed"></div>
|
||||
<div class="studio-input-area">
|
||||
<div class="studio-input-row">
|
||||
<textarea id="chat-input" placeholder="Envoyer un message…" rows="1"></textarea>
|
||||
<button id="chat-send" class="studio-send-btn">
|
||||
<svg width="18" height="18" 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="studio-stop-btn" style="display:none">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="studio-input-hint">/clear /help</div>
|
||||
<div class="chat-live-header">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="var(--accent)"><circle cx="12" cy="12" r="5"/></svg>
|
||||
<span>Live — Studio</span>
|
||||
</div>
|
||||
<div id="chat-feed" class="studio-feed"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="footer">
|
||||
<span>Muyue</span> extension v0.9.0
|
||||
<span>Muyue</span> extension v0.9.6
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import '../../styles/panel.css';
|
||||
import { getServerUrl, setServerUrl, fetchSessions, checkServerHealth } from '../../lib/config';
|
||||
import { getChatHistory, sendChat, clearChat } from '../../lib/api';
|
||||
import { getChatHistory } from '../../lib/api';
|
||||
|
||||
const $ = (s) => document.querySelector(s);
|
||||
const $$ = (s) => document.querySelectorAll(s);
|
||||
@@ -15,16 +15,13 @@ 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');
|
||||
const $btnInject = $('#btn-inject-session');
|
||||
const $injectStatus = $('#inject-status');
|
||||
const $toggleAutoInject = $('#toggle-auto-inject');
|
||||
|
||||
let serverOnline = false;
|
||||
let messages = [];
|
||||
let loading = false;
|
||||
let abortController = null;
|
||||
let currentStreamingEl = null;
|
||||
let lastMessageCount = -1;
|
||||
let pollInterval = null;
|
||||
|
||||
function dot(color) {
|
||||
return `<span class="dot dot-${color}"></span>`;
|
||||
@@ -54,6 +51,10 @@ function renderSessions(sessions) {
|
||||
`;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function formatText(text) {
|
||||
let html = text
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
@@ -95,10 +96,6 @@ function renderContent(text) {
|
||||
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}`;
|
||||
@@ -188,14 +185,6 @@ function createMessageEl(msg) {
|
||||
return el;
|
||||
}
|
||||
|
||||
function renderMessages() {
|
||||
$chatFeed.innerHTML = '';
|
||||
messages.forEach((msg) => {
|
||||
$chatFeed.appendChild(createMessageEl(msg));
|
||||
});
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
requestAnimationFrame(() => {
|
||||
$chatFeed.scrollTop = $chatFeed.scrollHeight;
|
||||
@@ -205,6 +194,9 @@ function scrollToBottom() {
|
||||
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}`));
|
||||
if (tabName === 'chat' && serverOnline) {
|
||||
pollStudio();
|
||||
}
|
||||
}
|
||||
|
||||
function updateChatVisibility() {
|
||||
@@ -217,187 +209,82 @@ function updateChatVisibility() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChatHistory() {
|
||||
async function pollStudio() {
|
||||
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.' }];
|
||||
const msgs = data.messages || [];
|
||||
if (msgs.length !== lastMessageCount) {
|
||||
lastMessageCount = msgs.length;
|
||||
$chatFeed.innerHTML = '';
|
||||
msgs.forEach((msg) => {
|
||||
$chatFeed.appendChild(createMessageEl(msg));
|
||||
});
|
||||
scrollToBottom();
|
||||
}
|
||||
renderMessages();
|
||||
} catch {
|
||||
messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }];
|
||||
renderMessages();
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function handleSend() {
|
||||
const text = $chatInput.value.trim();
|
||||
if (!text || loading) return;
|
||||
function injectSessionScript() {
|
||||
$injectStatus.style.display = 'block';
|
||||
$injectStatus.className = 'inject-status';
|
||||
$injectStatus.textContent = 'Injection en cours…';
|
||||
|
||||
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 });
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
if (!tabs || !tabs[0]) {
|
||||
$injectStatus.textContent = 'Erreur: aucun onglet actif';
|
||||
$injectStatus.classList.add('inject-error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
const tabId = tabs[0].id;
|
||||
|
||||
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;
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
func: () => {
|
||||
if (window.__muyueExtension) {
|
||||
return 'already_injected';
|
||||
}
|
||||
window.__muyueExtension = true;
|
||||
return 'fresh_inject';
|
||||
},
|
||||
}, (results) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
$injectStatus.textContent = 'Erreur: ' + chrome.runtime.lastError.message;
|
||||
$injectStatus.classList.add('inject-error');
|
||||
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;
|
||||
|
||||
const status = results?.[0]?.result;
|
||||
|
||||
if (status === 'already_injected') {
|
||||
$injectStatus.textContent = '✓ Script déjà injecté dans cette page';
|
||||
$injectStatus.classList.add('inject-success');
|
||||
} 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>';
|
||||
chrome.tabs.reload(tabId, {}, () => {
|
||||
$injectStatus.textContent = '✓ Page rechargée avec le script de session';
|
||||
$injectStatus.classList.add('inject-success');
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
setTimeout(() => {
|
||||
$injectStatus.style.display = 'none';
|
||||
}, 3000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (currentStreamingEl && currentStreamingEl.parentNode) {
|
||||
currentStreamingEl.remove();
|
||||
}
|
||||
async function loadAutoInjectSetting() {
|
||||
const result = await chrome.storage.local.get('muyue_auto_inject');
|
||||
$toggleAutoInject.checked = !!result.muyue_auto_inject;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
async function saveAutoInjectSetting(enabled) {
|
||||
await chrome.storage.local.set({ muyue_auto_inject: enabled });
|
||||
}
|
||||
|
||||
$$('.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) {
|
||||
@@ -408,6 +295,12 @@ $chatFeed.addEventListener('click', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
$btnInject.addEventListener('click', injectSessionScript);
|
||||
|
||||
$toggleAutoInject.addEventListener('change', () => {
|
||||
saveAutoInjectSetting($toggleAutoInject.checked);
|
||||
});
|
||||
|
||||
$btnSaveUrl.addEventListener('click', async () => {
|
||||
const url = $serverUrl.value.trim().replace(/\/$/, '');
|
||||
if (url) {
|
||||
@@ -442,6 +335,11 @@ async function refresh() {
|
||||
});
|
||||
}
|
||||
|
||||
loadAutoInjectSetting();
|
||||
refresh();
|
||||
loadChatHistory();
|
||||
setInterval(refresh, 10000);
|
||||
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
pollInterval = setInterval(() => {
|
||||
refresh();
|
||||
if (serverOnline) pollStudio();
|
||||
}, 3000);
|
||||
|
||||
@@ -704,3 +704,138 @@ header h1 { font-size: 16px; font-weight: 600; letter-spacing: -0.3px; color: va
|
||||
text-align: center;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* ── Inject Session ── */
|
||||
#btn-inject-session {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--accent-dim);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#btn-inject-session:hover {
|
||||
background: var(--accent-bg);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
#btn-inject-session svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inject-status {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-tertiary);
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.inject-status.inject-success {
|
||||
color: var(--success);
|
||||
background: rgba(0, 230, 118, 0.08);
|
||||
}
|
||||
|
||||
.inject-status.inject-error {
|
||||
color: var(--error);
|
||||
background: rgba(255, 23, 68, 0.08);
|
||||
}
|
||||
|
||||
/* ── Toggle Switch ── */
|
||||
.toggle-section {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toggle-desc {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 22px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toggle-slider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background: var(--text-tertiary);
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background: var(--accent-bg);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider::before {
|
||||
background: var(--accent);
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
/* ── Live Chat Header ── */
|
||||
.chat-live-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-live-header svg {
|
||||
animation: pulse-live 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-live {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default defineConfig({
|
||||
'notifications',
|
||||
'alarms',
|
||||
],
|
||||
host_permissions: ['http://127.0.0.1:*/*', 'http://localhost:*/*'],
|
||||
host_permissions: ['<all_urls>'],
|
||||
action: {
|
||||
default_icon: {
|
||||
16: 'icon/16.png',
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
const (
|
||||
Name = "muyue"
|
||||
Version = "0.9.5"
|
||||
Version = "0.9.6"
|
||||
Author = "La Légion de Muyue"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user