samarthya-bot 2.2.1 → 2.3.1
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/CHANGELOG.md +25 -0
- package/README.md +340 -353
- package/backend/bin/samarthya.js +29 -10
- package/backend/config/constants.js +4 -4
- package/backend/controllers/chatController.js +72 -0
- package/backend/controllers/telegramController.js +8 -0
- package/backend/package.json +1 -1
- package/backend/public/assets/index-DiMx9ERJ.css +1 -0
- package/backend/public/assets/index-Dn5WYZTH.js +32 -0
- package/backend/public/index.html +186 -16
- package/backend/public/robots.txt +32 -0
- package/backend/public/sitemap.xml +39 -0
- package/backend/services/agent/commandService.js +168 -0
- package/backend/services/llm/llmService.js +8 -0
- package/backend/services/planner/plannerService.js +17 -0
- package/backend/services/security/sandboxService.js +11 -4
- package/backend/services/system/platform.js +150 -0
- package/backend/services/tools/toolRegistry.js +521 -36
- package/backend/services/worker/workerClient.js +141 -29
- package/package.json +1 -1
- package/backend/public/assets/index-6PCzI3K2.js +0 -40
- package/backend/public/assets/index-6TF5jVRQ.js +0 -149
- package/backend/public/assets/index-B0U7rt6f.js +0 -46
- package/backend/public/assets/index-BF0RZh9i.js +0 -149
- package/backend/public/assets/index-BFRAq8Y1.js +0 -149
- package/backend/public/assets/index-CGw8cc8z.js +0 -149
- package/backend/public/assets/index-Ckf0GO1B.css +0 -1
- package/backend/public/assets/index-Cx0Ei-z7.js +0 -149
- package/backend/public/assets/index-DIPdcLv-.js +0 -25
- package/backend/public/assets/index-Da1E-MYB.js +0 -53
- package/backend/public/assets/index-DdCKkq38.js +0 -149
- package/backend/public/assets/index-Do4jNsZS.js +0 -19
- package/backend/public/assets/index-DyjpBYmS.js +0 -51
- package/backend/public/assets/index-DzlXcaXT.js +0 -149
- package/backend/public/assets/index-J7XSVHCz.css +0 -1
- package/backend/public/assets/index-Ui-pyZvK.js +0 -25
- package/backend/public/assets/index-kzffNwzo.js +0 -149
package/backend/bin/samarthya.js
CHANGED
|
@@ -4,6 +4,24 @@ const path = require('path');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
|
|
7
|
+
let platform;
|
|
8
|
+
try {
|
|
9
|
+
platform = require('../services/system/platform');
|
|
10
|
+
} catch (_) {
|
|
11
|
+
// Minimal inline fallback so the CLI still works if the module is missing.
|
|
12
|
+
const isWin = process.platform === 'win32';
|
|
13
|
+
platform = {
|
|
14
|
+
isWindows: isWin,
|
|
15
|
+
killPortCommand: (p) => isWin
|
|
16
|
+
? `for /f "tokens=5" %a in ('netstat -ano ^| findstr :${p} ^| findstr LISTENING') do taskkill /PID %a /F`
|
|
17
|
+
: `lsof -t -i:${p} | xargs kill -9 2>/dev/null || fuser -k ${p}/tcp 2>/dev/null || true`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isWin = process.platform === 'win32';
|
|
22
|
+
// `npm` is a .cmd shim on Windows and must be run through a shell.
|
|
23
|
+
const NPM = isWin ? 'npm.cmd' : 'npm';
|
|
24
|
+
|
|
7
25
|
const args = process.argv.slice(2);
|
|
8
26
|
const command = args[0];
|
|
9
27
|
const backendDir = path.join(__dirname, '..');
|
|
@@ -370,7 +388,10 @@ switch (command) {
|
|
|
370
388
|
const spin2 = new Spinner('Installing dependencies...');
|
|
371
389
|
spin2.start();
|
|
372
390
|
try {
|
|
373
|
-
|
|
391
|
+
// stdio:'ignore' already silences output — no shell-specific redirection
|
|
392
|
+
// (`2>/dev/null` is invalid on Windows cmd). `--omit=dev` replaces the
|
|
393
|
+
// deprecated `--production` flag and works on all platforms.
|
|
394
|
+
execSync(`${NPM} install --omit=dev`, { cwd: backendDir, stdio: 'ignore', shell: true });
|
|
374
395
|
spin2.stop('Dependencies installed');
|
|
375
396
|
} catch {
|
|
376
397
|
spin2.fail('Dependencies install failed (non-critical)');
|
|
@@ -409,7 +430,7 @@ switch (command) {
|
|
|
409
430
|
// Load .env and spawn the server
|
|
410
431
|
try { require('dotenv').config({ path: envPath }); } catch (e) { }
|
|
411
432
|
|
|
412
|
-
const gatewayChild = spawn(
|
|
433
|
+
const gatewayChild = spawn(process.execPath, ['server.js'], {
|
|
413
434
|
cwd: backendDir,
|
|
414
435
|
stdio: 'inherit'
|
|
415
436
|
});
|
|
@@ -422,9 +443,8 @@ switch (command) {
|
|
|
422
443
|
setTimeout(() => {
|
|
423
444
|
if (envVars['TELEGRAM_BOT_TOKEN'] && envVars['TELEGRAM_BOT_TOKEN'] !== 'dummy') {
|
|
424
445
|
info('Auto-starting tunnel for Telegram webhook...');
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
stdio: 'pipe', shell: isWin
|
|
446
|
+
const tunnelChild = spawn(NPM, ['exec', 'localtunnel', '--', '--port', '5000'], {
|
|
447
|
+
stdio: 'pipe', shell: true
|
|
428
448
|
});
|
|
429
449
|
tunnelChild.stdout.on('data', async (data) => {
|
|
430
450
|
const output = data.toString();
|
|
@@ -651,7 +671,7 @@ switch (command) {
|
|
|
651
671
|
console.log(` ${c.dim}Provider: ${c.saffron}${prov}${c.reset} ${c.dim}(${mod})${c.reset}`);
|
|
652
672
|
console.log(` ${c.dim}Dashboard: ${c.cyan}${c.under}http://localhost:5000${c.reset}\n`);
|
|
653
673
|
|
|
654
|
-
const child = spawn(
|
|
674
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
655
675
|
cwd: backendDir,
|
|
656
676
|
stdio: 'inherit'
|
|
657
677
|
});
|
|
@@ -698,9 +718,8 @@ switch (command) {
|
|
|
698
718
|
const spin = new Spinner('Connecting to tunnel service...');
|
|
699
719
|
spin.start();
|
|
700
720
|
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
stdio: 'pipe', shell: isWin
|
|
721
|
+
const tunnelProcess = spawn(NPM, ['exec', 'localtunnel', '--', '--port', '5000'], {
|
|
722
|
+
stdio: 'pipe', shell: true
|
|
704
723
|
});
|
|
705
724
|
|
|
706
725
|
tunnelProcess.stdout.on('data', async (data) => {
|
|
@@ -801,7 +820,7 @@ switch (command) {
|
|
|
801
820
|
|
|
802
821
|
setTimeout(() => {
|
|
803
822
|
spin.stop('Restarted');
|
|
804
|
-
const restartChild = spawn(
|
|
823
|
+
const restartChild = spawn(process.execPath, ['server.js'], {
|
|
805
824
|
cwd: backendDir,
|
|
806
825
|
stdio: 'inherit'
|
|
807
826
|
});
|
|
@@ -5,25 +5,25 @@ module.exports = {
|
|
|
5
5
|
student: {
|
|
6
6
|
name: '🎓 Student Pack',
|
|
7
7
|
nameHi: '🎓 स्टूडेंट पैक',
|
|
8
|
-
tools: ['web_search', 'summarize_text', 'note_take', 'calculate', 'reminder_set', 'file_read', 'file_write', 'file_list', 'weather_info', 'run_command', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
|
|
8
|
+
tools: ['web_search', 'summarize_text', 'note_take', 'calculate', 'reminder_set', 'file_read', 'file_write', 'file_list', 'weather_info', 'run_command', 'open_path', 'http_request', 'translate_text', 'currency_convert', 'timezone_now', 'qr_generate', 'base64_tool', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
|
|
9
9
|
description: 'Assignment summary, notes, exam reminders, calculations'
|
|
10
10
|
},
|
|
11
11
|
business: {
|
|
12
12
|
name: '🏢 Small Business Pack',
|
|
13
13
|
nameHi: '🏢 बिज़नेस पैक',
|
|
14
|
-
tools: ['web_search', 'send_email', 'calendar_schedule', 'gst_reminder', 'calculate', 'file_read', 'file_write', 'file_list', 'upi_generate', 'note_take', 'reminder_set', 'weather_info', 'system_info', 'run_command', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
|
|
14
|
+
tools: ['web_search', 'send_email', 'calendar_schedule', 'gst_reminder', 'calculate', 'file_read', 'file_write', 'file_list', 'upi_generate', 'qr_generate', 'currency_convert', 'note_take', 'reminder_set', 'weather_info', 'system_info', 'run_command', 'open_path', 'http_request', 'password_generate', 'url_shorten', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
|
|
15
15
|
description: 'GST reminders, email, UPI links, file management'
|
|
16
16
|
},
|
|
17
17
|
developer: {
|
|
18
18
|
name: '👨💻 Developer Pack',
|
|
19
19
|
nameHi: '👨💻 डेवलपर पैक',
|
|
20
|
-
tools: ['web_search', 'file_read', 'file_write', 'file_list', 'calculate', 'send_email', 'run_command', 'system_info', 'browser_action', 'ssh_deploy', 'note_take', 'reminder_set', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
|
|
20
|
+
tools: ['web_search', 'file_read', 'file_write', 'file_list', 'calculate', 'send_email', 'run_command', 'devops_execute_stream', 'open_path', 'http_request', 'password_generate', 'hash_text', 'base64_tool', 'url_shorten', 'ip_geolocate', 'crypto_price', 'system_info', 'browser_action', 'ssh_deploy', 'note_take', 'reminder_set', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
|
|
21
21
|
description: 'Shell commands, file ops, system info, browser control'
|
|
22
22
|
},
|
|
23
23
|
personal: {
|
|
24
24
|
name: '🏠 Personal Pack',
|
|
25
25
|
nameHi: '🏠 पर्सनल पैक',
|
|
26
|
-
tools: ['web_search', 'reminder_set', 'note_take', 'calculate', 'weather_info', 'file_read', 'file_write', 'file_list', 'calendar_schedule', 'upi_generate', 'send_email', 'system_info', 'browser_action', 'run_command', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
|
|
26
|
+
tools: ['web_search', 'reminder_set', 'note_take', 'calculate', 'weather_info', 'file_read', 'file_write', 'file_list', 'calendar_schedule', 'upi_generate', 'qr_generate', 'send_email', 'system_info', 'browser_action', 'run_command', 'open_path', 'http_request', 'password_generate', 'translate_text', 'currency_convert', 'crypto_price', 'url_shorten', 'ip_geolocate', 'timezone_now', 'clipboard_copy', 'capture_desktop_screenshot', 'schedule_background_task', 'simulate_task'],
|
|
27
27
|
description: 'Weather, notes, reminders, UPI, email, files'
|
|
28
28
|
}
|
|
29
29
|
},
|
|
@@ -3,6 +3,71 @@ const User = require('../models/User');
|
|
|
3
3
|
const plannerService = require('../services/planner/plannerService');
|
|
4
4
|
const securityService = require('../services/security/securityService');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Handle a message arriving from an external channel (Discord, etc.).
|
|
8
|
+
* Resolves/creates a per-channel user, routes through the agentic planner
|
|
9
|
+
* (so slash-commands like /help, /status, /new all work), and persists the
|
|
10
|
+
* conversation. Returns the assistant's text reply.
|
|
11
|
+
*/
|
|
12
|
+
exports.handleExternalMessage = async (message, externalUserId, channel = 'external') => {
|
|
13
|
+
try {
|
|
14
|
+
if (!message || !message.trim()) return '🤔 Empty message.';
|
|
15
|
+
|
|
16
|
+
const email = `${channel}_${externalUserId}@samarthya.local`;
|
|
17
|
+
let user = await User.findOne({ email });
|
|
18
|
+
if (!user) {
|
|
19
|
+
user = await User.create({
|
|
20
|
+
name: `${channel} user`,
|
|
21
|
+
email,
|
|
22
|
+
password: `${channel}_user`,
|
|
23
|
+
language: 'hinglish',
|
|
24
|
+
workType: 'personal',
|
|
25
|
+
activePack: 'personal',
|
|
26
|
+
source: channel
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Most recent active conversation for this channel (last 24h)
|
|
31
|
+
let conversation = await Conversation.findOne({
|
|
32
|
+
userId: user._id,
|
|
33
|
+
source: channel,
|
|
34
|
+
isActive: { $ne: false },
|
|
35
|
+
updatedAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
|
|
36
|
+
}).sort({ updatedAt: -1 });
|
|
37
|
+
|
|
38
|
+
const previousMessages = conversation?.messages?.slice(-6)?.map(m => ({
|
|
39
|
+
role: m.role, content: m.content
|
|
40
|
+
})) || [];
|
|
41
|
+
|
|
42
|
+
const result = await plannerService.processMessage(user, previousMessages, message);
|
|
43
|
+
|
|
44
|
+
// Honour a /new (reset) slash-command: deactivate the old thread so the
|
|
45
|
+
// next message starts a fresh conversation.
|
|
46
|
+
if (result.command?.action === 'new_conversation') {
|
|
47
|
+
if (conversation) { conversation.isActive = false; await conversation.save(); }
|
|
48
|
+
return result.response;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!conversation) {
|
|
52
|
+
conversation = new Conversation({ userId: user._id, title: message.substring(0, 40), source: channel, messages: [] });
|
|
53
|
+
}
|
|
54
|
+
conversation.messages.push({ role: 'user', content: message });
|
|
55
|
+
conversation.messages.push({
|
|
56
|
+
role: 'assistant',
|
|
57
|
+
content: result.response,
|
|
58
|
+
toolCalls: result.toolCalls,
|
|
59
|
+
language: result.language,
|
|
60
|
+
metadata: { tokensUsed: result.tokensUsed, model: result.model }
|
|
61
|
+
});
|
|
62
|
+
await conversation.save();
|
|
63
|
+
|
|
64
|
+
return result.response;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`External message (${channel}) error:`, error.message);
|
|
67
|
+
return '❌ Kuch error aa gaya processing mein. Please try again.';
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
6
71
|
// Send message and get AI response
|
|
7
72
|
exports.sendMessage = async (req, res) => {
|
|
8
73
|
try {
|
|
@@ -63,6 +128,12 @@ exports.sendMessage = async (req, res) => {
|
|
|
63
128
|
}
|
|
64
129
|
});
|
|
65
130
|
|
|
131
|
+
// Honour a /new (reset) slash-command on the web channel: deactivate the
|
|
132
|
+
// current conversation so the UI knows to open a fresh one.
|
|
133
|
+
if (result.command?.action === 'new_conversation') {
|
|
134
|
+
conversation.isActive = false;
|
|
135
|
+
}
|
|
136
|
+
|
|
66
137
|
await conversation.save();
|
|
67
138
|
|
|
68
139
|
// Emit via socket if available
|
|
@@ -82,6 +153,7 @@ exports.sendMessage = async (req, res) => {
|
|
|
82
153
|
res.json({
|
|
83
154
|
success: true,
|
|
84
155
|
conversationId: conversation._id,
|
|
156
|
+
command: result.command || null,
|
|
85
157
|
message: {
|
|
86
158
|
role: 'assistant',
|
|
87
159
|
content: result.response,
|
|
@@ -70,6 +70,7 @@ exports.handleMessage = async (req, res) => {
|
|
|
70
70
|
let conversation = await Conversation.findOne({
|
|
71
71
|
userId: user._id,
|
|
72
72
|
source: 'telegram',
|
|
73
|
+
isActive: { $ne: false },
|
|
73
74
|
updatedAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
|
|
74
75
|
}).sort({ updatedAt: -1 });
|
|
75
76
|
|
|
@@ -97,6 +98,13 @@ exports.handleMessage = async (req, res) => {
|
|
|
97
98
|
|
|
98
99
|
await telegramService.sendMessage(chatId, replyText);
|
|
99
100
|
|
|
101
|
+
// Honour a /new (reset) slash-command: deactivate the current thread so
|
|
102
|
+
// the next message starts fresh, and skip saving into the old one.
|
|
103
|
+
if (result.command?.action === 'new_conversation') {
|
|
104
|
+
if (conversation) { conversation.isActive = false; await conversation.save(); }
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
// Save conversation
|
|
101
109
|
if (!conversation) {
|
|
102
110
|
conversation = new Conversation({ userId: user._id, title: text.substring(0, 20), source: 'telegram', messages: [] });
|
package/backend/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg-primary: #06060a;--bg-secondary: #0e0e14;--bg-tertiary: #12121c;--bg-card: #0e0e16;--bg-hover: #14141e;--bg-input: #0a0a12;--accent-saffron: #FF9933;--accent-saffron-glow: rgba(255, 153, 51, .25);--accent-navy: #000080;--accent-green: #138808;--accent-green-glow: rgba(19, 136, 8, .2);--accent-primary: #FF9933;--accent-secondary: #e68a2e;--accent-white: #ffffff;--accent-gold: #f5c842;--accent-teal: #14b8a6;--accent-rose: #f43f5e;--accent-indigo: #6366f1;--gradient-primary: linear-gradient(135deg, #FF9933 0%, #e68a2e 50%, #f5c842 100%);--gradient-indian: linear-gradient(135deg, #FF9933 0%, #ffffff 50%, #138808 100%);--gradient-navy: linear-gradient(135deg, #000080 0%, #1a1a6e 100%);--gradient-glass: linear-gradient(135deg, rgba(255, 153, 51, .06) 0%, rgba(19, 136, 8, .03) 100%);--gradient-dark: linear-gradient(180deg, #0D0D0D 0%, #111118 100%);--gradient-hero: linear-gradient(135deg, rgba(255, 153, 51, .08) 0%, rgba(0, 0, 128, .05) 50%, rgba(19, 136, 8, .04) 100%);--gradient-card-hover: linear-gradient(135deg, rgba(255, 153, 51, .1) 0%, rgba(255, 153, 51, .02) 100%);--text-primary: #f0f0ff;--text-secondary: #8888a8;--text-muted: #3a3a4e;--text-accent: #FF9933;--border-primary: rgba(255, 153, 51, .18);--border-subtle: #1e1e28;--border-hover: rgba(255, 153, 51, .4);--shadow-sm: 0 2px 8px rgba(0, 0, 0, .4);--shadow-md: 0 4px 20px rgba(0, 0, 0, .5);--shadow-lg: 0 8px 40px rgba(0, 0, 0, .6);--shadow-glow: 0 0 24px rgba(255, 153, 51, .12);--shadow-glow-strong: 0 0 48px rgba(255, 153, 51, .22);--shadow-green-glow: 0 0 20px rgba(19, 136, 8, .15);--font-primary: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;--font-hindi: "Noto Sans Devanagari", "Inter", sans-serif;--radius-sm: 8px;--radius-md: 12px;--radius-lg: 16px;--radius-xl: 24px;--radius-full: 9999px;--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-normal: .25s cubic-bezier(.4, 0, .2, 1);--transition-slow: .4s cubic-bezier(.4, 0, .2, 1);--transition-spring: .5s cubic-bezier(.34, 1.56, .64, 1)}*,*:before,*:after{margin:0;padding:0;box-sizing:border-box}html{font-size:16px;scroll-behavior:smooth;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font-family:var(--font-primary);background:var(--bg-primary);color:var(--text-primary);min-height:100vh;overflow-x:hidden;line-height:1.6;position:relative}body:before{content:"";position:fixed;inset:0;z-index:-2;pointer-events:none;background:radial-gradient(40% 50% at 12% 8%,rgba(255,153,51,.1),transparent 70%),radial-gradient(45% 55% at 88% 12%,rgba(99,102,241,.08),transparent 70%),radial-gradient(50% 60% at 50% 100%,rgba(19,136,8,.08),transparent 70%);background-size:200% 200%;animation:auroraDrift 24s ease-in-out infinite}body:after{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;background-image:linear-gradient(rgba(255,255,255,.015) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.015) 1px,transparent 1px);background-size:44px 44px;mask-image:radial-gradient(ellipse 80% 60% at 50% 0%,#000 30%,transparent 80%);-webkit-mask-image:radial-gradient(ellipse 80% 60% at 50% 0%,#000 30%,transparent 80%)}@keyframes auroraDrift{0%,to{background-position:0% 0%,100% 0%,50% 100%}50%{background-position:20% 30%,70% 40%,40% 70%}}@media(prefers-reduced-motion:reduce){body:before{animation:none}}#root{min-height:100vh;display:flex;flex-direction:column;position:relative;z-index:0}::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:var(--bg-primary)}::-webkit-scrollbar-thumb{background:#f933;border-radius:var(--radius-full)}::-webkit-scrollbar-thumb:hover{background:var(--accent-saffron)}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeInDown{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}@keyframes slideInLeft{0%{opacity:0;transform:translate(-24px)}to{opacity:1;transform:translate(0)}}@keyframes slideInRight{0%{opacity:0;transform:translate(24px)}to{opacity:1;transform:translate(0)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}@keyframes float{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes glow{0%,to{box-shadow:0 0 5px #ff993326}50%{box-shadow:0 0 25px #ff993359}}@keyframes typing{0%,60%,to{opacity:.3;transform:scale(.8)}30%{opacity:1;transform:scale(1)}}@keyframes ripple{0%{transform:scale(0);opacity:.5}to{transform:scale(4);opacity:0}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes bounceIn{0%{transform:scale(.3);opacity:0}50%{transform:scale(1.05)}70%{transform:scale(.9)}to{transform:scale(1);opacity:1}}@keyframes typewriter{0%{width:0}to{width:100%}}@keyframes blink{50%{border-color:transparent}}@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}to{background-position:0% 50%}}@keyframes scaleIn{0%{transform:scale(.9);opacity:0}to{transform:scale(1);opacity:1}}@keyframes slideUp{0%{transform:translateY(40px);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes voicePulse{0%{box-shadow:0 0 #ff993380}70%{box-shadow:0 0 0 20px #f930}to{box-shadow:0 0 #f930}}@keyframes pulseAlarm{0%{transform:scale(1);box-shadow:0 0 20px #ef444466}to{transform:scale(1.02);box-shadow:0 0 40px #f939}}@keyframes ringAlarm{0%{transform:rotate(0)}25%{transform:rotate(15deg)}50%{transform:rotate(-15deg)}75%{transform:rotate(10deg)}to{transform:rotate(-10deg)}}.animate-fade-in{animation:fadeIn .4s ease-out}.animate-fade-in-up{animation:fadeInUp .5s ease-out}.animate-slide-left{animation:slideInLeft .4s ease-out}.animate-slide-right{animation:slideInRight .4s ease-out}.animate-bounce-in{animation:bounceIn .6s ease-out}.animate-scale-in{animation:scaleIn .3s ease-out}.glass{background:#1a1a2e8c;backdrop-filter:blur(24px) saturate(140%);-webkit-backdrop-filter:blur(24px) saturate(140%);border:1px solid var(--border-subtle);box-shadow:inset 0 1px #ffffff0a,var(--shadow-md)}.glass-strong{background:#0d0d0dc7;backdrop-filter:blur(40px) saturate(150%);-webkit-backdrop-filter:blur(40px) saturate(150%);border:1px solid var(--border-primary);box-shadow:inset 0 1px #ffffff0d,var(--shadow-lg)}.gradient-text-animated{background:linear-gradient(90deg,#f93,#f5c842,#138808,#f93);background-size:300% 100%;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;animation:gradientShift 6s ease infinite}.lift{transition:transform var(--transition-normal),box-shadow var(--transition-normal),border-color var(--transition-normal)}.lift:hover{transform:translateY(-3px);box-shadow:var(--shadow-lg),var(--shadow-glow);border-color:var(--border-hover)}.gradient-text{background:var(--gradient-primary);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.gradient-text-indian{background:var(--gradient-indian);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 24px;border:none;border-radius:var(--radius-md);font-family:var(--font-primary);font-size:.9rem;font-weight:600;cursor:pointer;transition:all var(--transition-normal);position:relative;overflow:hidden;letter-spacing:.01em}.btn:after{content:"";position:absolute;width:100%;height:100%;top:0;left:0;background:radial-gradient(circle,rgba(255,255,255,.12) 10%,transparent 60%);transform:scale(0);opacity:0;transition:all .5s}.btn:active:after{transform:scale(4);opacity:0;transition:0s}.btn:before{content:"";position:absolute;top:0;left:-75%;width:50%;height:100%;background:linear-gradient(120deg,transparent,rgba(255,255,255,.25),transparent);transform:skew(-20deg);transition:left .6s ease;pointer-events:none}.btn:hover:before{left:130%}.btn-primary{background:var(--gradient-primary);background-size:150% 150%;color:#0d0d0d;box-shadow:var(--shadow-glow)}.btn-primary:hover{box-shadow:var(--shadow-glow-strong);transform:translateY(-2px);background-position:100% 0}.btn-secondary{background:var(--bg-card);color:var(--text-primary);border:1px solid var(--border-primary)}.btn-secondary:hover{background:var(--bg-hover);border-color:var(--border-hover);transform:translateY(-1px)}.btn-ghost{background:transparent;color:var(--text-secondary)}.btn-ghost:hover{background:var(--bg-hover);color:var(--text-primary)}.input{width:100%;padding:12px 16px;background:var(--bg-input);border:1px solid var(--border-subtle);border-radius:var(--radius-md);color:var(--text-primary);font-family:var(--font-primary);font-size:.9375rem;transition:all var(--transition-normal);outline:none}.input:focus{border-color:var(--accent-saffron);box-shadow:0 0 0 3px var(--accent-saffron-glow)}.input::placeholder{color:var(--text-muted)}.badge{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:var(--radius-full);font-size:.75rem;font-weight:600;letter-spacing:.02em}.badge-low{background:#1388081f;color:#138808;border:1px solid rgba(19,136,8,.25)}.badge-medium{background:#ff99331f;color:#f93;border:1px solid rgba(255,153,51,.25)}.badge-high{background:#ef44441f;color:#ef4444;border:1px solid rgba(239,68,68,.25)}.badge-critical{background:#f43f5e1f;color:#f43f5e;border:1px solid rgba(244,63,94,.25);animation:glow 2s infinite}@media(max-width:768px){html{font-size:14px}}@media(max-width:480px){html{font-size:13px}}
|