仰望星辰工作室

clearlove2.1

clearlove2.1/ plugins/ai-polish/polish.html 4.7 KB · 126 行 原始文件
1<!-- AI 语句美化(应用型插件 ai-polish 静态注入;仅在发帖页生效) -->
2<script>
3(function () {
4 var form = document.getElementById('compose-form');
5 if (!form) return;
6 var ta = form.querySelector('textarea[name="content"]');
7 if (!ta) return;
8
9 var API = '/x/ai-polish';
10 var state = { label: 'AI 美化', maxChars: 600, busy: false, lastText: '' };
11
12 // 探测插件是否可用(未接入 AI / 需登录时按钮不出现)
13 fetch(API + '/config', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
14 .then(function (r) { return r.json(); })
15 .then(function (cfg) {
16 if (!cfg || !cfg.ok || !cfg.available) return;
17 state.label = cfg.label || state.label;
18 state.maxChars = cfg.max_chars || state.maxChars;
19 build();
20 })
21 .catch(function () { });
22
23 function build() {
24 var bar = document.createElement('div');
25 bar.className = 'ai-polish-bar';
26 bar.style.cssText = 'display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:8px';
27
28 var btn = document.createElement('button');
29 btn.type = 'button';
30 btn.className = 'btn btn-ghost ai-polish-btn';
31 btn.style.cssText = 'display:inline-flex;align-items:center;gap:6px';
32 var icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
33 icon.setAttribute('viewBox', '0 0 24 24');
34 icon.setAttribute('width', '16');
35 icon.setAttribute('height', '16');
36 icon.setAttribute('fill', 'none');
37 icon.setAttribute('stroke', 'currentColor');
38 icon.setAttribute('stroke-width', '2');
39 var p1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
40 p1.setAttribute('d', 'M12 3l1.8 4.6L18.5 9l-4.7 1.4L12 15l-1.8-4.6L5.5 9l4.7-1.4z');
41 var p2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
42 p2.setAttribute('d', 'M18.5 15.5l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8z');
43 icon.appendChild(p1);
44 icon.appendChild(p2);
45 var label = document.createElement('span');
46 label.textContent = state.label;
47 btn.appendChild(icon);
48 btn.appendChild(label);
49
50 var tip = document.createElement('span');
51 tip.style.cssText = 'font-size:12px';
52
53 var undo = document.createElement('button');
54 undo.type = 'button';
55 undo.className = 'mini-btn';
56 undo.textContent = '撤销';
57 undo.hidden = true;
58
59 bar.appendChild(btn);
60 bar.appendChild(tip);
61 bar.appendChild(undo);
62 // 插到「内容」这一整块(label)之后独立成行;若直接插在 textarea 后会落在 label 内受其排版影响
63 var anchor = ta.closest('label') || ta;
64 anchor.parentNode.insertBefore(bar, anchor.nextSibling);
65
66 btn.addEventListener('click', function () { run(btn, label, tip, undo); });
67 undo.addEventListener('click', function () {
68 ta.value = state.lastText;
69 ta.dispatchEvent(new Event('input', { bubbles: true }));
70 undo.hidden = true;
71 say(tip, '已恢复原文', false);
72 });
73 }
74
75 function run(btn, label, tip, undo) {
76 if (state.busy) return;
77 var text = (ta.value || '').trim();
78 if (text.length < 2) { say(tip, '请先输入内容', true); return; }
79 if (text.length > state.maxChars) { say(tip, '内容超过 ' + state.maxChars + ' 字,请分段美化', true); return; }
80
81 state.busy = true;
82 btn.disabled = true;
83 label.textContent = '美化中…';
84 tip.textContent = '';
85 undo.hidden = true;
86
87 request('POST', API + '/polish', { text: text })
88 .then(function (r) { return r.json(); })
89 .then(function (res) {
90 if (!res || !res.ok) { say(tip, (res && res.msg) || '美化失败,请稍后再试', true); return; }
91 state.lastText = ta.value;
92 ta.value = res.text;
93 ta.dispatchEvent(new Event('input', { bubbles: true }));
94 undo.hidden = false;
95 say(tip, '已完成美化', false);
96 })
97 .catch(function () { say(tip, '网络异常,请稍后再试', true); })
98 .then(function () {
99 state.busy = false;
100 btn.disabled = false;
101 label.textContent = state.label;
102 });
103 }
104
105 // 不依赖 app.js 的加载时机:直接带 CSRF 头请求
106 function request(method, url, body) {
107 var csrf = (document.querySelector('meta[name="csrf"]') || {}).content || '';
108 var opts = {
109 method: method,
110 headers: { 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' }
111 };
112 if (body !== undefined) {
113 opts.headers['Content-Type'] = 'application/json';
114 opts.body = JSON.stringify(body);
115 }
116 return fetch(url, opts);
117 }
118
119 function say(el, msg, isErr) {
120 el.textContent = msg;
121 el.style.color = isErr ? 'var(--clv-danger, #d9534f)' : 'var(--clv-muted, #8a8f9c)';
122 clearTimeout(el._t);
123 el._t = setTimeout(function () { el.textContent = ''; }, 4000);
124 }
125})();
126</script>