vg-coder-cli 2.0.20 → 2.0.23

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.
Files changed (50) hide show
  1. package/.vg/tree-state.json +9 -0
  2. package/package.json +5 -2
  3. package/scripts/build.js +49 -6
  4. package/src/index.js +4 -4
  5. package/src/server/api-server.js +83 -5
  6. package/src/server/views/css/structure.css +4 -1
  7. package/src/server/views/dashboard.css +51 -6
  8. package/src/server/views/dashboard.html +11 -3
  9. package/src/server/views/js/api.js +24 -0
  10. package/src/server/views/js/features/iframe-manager.js +2 -1
  11. package/src/server/views/js/features/resize.js +57 -0
  12. package/src/server/views/js/features/structure.js +109 -16
  13. package/src/server/views/js/main.js +36 -4
  14. package/src/server/views/vg-coder/background.js +48201 -2
  15. package/src/server/views/vg-coder/controller.js +496 -1
  16. package/src/server/views/vg-coder/manifest.json +13 -5
  17. package/src/server/views/vg-coder/{options.css → sidepanel.css} +34 -32
  18. package/src/server/views/vg-coder/{options.html → sidepanel.html} +2 -2
  19. package/src/server/views/vg-coder/sidepanel.js +347 -0
  20. package/vetgo-auto/README.md +3 -0
  21. package/vetgo-auto/chrome/CSP_IMPROVEMENTS.md +147 -0
  22. package/vetgo-auto/chrome/MANIFEST_V3_MIGRATION.md +123 -0
  23. package/vetgo-auto/chrome/assets/icon128.png +0 -0
  24. package/vetgo-auto/chrome/assets/icon16.png +0 -0
  25. package/vetgo-auto/chrome/assets/icon48.png +0 -0
  26. package/vetgo-auto/chrome/environments/environment.ts +13 -0
  27. package/vetgo-auto/chrome/manifest.json +66 -0
  28. package/vetgo-auto/chrome/rules.json +23 -0
  29. package/vetgo-auto/chrome/src/background.ts +200 -0
  30. package/vetgo-auto/chrome/src/controller.ts +98 -0
  31. package/vetgo-auto/chrome/src/controllers/common.firebase.ts +31 -0
  32. package/vetgo-auto/chrome/src/controllers/firebase-crud.ts +147 -0
  33. package/vetgo-auto/chrome/src/controllers/load-common-fuc.controller.ts +24 -0
  34. package/vetgo-auto/chrome/src/controllers/load-script.controller.ts +23 -0
  35. package/vetgo-auto/chrome/src/script-injector.ts +305 -0
  36. package/vetgo-auto/chrome/src/sidepanel.css +166 -0
  37. package/vetgo-auto/chrome/src/sidepanel.html +48 -0
  38. package/vetgo-auto/chrome/src/sidepanel.ts +127 -0
  39. package/vetgo-auto/chrome/src/utils/db-utils.ts +2 -0
  40. package/vetgo-auto/chrome/src/utils/environment-storage.service.ts +85 -0
  41. package/vetgo-auto/chrome/webpack.config.js +53 -0
  42. package/vetgo-auto/chrome/webpack.config.prod.js +54 -0
  43. package/vetgo-auto/package.json +30 -0
  44. package/vetgo-auto/tsconfig.json +27 -0
  45. package/vg-coder-cli-2.0.23.tgz +0 -0
  46. package/src/server/views/vg-coder/background.js.LICENSE.txt +0 -118
  47. package/src/server/views/vg-coder/options.js +0 -1
  48. package/vg-coder-cli-2.0.20.tgz +0 -0
  49. package/vg-coder-cli-2.0.21.tgz +0 -0
  50. package/vg-coder.zip +0 -0
