@waterplus-ai/waterbuddy 0.1.17 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/lib/client/index.js +466 -36
- package/lib/host/index.js +250 -23
- package/lib/host/llm-gateway.js +424 -0
- package/package.json +2 -1
- package/patches/cordis.patch.yml +67 -0
package/README.md
CHANGED
|
@@ -8,9 +8,18 @@ unchanged.
|
|
|
8
8
|
|
|
9
9
|
- WaterBuddy branding, favicon, and page title.
|
|
10
10
|
- Water-industry assistant persona for internal users.
|
|
11
|
-
-
|
|
11
|
+
- Login panel with two modes: WaterPlus "水务之窗" QR-code login and WaterPlus account login.
|
|
12
|
+
- Account registration (email verification code) and password reset inside the same panel.
|
|
12
13
|
- User profile and avatar display after login.
|
|
13
|
-
- Xiaojia MCP access with the current
|
|
14
|
+
- Xiaojia MCP access with the current session token attached by a local proxy.
|
|
15
|
+
|
|
16
|
+
## Sign-in modes
|
|
17
|
+
|
|
18
|
+
The sidebar user entry opens one panel with `扫码登录` and `账号登录` tabs. The account tab mirrors the
|
|
19
|
+
WaterPlus web sign-in contract: account and password login, `注册账号` with an emailed verification
|
|
20
|
+
code, and `忘记密码?` password reset. The client only talks to local `/api/waterbuddy/*` routes; the
|
|
21
|
+
Host performs every upstream call and keeps the token in process memory, so no credential or token
|
|
22
|
+
reaches the browser bundle.
|
|
14
23
|
|
|
15
24
|
## Install
|
|
16
25
|
|
package/lib/client/index.js
CHANGED
|
@@ -63,14 +63,363 @@ window.__ModuleLoader__.load({
|
|
|
63
63
|
: jsx('span', { style: { ...style, display: 'grid', placeItems: 'center', background: 'linear-gradient(135deg, #1d9bf0, #2563eb)', color: '#fff', fontSize: Math.max(14, size * 0.42), fontWeight: 700 }, children: user ? name.slice(0, 1) : '🔐' })
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function plainInputStyle() {
|
|
67
|
+
return { minWidth: 0, flex: '1 1 auto', height: 46, border: 0, outline: 'none', background: 'transparent', color: '#162033', fontSize: 14 }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fieldIcon(text) {
|
|
71
|
+
return jsx('span', { 'aria-hidden': 'true', style: { flex: '0 0 auto', width: 18, textAlign: 'center', color: '#9aa5b4', fontSize: 13 }, children: text })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function errorBox(message, onRetry) {
|
|
75
|
+
return jsxs('div', { role: 'alert', style: { display: 'flex', alignItems: 'flex-start', gap: 8, marginTop: 12, padding: '9px 12px', borderRadius: 10, background: '#fef2f2', border: '1px solid #fecaca', color: '#b91c1c', fontSize: 12.5, lineHeight: '18px', textAlign: 'left' }, children: [
|
|
76
|
+
jsx('span', { 'aria-hidden': 'true', style: { flex: '0 0 auto' }, children: '⚠' }),
|
|
77
|
+
jsx('span', { style: { minWidth: 0, flex: '1 1 auto', wordBreak: 'break-word' }, children: message }),
|
|
78
|
+
onRetry ? jsx('button', { type: 'button', onClick: onRetry, style: { flex: '0 0 auto', border: 0, background: 'transparent', color: '#b91c1c', fontSize: 12.5, fontWeight: 600, cursor: 'pointer', textDecoration: 'underline', padding: 0 }, children: '重试' }) : null,
|
|
79
|
+
] })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function primaryButtonStyle(disabled) {
|
|
83
|
+
return { width: '100%', height: 46, border: 0, borderRadius: 10, background: disabled ? '#9dc2e8' : '#1677d2', color: '#fff', fontSize: 15, fontWeight: 600, cursor: disabled ? 'not-allowed' : 'pointer', transition: 'background .15s ease' }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function linkButtonStyle() {
|
|
87
|
+
return { border: 0, background: 'transparent', color: '#1677d2', fontSize: 12.5, cursor: 'pointer', padding: 0 }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function agreementNotice() {
|
|
91
|
+
const linkStyle = { color: '#1677d2', textDecoration: 'none' }
|
|
92
|
+
return jsxs('div', { style: { display: 'flex', alignItems: 'flex-start', justifyContent: 'center', gap: 7, marginTop: 16, color: '#8a94a6', fontSize: 11.5, lineHeight: '17px', textAlign: 'left' }, children: [
|
|
93
|
+
jsx('input', { type: 'checkbox', checked: true, readOnly: true, tabIndex: -1, 'aria-label': '已阅读并同意服务协议和隐私政策', style: { flex: '0 0 auto', width: 13, height: 13, margin: '2px 0 0', accentColor: '#1677d2' } }),
|
|
94
|
+
jsxs('span', { style: { minWidth: 0 }, children: [
|
|
95
|
+
'已阅读并同意',
|
|
96
|
+
jsx('a', { href: 'https://ai.shuiwujia.com/agreement', target: '_blank', rel: 'noreferrer', style: linkStyle, children: '《服务协议》' }),
|
|
97
|
+
'和',
|
|
98
|
+
jsx('a', { href: 'https://ai.shuiwujia.com/privacy', target: '_blank', rel: 'noreferrer', style: linkStyle, children: '《隐私政策》' }),
|
|
99
|
+
] }),
|
|
100
|
+
] })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function passwordField(extra) {
|
|
104
|
+
return { display: 'flex', alignItems: 'center', gap: 8, height: 48, padding: '0 12px', border: '1px solid #e3e8ef', borderRadius: 10, background: '#f7f8fa', boxSizing: 'border-box', ...(extra || {}) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function plainInputStyle() {
|
|
108
|
+
return { minWidth: 0, flex: '1 1 auto', height: 46, border: 0, outline: 'none', background: 'transparent', color: '#162033', fontSize: 14 }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function fieldIcon(text) {
|
|
112
|
+
return jsx('span', { 'aria-hidden': 'true', style: { flex: '0 0 auto', width: 18, textAlign: 'center', color: '#9aa5b4', fontSize: 13 }, children: text })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function errorBox(message) {
|
|
116
|
+
return jsxs('div', { role: 'alert', style: { display: 'flex', alignItems: 'flex-start', gap: 8, padding: '9px 12px', borderRadius: 10, background: '#fef2f2', border: '1px solid #fecaca', color: '#b91c1c', fontSize: 12.5, lineHeight: '18px', textAlign: 'left' }, children: [
|
|
117
|
+
jsx('span', { 'aria-hidden': 'true', style: { flex: '0 0 auto' }, children: '⚠' }),
|
|
118
|
+
jsx('span', { style: { minWidth: 0, flex: '1 1 auto', wordBreak: 'break-word' }, children: message }),
|
|
119
|
+
] })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function primaryButtonStyle(disabled) {
|
|
123
|
+
return { width: '100%', height: 46, border: 0, borderRadius: 10, background: disabled ? '#9dc2e8' : '#1677d2', color: '#fff', fontSize: 15, fontWeight: 600, cursor: disabled ? 'not-allowed' : 'pointer', transition: 'background .15s ease' }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function linkButtonStyle() {
|
|
127
|
+
return { border: 0, background: 'transparent', color: '#1677d2', fontSize: 12.5, cursor: 'pointer', padding: 0 }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function agreementNotice() {
|
|
131
|
+
const linkStyle = { color: '#1677d2', textDecoration: 'none' }
|
|
132
|
+
return jsxs('div', { style: { display: 'flex', alignItems: 'flex-start', justifyContent: 'center', gap: 7, color: '#8a94a6', fontSize: 11.5, lineHeight: '17px', textAlign: 'left' }, children: [
|
|
133
|
+
jsx('input', { type: 'checkbox', checked: true, readOnly: true, tabIndex: -1, 'aria-label': '已阅读并同意服务协议和隐私政策', style: { flex: '0 0 auto', width: 13, height: 13, margin: '2px 0 0', accentColor: '#1677d2' } }),
|
|
134
|
+
jsxs('span', { style: { minWidth: 0 }, children: [
|
|
135
|
+
'已阅读并同意',
|
|
136
|
+
jsx('a', { href: 'https://ai.shuiwujia.com/agreement', target: '_blank', rel: 'noreferrer', style: linkStyle, children: '《服务协议》' }),
|
|
137
|
+
'和',
|
|
138
|
+
jsx('a', { href: 'https://ai.shuiwujia.com/privacy', target: '_blank', rel: 'noreferrer', style: linkStyle, children: '《隐私政策》' }),
|
|
139
|
+
] }),
|
|
140
|
+
] })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function useCountdown() {
|
|
144
|
+
const { useEffect, useRef, useState } = require('react')
|
|
145
|
+
const [countdown, setCountdown] = useState(0)
|
|
146
|
+
const timer = useRef(null)
|
|
147
|
+
const stop = () => {
|
|
148
|
+
if (timer.current !== null) { clearInterval(timer.current); timer.current = null }
|
|
149
|
+
}
|
|
150
|
+
const start = () => {
|
|
151
|
+
stop()
|
|
152
|
+
setCountdown(60)
|
|
153
|
+
timer.current = setInterval(() => {
|
|
154
|
+
setCountdown(value => {
|
|
155
|
+
if (value <= 1) { stop(); return 0 }
|
|
156
|
+
return value - 1
|
|
157
|
+
})
|
|
158
|
+
}, 1000)
|
|
159
|
+
}
|
|
160
|
+
useEffect(() => stop, [])
|
|
161
|
+
return [countdown, start]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function QrLogin({ session, onRestart, onClose }) {
|
|
165
|
+
const { qrCode, status, error, loading } = session
|
|
166
|
+
const statusText = { WAITING: '等待扫码', DEALING: '已扫码,请在手机上确认', SUCCESS: '登录成功', EXPIRED: '二维码已过期' }[status] || status
|
|
167
|
+
const tone = error ? '#dc2626' : status === 'SUCCESS' ? '#16a34a' : '#64748b'
|
|
168
|
+
return jsxs('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }, children: [
|
|
169
|
+
jsx('div', { style: { fontSize: 13.5, color: '#7b8799' }, children: `使用${SSO.title}扫码,安全登录 WaterBuddy` }),
|
|
170
|
+
qrCode
|
|
171
|
+
? jsx('img', { src: qrCode, alt: `${SSO.title}登录二维码`, style: { display: 'block', width: 206, height: 206, boxSizing: 'border-box', objectFit: 'contain', margin: '14px auto 10px', padding: 10, border: '1px solid #e5ebf3', borderRadius: 16, background: '#fff' } })
|
|
172
|
+
: jsx('div', { style: { width: 206, height: 206, boxSizing: 'border-box', margin: '14px auto 10px', display: 'grid', placeItems: 'center', border: '1px solid #e5ebf3', borderRadius: 16, background: '#f8fafc', color: '#7b8799', fontSize: 13 }, children: loading ? '二维码加载中…' : '二维码暂不可用' }),
|
|
173
|
+
jsx('div', { style: { minHeight: 22, color: tone, fontSize: 13 }, children: error || statusText || '请扫码登录' }),
|
|
174
|
+
jsxs('div', { style: { display: 'flex', gap: 10, justifyContent: 'center', marginTop: 14 }, children: [
|
|
175
|
+
jsx('button', { type: 'button', onClick: onRestart, disabled: loading, style: { height: 38, padding: '0 20px', border: 0, borderRadius: 10, background: '#1677d2', color: '#fff', fontSize: 13, fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? .65 : 1 }, children: '刷新二维码' }),
|
|
176
|
+
jsx('button', { type: 'button', onClick: onClose, style: { height: 38, padding: '0 20px', border: '1px solid #d9e1ec', borderRadius: 10, background: '#fff', color: '#475569', fontSize: 13, fontWeight: 600, cursor: 'pointer' }, children: '关闭' }),
|
|
177
|
+
] }),
|
|
178
|
+
] })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function postAuth(path, payload) {
|
|
182
|
+
const response = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
|
|
183
|
+
const body = await response.json().catch(() => ({}))
|
|
184
|
+
if (!response.ok) throw new Error(body.error || '请求失败,请重试')
|
|
185
|
+
return body
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function VerificationCodeField({ name, emailRef, sendPath, onError, onNotice }) {
|
|
189
|
+
const { useRef, useState } = require('react')
|
|
190
|
+
const codeRef = useRef(null)
|
|
191
|
+
const [countdown, startCountdown] = useCountdown()
|
|
192
|
+
const [sending, setSending] = useState(false)
|
|
193
|
+
|
|
194
|
+
const send = async () => {
|
|
195
|
+
const email = emailRef.current?.value?.trim() || ''
|
|
196
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return onError('请输入正确的邮箱')
|
|
197
|
+
setSending(true)
|
|
198
|
+
onError('')
|
|
199
|
+
if (onNotice) onNotice('')
|
|
200
|
+
try {
|
|
201
|
+
const body = await postAuth(sendPath, { email })
|
|
202
|
+
startCountdown()
|
|
203
|
+
if (onNotice) onNotice(body.message || '验证码已发送')
|
|
204
|
+
} catch (failure) {
|
|
205
|
+
onError(failure.message)
|
|
206
|
+
} finally {
|
|
207
|
+
setSending(false)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return jsxs('label', { style: passwordField({ padding: '0 6px 0 12px' }), children: [
|
|
212
|
+
fieldIcon('🛡'),
|
|
213
|
+
jsx('input', { ref: codeRef, name, inputMode: 'numeric', autoComplete: 'one-time-code', placeholder: '请输入验证码', style: plainInputStyle(), 'data-waterbuddy-code': 'true' }),
|
|
214
|
+
jsx('button', { type: 'button', disabled: countdown > 0 || sending, onClick: send, style: { flex: '0 0 auto', height: 36, padding: '0 14px', border: '1px solid #d9e1ec', borderRadius: 8, background: countdown > 0 || sending ? '#f1f5f9' : '#fff', color: countdown > 0 || sending ? '#9aa5b4' : '#1677d2', fontSize: 12.5, fontWeight: 600, cursor: countdown > 0 || sending ? 'not-allowed' : 'pointer' }, children: sending ? '发送中…' : countdown > 0 ? `${countdown}秒后重发` : '获取验证码' }),
|
|
215
|
+
] })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function AccountLoginForm({ onLogin, onSwitch }) {
|
|
219
|
+
const { useRef, useState } = require('react')
|
|
220
|
+
const loginId = useRef(null)
|
|
221
|
+
const password = useRef(null)
|
|
222
|
+
const [loading, setLoading] = useState(false)
|
|
223
|
+
const [error, setError] = useState('')
|
|
224
|
+
|
|
225
|
+
const submit = async event => {
|
|
226
|
+
if (event && event.preventDefault) event.preventDefault()
|
|
227
|
+
const id = loginId.current?.value?.trim() || ''
|
|
228
|
+
const secret = password.current?.value || ''
|
|
229
|
+
if (!id) return setError('请输入账号')
|
|
230
|
+
if (!secret) return setError('请输入密码')
|
|
231
|
+
setLoading(true); setError('')
|
|
232
|
+
try {
|
|
233
|
+
const body = await postAuth('/api/waterbuddy/auth/login', { loginId: id, password: secret })
|
|
234
|
+
return onLogin(body.user)
|
|
235
|
+
} catch (failure) {
|
|
236
|
+
setError(failure.message)
|
|
237
|
+
} finally {
|
|
238
|
+
setLoading(false)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return jsxs('form', { onSubmit: submit, style: { display: 'grid', gap: 14, textAlign: 'left' }, children: [
|
|
243
|
+
jsxs('label', { style: passwordField(), children: [
|
|
244
|
+
fieldIcon('👤'),
|
|
245
|
+
jsx('input', { ref: loginId, name: 'loginId', type: 'text', autoComplete: 'username', placeholder: '请输入账号', style: plainInputStyle(), onKeyDown: event => { if (event.key === 'Enter') submit(event) } }),
|
|
246
|
+
] }),
|
|
247
|
+
jsxs('label', { style: passwordField(), children: [
|
|
248
|
+
fieldIcon('🔒'),
|
|
249
|
+
jsx('input', { ref: password, name: 'password', type: 'password', autoComplete: 'current-password', placeholder: '请输入密码', style: plainInputStyle(), onKeyDown: event => { if (event.key === 'Enter') submit(event) } }),
|
|
250
|
+
] }),
|
|
251
|
+
jsx('div', { style: { marginTop: -4, textAlign: 'right' }, children: jsx('button', { type: 'button', onClick: () => onSwitch('password'), style: linkButtonStyle(), children: '忘记密码?' }) }),
|
|
252
|
+
error ? errorBox(error) : null,
|
|
253
|
+
jsx('button', { type: 'submit', disabled: loading, style: primaryButtonStyle(loading), children: loading ? '正在登录…' : '立即登录' }),
|
|
254
|
+
agreementNotice(),
|
|
255
|
+
jsxs('div', { style: { textAlign: 'center', color: '#8a94a6', fontSize: 12.5 }, children: [
|
|
256
|
+
'还没有账号?',
|
|
257
|
+
jsx('button', { type: 'button', onClick: () => onSwitch('register'), style: linkButtonStyle(), children: '注册账号' }),
|
|
258
|
+
] }),
|
|
259
|
+
] })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function RegisterForm({ onLogin, onSwitch }) {
|
|
263
|
+
const { useRef, useState } = require('react')
|
|
264
|
+
const email = useRef(null)
|
|
265
|
+
const code = useRef(null)
|
|
266
|
+
const password = useRef(null)
|
|
267
|
+
const name = useRef(null)
|
|
268
|
+
const [loading, setLoading] = useState(false)
|
|
269
|
+
const [error, setError] = useState('')
|
|
270
|
+
const [notice, setNotice] = useState('')
|
|
271
|
+
|
|
272
|
+
const onCodeError = message => setError(message || '')
|
|
273
|
+
const onCodeNotice = message => setNotice(message || '')
|
|
274
|
+
|
|
275
|
+
const submit = async event => {
|
|
276
|
+
if (event && event.preventDefault) event.preventDefault()
|
|
277
|
+
const mail = email.current?.value?.trim() || ''
|
|
278
|
+
const verification = code.current?.value?.trim() || ''
|
|
279
|
+
const secret = password.current?.value || ''
|
|
280
|
+
const displayName = name.current?.value?.trim() || ''
|
|
281
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(mail)) return setError('请输入正确的邮箱')
|
|
282
|
+
if (!/^\d{4,6}$/.test(verification)) return setError('验证码格式不正确')
|
|
283
|
+
if (!secret) return setError('请输入密码')
|
|
284
|
+
setLoading(true); setError(''); setNotice('')
|
|
285
|
+
try {
|
|
286
|
+
const body = await postAuth('/api/waterbuddy/auth/register', { email: mail, verifyCode: verification, password: secret, ...(displayName ? { name: displayName } : {}) })
|
|
287
|
+
return onLogin(body.user, body.message || '注册成功')
|
|
288
|
+
} catch (failure) {
|
|
289
|
+
setError(failure.message)
|
|
290
|
+
} finally {
|
|
291
|
+
setLoading(false)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return jsxs('form', { onSubmit: submit, style: { display: 'grid', gap: 11, textAlign: 'left' }, children: [
|
|
296
|
+
jsxs('label', { style: passwordField(), children: [
|
|
297
|
+
fieldIcon('✉'),
|
|
298
|
+
jsx('input', { ref: email, name: 'email', type: 'email', autoComplete: 'email', placeholder: '请输入邮箱', style: plainInputStyle() }),
|
|
299
|
+
] }),
|
|
300
|
+
jsx(VerificationCodeField, { name: 'verifyCode', emailRef: email, sendPath: '/api/waterbuddy/auth/register/code', onError: onCodeError, onNotice: onCodeNotice }),
|
|
301
|
+
jsxs('label', { style: passwordField(), children: [
|
|
302
|
+
fieldIcon('🔒'),
|
|
303
|
+
jsx('input', { ref: password, name: 'password', type: 'password', autoComplete: 'new-password', placeholder: '请输入密码', style: plainInputStyle() }),
|
|
304
|
+
] }),
|
|
305
|
+
jsxs('label', { style: passwordField(), children: [
|
|
306
|
+
fieldIcon('👤'),
|
|
307
|
+
jsx('input', { ref: name, name: 'name', type: 'text', placeholder: '请输入用户名(选填)', style: plainInputStyle() }),
|
|
308
|
+
] }),
|
|
309
|
+
error ? errorBox(error) : null,
|
|
310
|
+
notice ? jsx('div', { style: { color: '#16a34a', fontSize: 12.5 }, children: notice }) : null,
|
|
311
|
+
jsxs('div', { style: { display: 'flex', gap: 10, marginTop: 2 }, children: [
|
|
312
|
+
jsx('button', { type: 'submit', disabled: loading, style: { ...primaryButtonStyle(loading), flex: '1 1 auto' }, children: loading ? '注册中…' : '注册账号' }),
|
|
313
|
+
jsx('button', { type: 'button', onClick: () => onSwitch('account'), 'aria-label': '返回登录', title: '返回登录', style: { flex: '0 0 auto', width: 46, height: 46, border: '1px solid #d9e1ec', borderRadius: 10, background: '#fff', color: '#475569', fontSize: 16, cursor: 'pointer' }, children: '←' }),
|
|
314
|
+
] }),
|
|
315
|
+
agreementNotice(),
|
|
316
|
+
] })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function PasswordResetForm({ onSwitch }) {
|
|
320
|
+
const { useRef, useState } = require('react')
|
|
321
|
+
const email = useRef(null)
|
|
322
|
+
const code = useRef(null)
|
|
323
|
+
const password = useRef(null)
|
|
324
|
+
const confirm = useRef(null)
|
|
325
|
+
const [loading, setLoading] = useState(false)
|
|
326
|
+
const [error, setError] = useState('')
|
|
327
|
+
const [notice, setNotice] = useState('')
|
|
328
|
+
|
|
329
|
+
const onCodeError = message => setError(message || '')
|
|
330
|
+
const onCodeNotice = message => setNotice(message || '')
|
|
331
|
+
|
|
332
|
+
const submit = async event => {
|
|
333
|
+
if (event && event.preventDefault) event.preventDefault()
|
|
334
|
+
const mail = email.current?.value?.trim() || ''
|
|
335
|
+
const verification = code.current?.value?.trim() || ''
|
|
336
|
+
const secret = password.current?.value || ''
|
|
337
|
+
const repeated = confirm.current?.value || ''
|
|
338
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(mail)) return setError('请输入正确的邮箱')
|
|
339
|
+
if (!/^\d{4,6}$/.test(verification)) return setError('验证码格式不正确')
|
|
340
|
+
if (!secret) return setError('请输入新密码')
|
|
341
|
+
if (secret !== repeated) return setError('两次输入的密码不一致')
|
|
342
|
+
setLoading(true); setError(''); setNotice('')
|
|
343
|
+
try {
|
|
344
|
+
const body = await postAuth('/api/waterbuddy/auth/password/change', { email: mail, verifyCode: verification, password: secret })
|
|
345
|
+
setNotice(body.message || '密码修改成功')
|
|
346
|
+
onSwitch('account', { loginId: mail })
|
|
347
|
+
} catch (failure) {
|
|
348
|
+
setError(failure.message)
|
|
349
|
+
} finally {
|
|
350
|
+
setLoading(false)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return jsxs('form', { onSubmit: submit, style: { display: 'grid', gap: 11, textAlign: 'left' }, children: [
|
|
355
|
+
jsxs('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [
|
|
356
|
+
jsx('strong', { style: { color: '#162033', fontSize: 14 }, children: '重置密码' }),
|
|
357
|
+
jsx('button', { type: 'button', onClick: () => onSwitch('account'), style: linkButtonStyle(), children: '← 返回登录' }),
|
|
358
|
+
] }),
|
|
359
|
+
jsxs('label', { style: passwordField(), children: [
|
|
360
|
+
fieldIcon('✉'),
|
|
361
|
+
jsx('input', { ref: email, name: 'email', type: 'email', autoComplete: 'email', placeholder: '请输入邮箱', style: plainInputStyle() }),
|
|
362
|
+
] }),
|
|
363
|
+
jsx(VerificationCodeField, { name: 'verifyCode', emailRef: email, sendPath: '/api/waterbuddy/auth/password/code', onError: onCodeError, onNotice: onCodeNotice }),
|
|
364
|
+
jsxs('label', { style: passwordField(), children: [
|
|
365
|
+
fieldIcon('🔒'),
|
|
366
|
+
jsx('input', { ref: password, name: 'password', type: 'password', autoComplete: 'new-password', placeholder: '请输入新密码', style: plainInputStyle() }),
|
|
367
|
+
] }),
|
|
368
|
+
jsxs('label', { style: passwordField(), children: [
|
|
369
|
+
fieldIcon('🔒'),
|
|
370
|
+
jsx('input', { ref: confirm, name: 'confirmPassword', type: 'password', autoComplete: 'new-password', placeholder: '请再次输入新密码', style: plainInputStyle() }),
|
|
371
|
+
] }),
|
|
372
|
+
error ? errorBox(error) : null,
|
|
373
|
+
notice ? jsx('div', { style: { color: '#16a34a', fontSize: 12.5 }, children: notice }) : null,
|
|
374
|
+
jsx('button', { type: 'submit', disabled: loading, style: { ...primaryButtonStyle(loading), marginTop: 2 }, children: loading ? '提交中…' : '修改密码' }),
|
|
375
|
+
] })
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function LoginPanel({ session, onLogin, onRestart, onClose }) {
|
|
379
|
+
const { useState } = require('react')
|
|
380
|
+
const [mode, setMode] = useState('qr')
|
|
381
|
+
const [loginId, setLoginId] = useState('')
|
|
382
|
+
const [notice, setNotice] = useState('')
|
|
383
|
+
|
|
384
|
+
const switchMode = (next, options) => {
|
|
385
|
+
setMode(next)
|
|
386
|
+
setNotice(options?.loginId || '')
|
|
387
|
+
}
|
|
388
|
+
const handleLogin = (user, message) => {
|
|
389
|
+
setNotice(message || '')
|
|
390
|
+
onLogin(user)
|
|
391
|
+
}
|
|
392
|
+
const tabs = [{ key: 'qr', title: '扫码登录' }, { key: 'account', title: '账号登录' }]
|
|
393
|
+
const activeTab = mode === 'qr' ? 'qr' : 'account'
|
|
394
|
+
|
|
395
|
+
return jsxs('div', { style: { padding: '18px 26px 24px' }, children: [
|
|
396
|
+
jsxs('div', { style: { display: 'flex', borderBottom: '1px solid #e8edf4' }, children: tabs.map(tab => jsxs('button', {
|
|
397
|
+
key: tab.key,
|
|
398
|
+
type: 'button',
|
|
399
|
+
onClick: () => switchMode(tab.key),
|
|
400
|
+
style: { position: 'relative', flex: '1 1 0', height: 46, border: 0, background: 'transparent', color: activeTab === tab.key ? '#162033' : '#7b8799', fontSize: 15, fontWeight: 600, cursor: 'pointer', transition: 'color .15s ease' },
|
|
401
|
+
children: [
|
|
402
|
+
tab.title,
|
|
403
|
+
activeTab === tab.key ? jsx('span', { style: { position: 'absolute', left: '50%', bottom: -1, width: 32, height: 3, marginLeft: -16, borderRadius: 2, background: '#1677d2' } }) : null,
|
|
404
|
+
],
|
|
405
|
+
})) }),
|
|
406
|
+
notice ? jsx('div', { style: { marginTop: 12, padding: '8px 12px', borderRadius: 10, background: '#f0fdf4', border: '1px solid #bbf7d0', color: '#15803d', fontSize: 12.5, textAlign: 'center' }, children: notice }) : null,
|
|
407
|
+
jsx('div', { style: { paddingTop: 18, maxHeight: 'min(58vh, 470px)', overflowY: 'auto' }, children: mode === 'qr'
|
|
408
|
+
? jsx(QrLogin, { session, onRestart, onClose })
|
|
409
|
+
: mode === 'register'
|
|
410
|
+
? jsx(RegisterForm, { onLogin: handleLogin, onSwitch: switchMode })
|
|
411
|
+
: mode === 'password'
|
|
412
|
+
? jsx(PasswordResetForm, { onSwitch: switchMode })
|
|
413
|
+
: jsx(AccountLoginForm, { onLogin: handleLogin, onSwitch: switchMode }) }),
|
|
414
|
+
] })
|
|
415
|
+
}
|
|
416
|
+
|
|
66
417
|
function UserAction({ wide }) {
|
|
67
|
-
const { useEffect, useState } = require('react')
|
|
418
|
+
const { useEffect, useRef, useState } = require('react')
|
|
68
419
|
const [open, setOpen] = useState(false)
|
|
69
|
-
const [loading, setLoading] = useState(false)
|
|
70
|
-
const [qr, setQr] = useState(null)
|
|
71
|
-
const [status, setStatus] = useState('')
|
|
72
420
|
const [user, setUser] = useState(null)
|
|
73
|
-
const [
|
|
421
|
+
const [llm, setLlm] = useState(null)
|
|
422
|
+
const [session, setSession] = useState({ qrCode: '', status: '', error: '', loading: false })
|
|
74
423
|
|
|
75
424
|
useEffect(() => {
|
|
76
425
|
let cancelled = false
|
|
@@ -80,54 +429,127 @@ window.__ModuleLoader__.load({
|
|
|
80
429
|
return () => { cancelled = true }
|
|
81
430
|
}, [])
|
|
82
431
|
|
|
432
|
+
// 额度轮询:依赖 user,登录/登出后会立刻重新取值(Host 此时已换过 key)。
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
let cancelled = false
|
|
435
|
+
const load = async () => {
|
|
436
|
+
try {
|
|
437
|
+
const response = await fetch('/api/waterbuddy/llm/status')
|
|
438
|
+
const body = await response.json()
|
|
439
|
+
if (!cancelled) setLlm(body.llm || null)
|
|
440
|
+
} catch (_) {}
|
|
441
|
+
}
|
|
442
|
+
load()
|
|
443
|
+
const timer = setInterval(load, 15000)
|
|
444
|
+
return () => { cancelled = true; clearInterval(timer) }
|
|
445
|
+
}, [user])
|
|
446
|
+
|
|
447
|
+
const startLogin = async () => {
|
|
448
|
+
setSession({ qrCode: '', status: '二维码加载中', error: '', loading: true })
|
|
449
|
+
try {
|
|
450
|
+
const response = await fetch('/api/waterbuddy/auth/qr', { method: 'POST' })
|
|
451
|
+
const body = await response.json()
|
|
452
|
+
if (!response.ok) throw new Error(body.error || '二维码获取失败')
|
|
453
|
+
setSession({ qrCode: body.qrCode, status: 'WAITING', error: '', loading: false, sessionId: body.sessionId })
|
|
454
|
+
} catch (failure) {
|
|
455
|
+
setSession({ qrCode: '', status: '', error: failure.message, loading: false })
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
83
459
|
useEffect(() => {
|
|
84
|
-
if (!open || !
|
|
460
|
+
if (!open || !session.sessionId || user) return undefined
|
|
85
461
|
let stopped = false
|
|
86
462
|
let timer
|
|
87
463
|
const poll = async () => {
|
|
88
464
|
try {
|
|
89
|
-
const response = await fetch(`/api/waterbuddy/auth/status?sessionId=${encodeURIComponent(
|
|
465
|
+
const response = await fetch(`/api/waterbuddy/auth/status?sessionId=${encodeURIComponent(session.sessionId)}`)
|
|
90
466
|
const body = await response.json()
|
|
91
467
|
if (stopped) return
|
|
92
|
-
setStatus(body.status || '')
|
|
93
468
|
if (body.status === 'SUCCESS') {
|
|
94
469
|
setUser(body.user || null)
|
|
95
|
-
|
|
470
|
+
setSession(current => ({ ...current, status: 'SUCCESS', error: '' }))
|
|
96
471
|
return
|
|
97
472
|
}
|
|
98
473
|
if (body.status === 'EXPIRED') {
|
|
99
|
-
|
|
100
|
-
setLoading(false)
|
|
474
|
+
setSession(current => ({ ...current, status: 'EXPIRED', error: '二维码已过期,请重新获取' }))
|
|
101
475
|
return
|
|
102
476
|
}
|
|
477
|
+
setSession(current => ({ ...current, status: body.status || 'WAITING' }))
|
|
103
478
|
} catch (_) {
|
|
104
|
-
if (!stopped)
|
|
479
|
+
if (!stopped) setSession(current => ({ ...current, error: '查询扫码状态失败,请重试' }))
|
|
105
480
|
}
|
|
106
481
|
if (!stopped) timer = setTimeout(poll, 2000)
|
|
107
482
|
}
|
|
108
483
|
poll()
|
|
109
484
|
return () => { stopped = true; if (timer) clearTimeout(timer) }
|
|
110
|
-
}, [open,
|
|
485
|
+
}, [open, session.sessionId, user])
|
|
111
486
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
487
|
+
const openPanel = () => { setOpen(true); if (!user && !session.sessionId) startLogin() }
|
|
488
|
+
// 额度耗尽是**主动**提示:模型调用由 Harness 自己发起,插件看不到那次 403
|
|
489
|
+
// (harness 还会把它归类成「API 密钥无效」,文案完全误导),只能靠额度轮询
|
|
490
|
+
// 自己发现并把登录面板推给用户。
|
|
491
|
+
//
|
|
492
|
+
// 必须走 openPanel 而不是裸 setOpen:裸开会得到一个「二维码暂不可用」的空面板,
|
|
493
|
+
// 因为扫码会话根本没发起(实测踩过)。
|
|
494
|
+
const exhaustedNotified = useRef(false)
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
if (!llm || llm.managesQuota !== true || user) {
|
|
497
|
+
exhaustedNotified.current = false
|
|
498
|
+
return
|
|
121
499
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
500
|
+
if (llm.exhausted === true && !exhaustedNotified.current) {
|
|
501
|
+
exhaustedNotified.current = true
|
|
502
|
+
openPanel()
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
if (llm.exhausted !== true) exhaustedNotified.current = false
|
|
506
|
+
}, [llm, user])
|
|
507
|
+
const logout = async () => { await fetch('/api/waterbuddy/logout', { method: 'POST' }); setUser(null); setOpen(false); setSession({ qrCode: '', status: '', error: '', loading: false }) }
|
|
127
508
|
const name = userName(user)
|
|
128
|
-
const account = user?.loginId || (user?.id ? `${user.id.slice(0, 8)}…` : '水务之窗账号')
|
|
129
|
-
const isAdmin = user?.isAdmin === 1 || user?.isAdmin === true
|
|
130
509
|
const buttonStyle = { borderRadius: 10, padding: '9px 18px', fontSize: 13, fontWeight: 600, cursor: 'pointer' }
|
|
510
|
+
// 余额单位是 sub2api 的美元,不是人民币(见 SUB2API-TOKEN-QUOTA-PLAN §2)。
|
|
511
|
+
const balanceText = `$${Number(llm?.balance || 0).toFixed(2)}`
|
|
512
|
+
// 用户把默认模型换成别人的提供方时,消耗的不是我们的额度,一律不展示额度。
|
|
513
|
+
const managesQuota = llm?.managesQuota === true
|
|
514
|
+
const exhausted = managesQuota && llm?.exhausted === true
|
|
515
|
+
// 未登录时展示试用额度,登录后展示账号额度;耗尽一律优先提示
|
|
516
|
+
const subtitle = !managesQuota
|
|
517
|
+
? (user ? '水务之窗' : '扫码或账号登录')
|
|
518
|
+
: exhausted
|
|
519
|
+
? '额度已用完'
|
|
520
|
+
: (user ? `额度 ${balanceText}` : `试用额度 ${balanceText}`)
|
|
521
|
+
|
|
522
|
+
function userPanel() {
|
|
523
|
+
const account = user?.loginId || (user?.id ? `${String(user.id).slice(0, 8)}…` : '水务之窗账号')
|
|
524
|
+
const isAdmin = user?.isAdmin === 1 || user?.isAdmin === true
|
|
525
|
+
return jsxs(Fragment, { children: [
|
|
526
|
+
jsx('div', { style: { position: 'relative', padding: '28px 28px 44px', textAlign: 'center', color: '#fff', background: 'linear-gradient(135deg, #0f6fd6 0%, #1d9bf0 58%, #38bdf8 100%)' }, children: [
|
|
527
|
+
jsx('button', { type: 'button', onClick: () => setOpen(false), 'aria-label': '关闭', style: { position: 'absolute', top: 14, right: 16, width: 30, height: 30, border: 0, borderRadius: '50%', background: 'rgba(255,255,255,.18)', color: '#fff', fontSize: 20, lineHeight: 1, cursor: 'pointer' }, children: '×' }),
|
|
528
|
+
jsx('div', { style: { fontSize: 12, opacity: .82, letterSpacing: 1 }, children: 'WATERBUDDY' }),
|
|
529
|
+
jsx('div', { style: { marginTop: 8, fontSize: 15, fontWeight: 600 }, children: 'WaterBuddy' }),
|
|
530
|
+
jsx('div', { style: { position: 'absolute', left: '50%', bottom: -35, transform: 'translateX(-50%)', padding: 5, borderRadius: '50%', background: '#fff', boxShadow: '0 6px 18px rgba(15,23,42,.18)' }, children: jsx(UserAvatar, { user, size: 62 }) }),
|
|
531
|
+
] }),
|
|
532
|
+
jsxs('div', { style: { padding: '48px 28px 26px', textAlign: 'center' }, children: [
|
|
533
|
+
jsx('h3', { style: { margin: 0, color: '#162033', fontSize: 22 }, children: name }),
|
|
534
|
+
jsx('p', { style: { margin: '7px 0 18px', color: '#7b8799', fontSize: 13 }, children: user.organization || user.department || '水务之窗' }),
|
|
535
|
+
jsxs('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, textAlign: 'left' }, children: [
|
|
536
|
+
jsxs('div', { style: { padding: '12px 14px', borderRadius: 12, background: '#f5f8fc' }, children: [
|
|
537
|
+
jsx('small', { style: { color: '#8a94a6', fontSize: 11 }, children: '账号' }),
|
|
538
|
+
jsx('div', { style: { marginTop: 4, color: '#334155', fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: account }),
|
|
539
|
+
] }),
|
|
540
|
+
jsxs('div', { style: { padding: '12px 14px', borderRadius: 12, background: '#f5f8fc' }, children: [
|
|
541
|
+
jsx('small', { style: { color: '#8a94a6', fontSize: 11 }, children: '身份' }),
|
|
542
|
+
jsx('div', { style: { marginTop: 4, color: '#334155', fontSize: 13, fontWeight: 600 }, children: isAdmin ? '管理员' : '内部成员' }),
|
|
543
|
+
] }),
|
|
544
|
+
managesQuota ? jsxs('div', { style: { padding: '12px 14px', borderRadius: 12, background: exhausted ? '#fff7ed' : '#f5f8fc' }, children: [
|
|
545
|
+
jsx('small', { style: { color: '#8a94a6', fontSize: 11 }, children: '剩余额度' }),
|
|
546
|
+
jsx('div', { style: { marginTop: 4, color: exhausted ? '#b45309' : '#334155', fontSize: 13, fontWeight: 600 }, children: balanceText }),
|
|
547
|
+
] }) : null,
|
|
548
|
+
] }),
|
|
549
|
+
jsx('button', { type: 'button', onClick: logout, style: { ...buttonStyle, width: '100%', marginTop: 18, border: '1px solid #d9e1ec', background: '#fff', color: '#475569' }, children: '退出登录' }),
|
|
550
|
+
] }),
|
|
551
|
+
] })
|
|
552
|
+
}
|
|
131
553
|
|
|
132
554
|
return jsxs(Fragment, {
|
|
133
555
|
children: [
|
|
@@ -136,13 +558,21 @@ window.__ModuleLoader__.load({
|
|
|
136
558
|
onClick: openPanel,
|
|
137
559
|
title: user ? name : '登录 WaterBuddy',
|
|
138
560
|
style: { display: 'flex', alignItems: 'center', gap: 8, width: wide ? 'auto' : 40, maxWidth: wide ? 150 : 40, minHeight: 38, padding: wide ? '5px 9px 5px 6px' : 3, border: user ? '1px solid #e1eafa' : '1px solid transparent', borderRadius: 12, background: user ? '#f5f8ff' : 'transparent', color: 'var(--dsw-text-secondary, #586174)', cursor: 'pointer', textAlign: 'left', transition: 'background .15s ease, border-color .15s ease' },
|
|
139
|
-
children: [user ? jsx(UserAvatar, { user, size: 28 }) : jsx('img', { src: SSO.image, alt: SSO.title, style: { width: 28, height: 28, borderRadius: 8, objectFit: 'contain', padding: 2, boxSizing: 'border-box', flex: '0 0 auto', background: '#fff', border: '1px solid #dce9fb' } }), wide ? jsxs('span', { style: { minWidth: 0, display: 'grid', gap: 1 }, children: [jsx('strong', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12.5, lineHeight: '16px', color: 'var(--dsw-text-primary, #1f2937)' }, children: user ? name : SSO.title }), jsx('small', { style: { color: '#8a94a6', fontSize: 10.5, lineHeight: '14px' }, children:
|
|
561
|
+
children: [user ? jsx(UserAvatar, { user, size: 28 }) : jsx('img', { src: SSO.image, alt: SSO.title, style: { width: 28, height: 28, borderRadius: 8, objectFit: 'contain', padding: 2, boxSizing: 'border-box', flex: '0 0 auto', background: '#fff', border: '1px solid #dce9fb' } }), wide ? jsxs('span', { style: { minWidth: 0, display: 'grid', gap: 1 }, children: [jsx('strong', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12.5, lineHeight: '16px', color: 'var(--dsw-text-primary, #1f2937)' }, children: user ? name : SSO.title }), jsx('small', { style: { color: exhausted ? '#b45309' : '#8a94a6', fontSize: 10.5, lineHeight: '14px' }, children: subtitle })] }) : null],
|
|
140
562
|
}),
|
|
141
|
-
open ? jsx('div', { style: { position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(15, 23, 42, .42)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }, onClick: event => { if (event.target === event.currentTarget) setOpen(false) }, children: jsx('div', { style: { width: 420, maxWidth: 'calc(100vw - 32px)', borderRadius: 22, background: '#fff', overflow: 'hidden', boxShadow: '0 24px 80px rgba(15,23,42,.28)' }, children: user ?
|
|
563
|
+
open ? jsx('div', { style: { position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(15, 23, 42, .42)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }, onClick: event => { if (event.target === event.currentTarget) setOpen(false) }, children: jsx('div', { style: { width: 420, maxWidth: 'calc(100vw - 32px)', borderRadius: 22, background: '#fff', overflow: 'hidden', boxShadow: '0 24px 80px rgba(15,23,42,.28)' }, children: user ? userPanel() : jsxs('div', { children: [
|
|
564
|
+
jsxs('div', { style: { position: 'relative', padding: '22px 26px 0', textAlign: 'center' }, children: [
|
|
565
|
+
jsx('button', { type: 'button', onClick: () => setOpen(false), 'aria-label': '关闭', style: { position: 'absolute', top: 14, right: 16, width: 30, height: 30, border: 0, borderRadius: '50%', background: '#f1f5f9', color: '#64748b', fontSize: 20, lineHeight: 1, cursor: 'pointer' }, children: '×' }),
|
|
566
|
+
jsx('img', { src: SSO.image, alt: SSO.title, style: { display: 'block', width: 52, height: 52, objectFit: 'contain', margin: '0 auto' } }),
|
|
567
|
+
jsx('h3', { style: { margin: '10px 0 4px', color: '#162033', fontSize: 21 }, children: 'WaterBuddy' }),
|
|
568
|
+
jsx('p', { style: { margin: 0, color: '#7b8799', fontSize: 12.5 }, children: '登录后同步水务之窗账号与知识库权限' }),
|
|
569
|
+
exhausted ? jsx('div', { style: { margin: '14px 26px 0', padding: '10px 12px', borderRadius: 10, background: '#fff7ed', border: '1px solid #fed7aa', color: '#b45309', fontSize: 12.5, lineHeight: 1.5 }, children: '试用额度已用完,登录后即可继续使用(额度升为账号档)' }) : null,
|
|
570
|
+
] }),
|
|
571
|
+
jsx(LoginPanel, { session, onLogin: nextUser => setUser(nextUser || null), onRestart: startLogin, onClose: () => setOpen(false) }),
|
|
572
|
+
] }) }) }) : null,
|
|
142
573
|
],
|
|
143
574
|
})
|
|
144
575
|
}
|
|
145
|
-
|
|
146
576
|
function SkipWelcomeNotice({ complete }) {
|
|
147
577
|
const { useEffect } = require('react')
|
|
148
578
|
useEffect(() => {
|
|
@@ -175,10 +605,10 @@ window.__ModuleLoader__.load({
|
|
|
175
605
|
const style = document.createElement('style')
|
|
176
606
|
style.dataset.waterbuddy = 'sidebar-footer-layout'
|
|
177
607
|
style.textContent = [
|
|
178
|
-
'.dshp-
|
|
179
|
-
'.dshp-
|
|
180
|
-
'.dshp-
|
|
181
|
-
'.dshp-
|
|
608
|
+
'.dshp-panel.dshp-panel--wide .dshp-panel__foot-area{flex-direction:row;align-items:center;gap:8px}',
|
|
609
|
+
'.dshp-panel.dshp-panel--wide .dshp-panel__settings-area{order:1;flex:0 0 auto;width:auto;min-width:0}',
|
|
610
|
+
'.dshp-panel.dshp-panel--wide .dshp-panel__footer-actions{order:2;flex:1 1 auto;width:auto;min-width:0;justify-content:flex-end}',
|
|
611
|
+
'.dshp-panel.dshp-panel--wide .dshp-panel__footer-actions>div{display:flex;justify-content:flex-end;min-width:0}',
|
|
182
612
|
].join('')
|
|
183
613
|
document.head.append(style)
|
|
184
614
|
return () => { style.remove() }
|