@@ -0,0 +1,31 @@
1
+ import firebase from 'firebase/compat/app';
2
+ import 'firebase/compat/firestore';
3
+ import "firebase/compat/database";
4
+ import { EnvironmentStorageService } from '../utils/environment-storage.service';
5
+
6
+ // Không export trực tiếp biến đã khởi tạo nữa vì cần chờ async config
7
+ // export const FirebaseDatabase = firebase.database();
8
+
9
+ let initializedApp: firebase.app.App = null;
10
+
11
+ export const getFirebaseDatabase = async () => {
12
+ if (!initializedApp) {
13
+ // Lấy config từ storage hoặc mặc định
14
+ const config = await EnvironmentStorageService.getFirebaseConfig();
15
+
16
+ // Kiểm tra xem đã có app nào tên mặc định chưa để tránh lỗi duplicate app
17
+ if (firebase.apps.length === 0) {
18
+ initializedApp = firebase.initializeApp(config);
19
+ } else {
20
+ // Nếu đã có rồi (do reload extension context) thì dùng lại,
21
+ // nhưng nếu config thay đổi thì cần logic phức tạp hơn (xóa app cũ).
22
+ // Ở đây giả định đơn giản là dùng app hiện tại hoặc tái khởi tạo nếu cần.
23
+ // Với extension service worker, biến toàn cục có thể bị reset.
24
+
25
+ // Để an toàn, ta delete app cũ và init lại với config mới nhất
26
+ await firebase.app().delete().catch(() => { });
27
+ initializedApp = firebase.initializeApp(config);
28
+ }
29
+ }
30
+ return initializedApp.database();
31
+ };
@@ -0,0 +1,147 @@
1
+ import * as cloneDeep from 'lodash/cloneDeep';
2
+ import { uuid } from "../utils/db-utils";
3
+ interface EntityModel {
4
+ id?: string; // Optional for new entities
5
+ seqNo: number;
6
+ deleted: boolean;
7
+ }
8
+ export function firebaseSerialize<T>(object: T) {
9
+ return cloneDeep(object);
10
+ }
11
+ export class RealtimeDbCrud<T extends EntityModel> {
12
+ constructor(
13
+ protected database: any
14
+ ) {
15
+ this.database = database;
16
+ }
17
+ getCurrentTimestamp() {
18
+ return new Date().getTime();
19
+ }
20
+
21
+ addAll(list, collectionName: string) {
22
+ const updates = {};
23
+ for (let item of list) {
24
+ if (!item.id) {
25
+ item.id = uuid();
26
+ }
27
+ item = { ...item, seqNo: this.getCurrentTimestamp(), deleted: false };
28
+ updates[`/${collectionName}/${item.id}`] = firebaseSerialize(item);
29
+ }
30
+ return this.database.ref().update(updates).then(() => list);
31
+ }
32
+
33
+ list(collectionName: string) {
34
+ return new Promise((resolve, reject) => {
35
+ this.database
36
+ .ref(collectionName)
37
+ .once('value')
38
+ .then((snapshot) => {
39
+ const data = snapshot.val();
40
+ const list = Object.keys(data).map((key) => ({ id: key, ...data[key] }));
41
+ resolve(list);
42
+ })
43
+ .catch((error) => {
44
+ reject(error);
45
+ });
46
+ });
47
+ }
48
+ // work okie
49
+ getRT(id: string,collectionName: string, callback: (item: T) => void ) {
50
+ const databaseRef = this.database.ref(`${collectionName}/${id}`);
51
+ databaseRef.on('value', (snapshot) => {
52
+ const data = snapshot.val();
53
+ if (data) {
54
+ callback(data);
55
+ } else {
56
+ callback(null);
57
+ }
58
+ });
59
+ }
60
+ // work okie
61
+ get(id: string,collectionName: string) {
62
+ return new Promise((resolve, reject) => {
63
+ this.database
64
+ .ref(`${collectionName}/${id}`)
65
+ .once('value')
66
+ .then((snapshot) => {
67
+ const data = snapshot.val();
68
+ if (data) {
69
+ console.log(data);
70
+ resolve({ id, ...data });
71
+ } else {
72
+ resolve(null);
73
+ }
74
+ })
75
+ .catch((error) => {
76
+ reject(error);
77
+ });
78
+ });
79
+ }
80
+
81
+ add(entity, collectionName: string) {
82
+ entity = { ...entity, seqNo: this.getCurrentTimestamp(), deleted: false };
83
+ return new Promise((resolve, reject) => {
84
+ if (entity.id) {
85
+ const saveData = firebaseSerialize(entity);
86
+ this.database
87
+ .ref(`${collectionName}/${entity.id}`)
88
+ .set(saveData)
89
+ .then(() => {
90
+ resolve(entity);
91
+ })
92
+ .catch((error) => {
93
+ reject(error);
94
+ });
95
+ } else {
96
+ const id = uuid();
97
+ const newEntity = { id, ...entity };
98
+ this.database
99
+ .ref(collectionName)
100
+ .push(firebaseSerialize(newEntity))
101
+ .then(() => {
102
+ resolve(newEntity);
103
+ })
104
+ .catch((error) => {
105
+ reject(error);
106
+ });
107
+ }
108
+ });
109
+ }
110
+
111
+ update(entity, collectionName: string) {
112
+ return this.add(entity, collectionName );
113
+ }
114
+
115
+ listBySeq(maxSeqNo, collectionName: string) {
116
+ return new Promise((resolve, reject) => {
117
+ this.database
118
+ .ref(collectionName)
119
+ .orderByChild('seqNo')
120
+ .startAt(maxSeqNo)
121
+ .once('value')
122
+ .then((snapshot) => {
123
+ const data = snapshot.val();
124
+ const list = Object.keys(data).map((key) => ({ id: key, ...data[key] }));
125
+ resolve(list);
126
+ })
127
+ .catch((error) => {
128
+ reject(error);
129
+ });
130
+ });
131
+ }
132
+
133
+ delete(entity,collectionName: string) {
134
+ entity = { ...entity, seqNo: this.getCurrentTimestamp(), deleted: true };
135
+ return new Promise((resolve, reject) => {
136
+ this.database
137
+ .ref(`${collectionName}/${entity.id}`)
138
+ .set(firebaseSerialize(entity))
139
+ .then(() => {
140
+ resolve(entity);
141
+ })
142
+ .catch((error) => {
143
+ reject(error);
144
+ });
145
+ });
146
+ }
147
+ }
@@ -0,0 +1,24 @@
1
+ import { getFirebaseDatabase } from './common.firebase';
2
+ import { RealtimeDbCrud } from "./firebase-crud";
3
+ import { EnvironmentStorageService } from "../utils/environment-storage.service";
4
+
5
+ const COMMON_FUC = 'COMMON-FUNC';
6
+
7
+ export module CommonFunc {
8
+ export const load = async (): Promise<any> => {
9
+ const environmentName = await EnvironmentStorageService.getEnvironmentName();
10
+
11
+ // Khởi tạo DB async
12
+ const database = await getFirebaseDatabase();
13
+ const db = new RealtimeDbCrud<any>(database);
14
+
15
+ const list = await db.list(`ENV/${environmentName}/script`) as any[];
16
+ const l = (list || []).filter((it: any) => it.domain === COMMON_FUC && it.actionType === 'MAIN');
17
+
18
+ if (l.length !== 0) {
19
+ return l[0].code;
20
+ } else {
21
+ return `console.log('empty...${COMMON_FUC}')`;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ import { getFirebaseDatabase } from './common.firebase';
2
+ import { RealtimeDbCrud } from "./firebase-crud";
3
+ import { EnvironmentStorageService } from "../utils/environment-storage.service";
4
+
5
+ export module LoadScriptController {
6
+
7
+ export const loadScriptByDomain = async (domain: string, actionType: string): Promise<any> => {
8
+ const environmentName = await EnvironmentStorageService.getEnvironmentName();
9
+
10
+ // Khởi tạo DB async
11
+ const database = await getFirebaseDatabase();
12
+ const db = new RealtimeDbCrud<any>(database);
13
+
14
+ const list = await db.list(`ENV/${environmentName}/script`) as any[];
15
+ const l = (list || []).filter((it: any) => it.domain === domain && it.actionType === actionType.toUpperCase());
16
+
17
+ if (l.length !== 0) {
18
+ return l[0].code;
19
+ } else {
20
+ return "console.log('empty...')";
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,305 @@
1
+ // Alternative script injection methods for CSP-restricted environments
2
+
3
+ export class ScriptInjector {
4
+
5
+ /**
6
+ * Detect if site has strict CSP that blocks blob/data URLs
7
+ */
8
+ private static detectStrictCSP(): boolean {
9
+ try {
10
+ // Check for CSP meta tags
11
+ const cspMetas = document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]');
12
+ for (let i = 0; i < cspMetas.length; i++) {
13
+ const meta = cspMetas[i];
14
+ const content = meta.getAttribute('content') || '';
15
+ if (content.includes('script-src') && !content.includes('blob:') && !content.includes('data:')) {
16
+ return true;
17
+ }
18
+ }
19
+
20
+ // Check common strict CSP domains
21
+ const strictDomains = [
22
+ 'midjourney.com',
23
+ 'openai.com',
24
+ 'github.com',
25
+ 'google.com',
26
+ 'googletagmanager.com',
27
+ 'facebook.com',
28
+ 'twitter.com',
29
+ 'linkedin.com'
30
+ ];
31
+
32
+ const hostname = window.location.hostname.toLowerCase();
33
+ return strictDomains.some(domain => hostname.includes(domain));
34
+ } catch (error) {
35
+ console.error('CSP detection error:', error);
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Safely get head element or wait for it
42
+ */
43
+ private static getHeadElement(): Promise<HTMLHeadElement> {
44
+ return new Promise((resolve) => {
45
+ const tryGetHead = () => {
46
+ const head = document.head || document.getElementsByTagName('head')[0];
47
+ if (head) {
48
+ resolve(head as HTMLHeadElement);
49
+ return;
50
+ }
51
+
52
+ // If no head, wait for DOM
53
+ if (document.readyState === 'loading') {
54
+ document.addEventListener('DOMContentLoaded', tryGetHead, { once: true });
55
+ } else {
56
+ // Create head if it doesn't exist
57
+ const newHead = document.createElement('head');
58
+ if (document.documentElement) {
59
+ document.documentElement.appendChild(newHead);
60
+ resolve(newHead);
61
+ } else {
62
+ // Last resort: wait a bit and try again
63
+ setTimeout(tryGetHead, 100);
64
+ }
65
+ }
66
+ };
67
+
68
+ tryGetHead();
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Method 1: Blob URL injection (works in most cases)
74
+ */
75
+ static async injectViaBlob(script: string, actionType: string): Promise<boolean> {
76
+ try {
77
+ const blob = new Blob([script], { type: 'application/javascript' });
78
+ const blobUrl = URL.createObjectURL(blob);
79
+
80
+ const scriptElement = document.createElement('script');
81
+ scriptElement.type = 'text/javascript';
82
+ scriptElement.id = actionType;
83
+ scriptElement.src = blobUrl;
84
+
85
+ return new Promise(async (resolve) => {
86
+ scriptElement.onload = () => {
87
+ URL.revokeObjectURL(blobUrl);
88
+ resolve(true);
89
+ };
90
+
91
+ scriptElement.onerror = () => {
92
+ URL.revokeObjectURL(blobUrl);
93
+ resolve(false);
94
+ };
95
+
96
+ try {
97
+ const head = await this.getHeadElement();
98
+ head.appendChild(scriptElement);
99
+ } catch (error) {
100
+ console.error('Failed to get head element:', error);
101
+ URL.revokeObjectURL(blobUrl);
102
+ resolve(false);
103
+ }
104
+ });
105
+ } catch (error) {
106
+ console.error('Blob injection failed:', error);
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Method 2: Data URL injection (fallback)
113
+ */
114
+ static async injectViaDataUrl(script: string, actionType: string): Promise<boolean> {
115
+ try {
116
+ const dataUrl = 'data:application/javascript;base64,' + btoa(script);
117
+ const scriptElement = document.createElement('script');
118
+ scriptElement.type = 'text/javascript';
119
+ scriptElement.id = actionType + '_data';
120
+ scriptElement.src = dataUrl;
121
+
122
+ return new Promise(async (resolve) => {
123
+ scriptElement.onload = () => resolve(true);
124
+ scriptElement.onerror = () => resolve(false);
125
+
126
+ try {
127
+ const head = await this.getHeadElement();
128
+ head.appendChild(scriptElement);
129
+ } catch (error) {
130
+ console.error('Failed to get head element for data URL:', error);
131
+ resolve(false);
132
+ }
133
+ });
134
+ } catch (error) {
135
+ console.error('Data URL injection failed:', error);
136
+ return false;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Method 3: PostMessage bridge injection
142
+ */
143
+ static injectViaPostMessage(script: string, actionType: string): Promise<boolean> {
144
+ return new Promise((resolve) => {
145
+ try {
146
+ // Create a bridge script that listens for postMessage
147
+ const bridgeScript = `
148
+ window.addEventListener('message', function(event) {
149
+ if (event.source !== window || !event.data.type || event.data.type !== 'VETGO_SCRIPT_INJECT') {
150
+ return;
151
+ }
152
+ try {
153
+ const func = new Function(event.data.script);
154
+ func();
155
+ window.postMessage({type: 'VETGO_SCRIPT_SUCCESS', actionType: event.data.actionType}, '*');
156
+ } catch (error) {
157
+ console.error('PostMessage script execution failed:', error);
158
+ window.postMessage({type: 'VETGO_SCRIPT_ERROR', actionType: event.data.actionType, error: error.message}, '*');
159
+ }
160
+ });
161
+ `;
162
+
163
+ // First inject the bridge
164
+ this.injectViaBlob(bridgeScript, actionType + '_bridge').then((bridgeSuccess) => {
165
+ if (bridgeSuccess) {
166
+ // Listen for response
167
+ const responseHandler = (event: MessageEvent) => {
168
+ if (event.source !== window || !event.data.type) return;
169
+
170
+ if (event.data.type === 'VETGO_SCRIPT_SUCCESS' && event.data.actionType === actionType) {
171
+ window.removeEventListener('message', responseHandler);
172
+ resolve(true);
173
+ } else if (event.data.type === 'VETGO_SCRIPT_ERROR' && event.data.actionType === actionType) {
174
+ window.removeEventListener('message', responseHandler);
175
+ resolve(false);
176
+ }
177
+ };
178
+
179
+ window.addEventListener('message', responseHandler);
180
+
181
+ // Send script via postMessage
182
+ window.postMessage({
183
+ type: 'VETGO_SCRIPT_INJECT',
184
+ script: script,
185
+ actionType: actionType
186
+ }, '*');
187
+
188
+ // Timeout after 5 seconds
189
+ setTimeout(() => {
190
+ window.removeEventListener('message', responseHandler);
191
+ resolve(false);
192
+ }, 5000);
193
+ } else {
194
+ resolve(false);
195
+ }
196
+ });
197
+ } catch (error) {
198
+ console.error('PostMessage injection failed:', error);
199
+ resolve(false);
200
+ }
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Method 4: Background script injection via chrome.scripting
206
+ */
207
+ static injectViaBackground(script: string, actionType: string): Promise<boolean> {
208
+ return new Promise((resolve) => {
209
+ try {
210
+ chrome.runtime.sendMessage({
211
+ action: "INJECT_SCRIPT",
212
+ script: script,
213
+ actionType: actionType
214
+ }, (response) => {
215
+ if (chrome.runtime.lastError) {
216
+ console.error('Background injection failed:', chrome.runtime.lastError);
217
+ resolve(false);
218
+ } else {
219
+ resolve(response && response.success);
220
+ }
221
+ });
222
+ } catch (error) {
223
+ console.error('Background script injection failed:', error);
224
+ resolve(false);
225
+ }
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Method 5: Direct eval in content script context (last resort)
231
+ */
232
+ static injectViaEval(script: string, actionType: string): Promise<boolean> {
233
+ return new Promise((resolve) => {
234
+ try {
235
+ console.log(`Attempting direct eval for ${actionType}`);
236
+
237
+ // Create isolated function to avoid polluting global scope
238
+ const isolatedExecution = new Function('script', `
239
+ try {
240
+ ${script}
241
+ return true;
242
+ } catch (error) {
243
+ console.error('Eval execution error:', error);
244
+ return false;
245
+ }
246
+ `);
247
+
248
+ const success = isolatedExecution(script);
249
+ resolve(success);
250
+ } catch (error) {
251
+ console.error('Eval injection failed:', error);
252
+ resolve(false);
253
+ }
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Try all methods in smart order based on CSP detection
259
+ */
260
+ static async injectScript(script: string, actionType: string): Promise<boolean> {
261
+ const hasStrictCSP = this.detectStrictCSP();
262
+
263
+ // Smart method ordering based on CSP detection
264
+ const methods = hasStrictCSP ? [
265
+ // For strict CSP sites, try background injection first
266
+ () => this.injectViaBackground(script, actionType),
267
+ () => this.injectViaEval(script, actionType),
268
+ () => this.injectViaPostMessage(script, actionType),
269
+ () => this.injectViaBlob(script, actionType),
270
+ () => this.injectViaDataUrl(script, actionType)
271
+ ] : [
272
+ // For normal sites, try blob first (fastest)
273
+ () => this.injectViaBlob(script, actionType),
274
+ () => this.injectViaDataUrl(script, actionType),
275
+ () => this.injectViaBackground(script, actionType),
276
+ () => this.injectViaEval(script, actionType),
277
+ () => this.injectViaPostMessage(script, actionType)
278
+ ];
279
+
280
+ console.log(`Injecting script for ${actionType} (Strict CSP: ${hasStrictCSP})`);
281
+
282
+ for (let i = 0; i < methods.length; i++) {
283
+ const method = methods[i];
284
+ const methodName = hasStrictCSP ?
285
+ ['Background', 'Eval', 'PostMessage', 'Blob', 'DataURL'][i] :
286
+ ['Blob', 'DataURL', 'Background', 'Eval', 'PostMessage'][i];
287
+
288
+ try {
289
+ console.log(`Trying ${methodName} injection...`);
290
+ const success = await method();
291
+ if (success) {
292
+ console.log(`✅ Script injection successful via ${methodName} for ${actionType}`);
293
+ return true;
294
+ } else {
295
+ console.log(`❌ ${methodName} injection failed for ${actionType}`);
296
+ }
297
+ } catch (error) {
298
+ console.error(`❌ ${methodName} injection error:`, error);
299
+ }
300
+ }
301
+
302
+ console.error(`❌ All injection methods failed for ${actionType}`);
303
+ return false;
304
+ }
305
+ }
@@ -0,0 +1,166 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ display: flex;
12
+ align-items: flex-start;
13
+ justify-content: center;
14
+ padding: 0;
15
+ }
16
+
17
+ .container {
18
+ background: white;
19
+ border-radius: 0;
20
+ box-shadow: none;
21
+ padding: 24px;
22
+ width: 100%;
23
+ min-height: 100vh;
24
+ }
25
+
26
+ h1 {
27
+ color: #333;
28
+ font-size: 24px;
29
+ margin-bottom: 8px;
30
+ font-weight: 600;
31
+ }
32
+
33
+ .subtitle {
34
+ color: #666;
35
+ font-size: 13px;
36
+ margin-bottom: 24px;
37
+ }
38
+
39
+ .form-group {
40
+ margin-bottom: 20px;
41
+ }
42
+
43
+ label {
44
+ display: block;
45
+ color: #444;
46
+ font-size: 13px;
47
+ font-weight: 500;
48
+ margin-bottom: 6px;
49
+ }
50
+
51
+ input[type="text"] {
52
+ width: 100%;
53
+ padding: 10px 12px;
54
+ border: 2px solid #e0e0e0;
55
+ border-radius: 6px;
56
+ font-size: 14px;
57
+ transition: all 0.3s ease;
58
+ outline: none;
59
+ }
60
+
61
+ input[type="text"]:focus {
62
+ border-color: #667eea;
63
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
64
+ }
65
+
66
+ .hint {
67
+ color: #888;
68
+ font-size: 11px;
69
+ margin-top: 4px;
70
+ font-style: italic;
71
+ }
72
+
73
+ .button-group {
74
+ display: flex;
75
+ gap: 10px;
76
+ margin-top: 24px;
77
+ }
78
+
79
+ button {
80
+ flex: 1;
81
+ padding: 10px 20px;
82
+ border: none;
83
+ border-radius: 6px;
84
+ font-size: 14px;
85
+ font-weight: 600;
86
+ cursor: pointer;
87
+ transition: all 0.3s ease;
88
+ outline: none;
89
+ }
90
+
91
+ .btn-primary {
92
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
93
+ color: white;
94
+ }
95
+
96
+ .btn-primary:hover {
97
+ transform: translateY(-1px);
98
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
99
+ }
100
+
101
+ .btn-primary:active {
102
+ transform: translateY(0);
103
+ }
104
+
105
+ .btn-secondary {
106
+ background: #f5f5f5;
107
+ color: #666;
108
+ }
109
+
110
+ .btn-secondary:hover {
111
+ background: #e0e0e0;
112
+ }
113
+
114
+ .message {
115
+ padding: 10px 14px;
116
+ border-radius: 6px;
117
+ margin-bottom: 16px;
118
+ font-size: 13px;
119
+ display: none;
120
+ animation: slideIn 0.3s ease;
121
+ }
122
+
123
+ @keyframes slideIn {
124
+ from {
125
+ opacity: 0;
126
+ transform: translateY(-10px);
127
+ }
128
+ to {
129
+ opacity: 1;
130
+ transform: translateY(0);
131
+ }
132
+ }
133
+
134
+ .message.success {
135
+ background: #d4edda;
136
+ color: #155724;
137
+ border: 1px solid #c3e6cb;
138
+ display: block;
139
+ }
140
+
141
+ .message.error {
142
+ background: #f8d7da;
143
+ color: #721c24;
144
+ border: 1px solid #f5c6cb;
145
+ display: block;
146
+ }
147
+
148
+ .current-value {
149
+ background: #f8f9fa;
150
+ padding: 10px 14px;
151
+ border-radius: 6px;
152
+ margin-bottom: 16px;
153
+ border-left: 4px solid #667eea;
154
+ }
155
+
156
+ .current-value strong {
157
+ color: #667eea;
158
+ font-weight: 600;
159
+ font-size: 13px;
160
+ }
161
+
162
+ .current-value span {
163
+ color: #333;
164
+ font-family: 'Courier New', monospace;
165
+ font-size: 13px;
166
+ }