Upload 301 files
Browse files- ai_tools.html +802 -0
- api_server_extended.py +193 -27
- templates/ai_tools.html +802 -0
- templates/index.html +4 -0
ai_tools.html
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>AI Tools - Crypto Intelligence Hub</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
| 16 |
+
background: linear-gradient(135deg, #050816 0%, #0a1128 100%);
|
| 17 |
+
color: #e2e8f0;
|
| 18 |
+
min-height: 100vh;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.header {
|
| 23 |
+
text-align: center;
|
| 24 |
+
margin-bottom: 40px;
|
| 25 |
+
padding: 30px 20px;
|
| 26 |
+
background: rgba(15, 23, 42, 0.6);
|
| 27 |
+
border-radius: 16px;
|
| 28 |
+
backdrop-filter: blur(10px);
|
| 29 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.header h1 {
|
| 33 |
+
font-size: 2.5rem;
|
| 34 |
+
font-weight: 700;
|
| 35 |
+
margin-bottom: 10px;
|
| 36 |
+
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
|
| 37 |
+
-webkit-background-clip: text;
|
| 38 |
+
-webkit-text-fill-color: transparent;
|
| 39 |
+
background-clip: text;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.header p {
|
| 43 |
+
color: #94a3b8;
|
| 44 |
+
font-size: 1.1rem;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.container {
|
| 48 |
+
max-width: 1400px;
|
| 49 |
+
margin: 0 auto;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.card {
|
| 53 |
+
background: rgba(15, 23, 42, 0.8);
|
| 54 |
+
border-radius: 16px;
|
| 55 |
+
padding: 30px;
|
| 56 |
+
margin-bottom: 30px;
|
| 57 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 58 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
| 59 |
+
backdrop-filter: blur(10px);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.card-title {
|
| 63 |
+
font-size: 1.8rem;
|
| 64 |
+
font-weight: 600;
|
| 65 |
+
margin-bottom: 25px;
|
| 66 |
+
color: #f1f5f9;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.form-group {
|
| 70 |
+
margin-bottom: 20px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.form-label {
|
| 74 |
+
display: block;
|
| 75 |
+
margin-bottom: 8px;
|
| 76 |
+
color: #cbd5e1;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
font-size: 0.95rem;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.form-input,
|
| 82 |
+
.form-textarea,
|
| 83 |
+
.form-select {
|
| 84 |
+
width: 100%;
|
| 85 |
+
padding: 12px 16px;
|
| 86 |
+
background: rgba(30, 41, 59, 0.8);
|
| 87 |
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
| 88 |
+
border-radius: 8px;
|
| 89 |
+
color: #e2e8f0;
|
| 90 |
+
font-size: 1rem;
|
| 91 |
+
transition: all 0.3s ease;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.form-input:focus,
|
| 95 |
+
.form-textarea:focus,
|
| 96 |
+
.form-select:focus {
|
| 97 |
+
outline: none;
|
| 98 |
+
border-color: #60a5fa;
|
| 99 |
+
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.form-textarea {
|
| 103 |
+
min-height: 120px;
|
| 104 |
+
resize: vertical;
|
| 105 |
+
font-family: inherit;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.btn {
|
| 109 |
+
padding: 12px 24px;
|
| 110 |
+
font-size: 1rem;
|
| 111 |
+
font-weight: 600;
|
| 112 |
+
border: none;
|
| 113 |
+
border-radius: 8px;
|
| 114 |
+
cursor: pointer;
|
| 115 |
+
transition: all 0.3s ease;
|
| 116 |
+
display: inline-flex;
|
| 117 |
+
align-items: center;
|
| 118 |
+
gap: 8px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.btn-primary {
|
| 122 |
+
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
| 123 |
+
color: white;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.btn-primary:hover:not(:disabled) {
|
| 127 |
+
transform: translateY(-2px);
|
| 128 |
+
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.btn-primary:disabled {
|
| 132 |
+
opacity: 0.6;
|
| 133 |
+
cursor: not-allowed;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.btn-secondary {
|
| 137 |
+
background: rgba(71, 85, 105, 0.8);
|
| 138 |
+
color: #e2e8f0;
|
| 139 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.btn-secondary:hover:not(:disabled) {
|
| 143 |
+
background: rgba(100, 116, 139, 0.9);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.result-box {
|
| 147 |
+
margin-top: 25px;
|
| 148 |
+
padding: 20px;
|
| 149 |
+
background: rgba(30, 41, 59, 0.6);
|
| 150 |
+
border-radius: 12px;
|
| 151 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.error-box {
|
| 155 |
+
margin-top: 25px;
|
| 156 |
+
padding: 16px;
|
| 157 |
+
background: rgba(239, 68, 68, 0.1);
|
| 158 |
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 159 |
+
border-radius: 8px;
|
| 160 |
+
color: #fca5a5;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.success-box {
|
| 164 |
+
margin-top: 25px;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
background: rgba(34, 197, 94, 0.1);
|
| 167 |
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
| 168 |
+
border-radius: 12px;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.badge {
|
| 172 |
+
display: inline-block;
|
| 173 |
+
padding: 6px 14px;
|
| 174 |
+
border-radius: 20px;
|
| 175 |
+
font-size: 0.9rem;
|
| 176 |
+
font-weight: 600;
|
| 177 |
+
margin-right: 10px;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.badge-positive {
|
| 181 |
+
background: rgba(34, 197, 94, 0.2);
|
| 182 |
+
color: #4ade80;
|
| 183 |
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.badge-negative {
|
| 187 |
+
background: rgba(239, 68, 68, 0.2);
|
| 188 |
+
color: #f87171;
|
| 189 |
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.badge-neutral {
|
| 193 |
+
background: rgba(148, 163, 184, 0.2);
|
| 194 |
+
color: #94a3b8;
|
| 195 |
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.badge-success {
|
| 199 |
+
background: rgba(34, 197, 94, 0.2);
|
| 200 |
+
color: #4ade80;
|
| 201 |
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.badge-danger {
|
| 205 |
+
background: rgba(239, 68, 68, 0.2);
|
| 206 |
+
color: #f87171;
|
| 207 |
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.score-bar {
|
| 211 |
+
margin-top: 15px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.score-item {
|
| 215 |
+
display: flex;
|
| 216 |
+
align-items: center;
|
| 217 |
+
margin-bottom: 8px;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.score-label {
|
| 221 |
+
min-width: 80px;
|
| 222 |
+
font-size: 0.9rem;
|
| 223 |
+
color: #cbd5e1;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.score-progress {
|
| 227 |
+
flex: 1;
|
| 228 |
+
height: 8px;
|
| 229 |
+
background: rgba(30, 41, 59, 0.8);
|
| 230 |
+
border-radius: 4px;
|
| 231 |
+
overflow: hidden;
|
| 232 |
+
margin: 0 12px;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.score-fill {
|
| 236 |
+
height: 100%;
|
| 237 |
+
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
|
| 238 |
+
border-radius: 4px;
|
| 239 |
+
transition: width 0.5s ease;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.score-value {
|
| 243 |
+
min-width: 50px;
|
| 244 |
+
text-align: right;
|
| 245 |
+
font-weight: 600;
|
| 246 |
+
color: #e2e8f0;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.table-container {
|
| 250 |
+
overflow-x: auto;
|
| 251 |
+
margin-top: 20px;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
table {
|
| 255 |
+
width: 100%;
|
| 256 |
+
border-collapse: collapse;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
th {
|
| 260 |
+
background: rgba(30, 41, 59, 0.8);
|
| 261 |
+
padding: 12px;
|
| 262 |
+
text-align: left;
|
| 263 |
+
font-weight: 600;
|
| 264 |
+
color: #f1f5f9;
|
| 265 |
+
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
td {
|
| 269 |
+
padding: 12px;
|
| 270 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| 271 |
+
color: #cbd5e1;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
tr:hover {
|
| 275 |
+
background: rgba(30, 41, 59, 0.4);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.info-box {
|
| 279 |
+
padding: 16px;
|
| 280 |
+
background: rgba(59, 130, 246, 0.1);
|
| 281 |
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
| 282 |
+
border-radius: 8px;
|
| 283 |
+
margin: 15px 0;
|
| 284 |
+
color: #93c5fd;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.warning-box {
|
| 288 |
+
padding: 16px;
|
| 289 |
+
background: rgba(251, 191, 36, 0.1);
|
| 290 |
+
border: 1px solid rgba(251, 191, 36, 0.3);
|
| 291 |
+
border-radius: 8px;
|
| 292 |
+
margin: 15px 0;
|
| 293 |
+
color: #fcd34d;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.status-grid {
|
| 297 |
+
display: grid;
|
| 298 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 299 |
+
gap: 15px;
|
| 300 |
+
margin: 20px 0;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.status-item {
|
| 304 |
+
padding: 15px;
|
| 305 |
+
background: rgba(30, 41, 59, 0.6);
|
| 306 |
+
border-radius: 8px;
|
| 307 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.status-label {
|
| 311 |
+
font-size: 0.85rem;
|
| 312 |
+
color: #94a3b8;
|
| 313 |
+
margin-bottom: 5px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.status-value {
|
| 317 |
+
font-size: 1.3rem;
|
| 318 |
+
font-weight: 700;
|
| 319 |
+
color: #f1f5f9;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.summary-text {
|
| 323 |
+
padding: 20px;
|
| 324 |
+
background: rgba(30, 41, 59, 0.8);
|
| 325 |
+
border-radius: 8px;
|
| 326 |
+
border-left: 4px solid #60a5fa;
|
| 327 |
+
font-size: 1.05rem;
|
| 328 |
+
line-height: 1.7;
|
| 329 |
+
color: #e2e8f0;
|
| 330 |
+
margin-bottom: 20px;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.sentences-list {
|
| 334 |
+
list-style: none;
|
| 335 |
+
padding: 0;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.sentences-list li {
|
| 339 |
+
padding: 12px 15px;
|
| 340 |
+
background: rgba(30, 41, 59, 0.6);
|
| 341 |
+
border-radius: 8px;
|
| 342 |
+
margin-bottom: 10px;
|
| 343 |
+
border-left: 3px solid #8b5cf6;
|
| 344 |
+
color: #cbd5e1;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.sentences-list li:before {
|
| 348 |
+
content: "→";
|
| 349 |
+
margin-right: 10px;
|
| 350 |
+
color: #8b5cf6;
|
| 351 |
+
font-weight: bold;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.loading {
|
| 355 |
+
display: inline-block;
|
| 356 |
+
width: 16px;
|
| 357 |
+
height: 16px;
|
| 358 |
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
| 359 |
+
border-top-color: #fff;
|
| 360 |
+
border-radius: 50%;
|
| 361 |
+
animation: spin 0.6s linear infinite;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
@keyframes spin {
|
| 365 |
+
to { transform: rotate(360deg); }
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.two-column {
|
| 369 |
+
display: grid;
|
| 370 |
+
grid-template-columns: 1fr 1fr;
|
| 371 |
+
gap: 20px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
@media (max-width: 768px) {
|
| 375 |
+
.header h1 {
|
| 376 |
+
font-size: 1.8rem;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.header p {
|
| 380 |
+
font-size: 0.95rem;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.card {
|
| 384 |
+
padding: 20px;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.card-title {
|
| 388 |
+
font-size: 1.4rem;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.two-column {
|
| 392 |
+
grid-template-columns: 1fr;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.status-grid {
|
| 396 |
+
grid-template-columns: 1fr;
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.hidden {
|
| 401 |
+
display: none;
|
| 402 |
+
}
|
| 403 |
+
</style>
|
| 404 |
+
</head>
|
| 405 |
+
<body>
|
| 406 |
+
<div class="container">
|
| 407 |
+
<div class="header">
|
| 408 |
+
<h1>AI Tools – Crypto Intelligence Hub</h1>
|
| 409 |
+
<p>Sentiment, Summaries, and Model Diagnostics</p>
|
| 410 |
+
</div>
|
| 411 |
+
|
| 412 |
+
<!-- Sentiment Playground -->
|
| 413 |
+
<div class="card">
|
| 414 |
+
<h2 class="card-title">Sentiment Playground</h2>
|
| 415 |
+
|
| 416 |
+
<div class="form-group">
|
| 417 |
+
<label class="form-label" for="sentiment-input">Enter Text</label>
|
| 418 |
+
<textarea
|
| 419 |
+
id="sentiment-input"
|
| 420 |
+
class="form-textarea"
|
| 421 |
+
placeholder="Enter text to analyze sentiment (tweets, news, or any text)..."
|
| 422 |
+
></textarea>
|
| 423 |
+
</div>
|
| 424 |
+
|
| 425 |
+
<div class="two-column">
|
| 426 |
+
<div class="form-group">
|
| 427 |
+
<label class="form-label" for="sentiment-source">Source Type</label>
|
| 428 |
+
<select id="sentiment-source" class="form-select">
|
| 429 |
+
<option value="user">User Input</option>
|
| 430 |
+
<option value="tweet">Tweet</option>
|
| 431 |
+
<option value="news">News</option>
|
| 432 |
+
</select>
|
| 433 |
+
</div>
|
| 434 |
+
|
| 435 |
+
<div class="form-group">
|
| 436 |
+
<label class="form-label" for="sentiment-model-key">Model Key (Optional)</label>
|
| 437 |
+
<input
|
| 438 |
+
type="text"
|
| 439 |
+
id="sentiment-model-key"
|
| 440 |
+
class="form-input"
|
| 441 |
+
placeholder="Leave empty for default model"
|
| 442 |
+
/>
|
| 443 |
+
</div>
|
| 444 |
+
</div>
|
| 445 |
+
|
| 446 |
+
<button id="analyze-sentiment-btn" class="btn btn-primary">
|
| 447 |
+
Analyze Sentiment
|
| 448 |
+
</button>
|
| 449 |
+
|
| 450 |
+
<div id="sentiment-result" class="hidden"></div>
|
| 451 |
+
</div>
|
| 452 |
+
|
| 453 |
+
<!-- Text Summarizer -->
|
| 454 |
+
<div class="card">
|
| 455 |
+
<h2 class="card-title">Text Summarizer</h2>
|
| 456 |
+
|
| 457 |
+
<div class="form-group">
|
| 458 |
+
<label class="form-label" for="summary-input">Enter Long Text</label>
|
| 459 |
+
<textarea
|
| 460 |
+
id="summary-input"
|
| 461 |
+
class="form-textarea"
|
| 462 |
+
placeholder="Paste article or long text to summarize..."
|
| 463 |
+
style="min-height: 180px;"
|
| 464 |
+
></textarea>
|
| 465 |
+
</div>
|
| 466 |
+
|
| 467 |
+
<div class="form-group">
|
| 468 |
+
<label class="form-label" for="max-sentences">Maximum Sentences</label>
|
| 469 |
+
<select id="max-sentences" class="form-select">
|
| 470 |
+
<option value="2">2 sentences</option>
|
| 471 |
+
<option value="3" selected>3 sentences</option>
|
| 472 |
+
<option value="4">4 sentences</option>
|
| 473 |
+
<option value="5">5 sentences</option>
|
| 474 |
+
</select>
|
| 475 |
+
</div>
|
| 476 |
+
|
| 477 |
+
<button id="summarize-btn" class="btn btn-primary">
|
| 478 |
+
Summarize
|
| 479 |
+
</button>
|
| 480 |
+
|
| 481 |
+
<div id="summary-result" class="hidden"></div>
|
| 482 |
+
</div>
|
| 483 |
+
|
| 484 |
+
<!-- Model Status & Diagnostics -->
|
| 485 |
+
<div class="card">
|
| 486 |
+
<h2 class="card-title">Model Status & Diagnostics</h2>
|
| 487 |
+
|
| 488 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
| 489 |
+
<h3 style="color: #cbd5e1; font-size: 1.2rem;">Registry Status</h3>
|
| 490 |
+
<button id="refresh-status-btn" class="btn btn-secondary">
|
| 491 |
+
Refresh
|
| 492 |
+
</button>
|
| 493 |
+
</div>
|
| 494 |
+
|
| 495 |
+
<div id="registry-status"></div>
|
| 496 |
+
|
| 497 |
+
<h3 style="color: #cbd5e1; font-size: 1.2rem; margin: 30px 0 15px 0;">Models Table</h3>
|
| 498 |
+
<div id="models-table"></div>
|
| 499 |
+
</div>
|
| 500 |
+
</div>
|
| 501 |
+
|
| 502 |
+
<script>
|
| 503 |
+
(function() {
|
| 504 |
+
'use strict';
|
| 505 |
+
|
| 506 |
+
const AITools = {
|
| 507 |
+
// Sentiment Analysis
|
| 508 |
+
async analyzeSentiment() {
|
| 509 |
+
const text = document.getElementById('sentiment-input').value.trim();
|
| 510 |
+
const source = document.getElementById('sentiment-source').value;
|
| 511 |
+
const modelKey = document.getElementById('sentiment-model-key').value.trim();
|
| 512 |
+
const btn = document.getElementById('analyze-sentiment-btn');
|
| 513 |
+
const resultDiv = document.getElementById('sentiment-result');
|
| 514 |
+
|
| 515 |
+
if (!text) {
|
| 516 |
+
this.showError(resultDiv, 'Please enter text to analyze');
|
| 517 |
+
return;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
btn.disabled = true;
|
| 521 |
+
btn.innerHTML = '<span class="loading"></span> Analyzing...';
|
| 522 |
+
resultDiv.classList.add('hidden');
|
| 523 |
+
|
| 524 |
+
try {
|
| 525 |
+
const payload = { text, source };
|
| 526 |
+
if (modelKey) payload.model_key = modelKey;
|
| 527 |
+
|
| 528 |
+
const response = await fetch('/api/sentiment/analyze', {
|
| 529 |
+
method: 'POST',
|
| 530 |
+
headers: { 'Content-Type': 'application/json' },
|
| 531 |
+
body: JSON.stringify(payload)
|
| 532 |
+
});
|
| 533 |
+
|
| 534 |
+
const data = await response.json();
|
| 535 |
+
|
| 536 |
+
if (!response.ok || !data.ok) {
|
| 537 |
+
throw new Error(data.error || 'Sentiment analysis failed');
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
this.displaySentimentResult(resultDiv, data);
|
| 541 |
+
} catch (error) {
|
| 542 |
+
this.showError(resultDiv, error.message);
|
| 543 |
+
} finally {
|
| 544 |
+
btn.disabled = false;
|
| 545 |
+
btn.innerHTML = 'Analyze Sentiment';
|
| 546 |
+
}
|
| 547 |
+
},
|
| 548 |
+
|
| 549 |
+
displaySentimentResult(container, data) {
|
| 550 |
+
const label = data.label || 'unknown';
|
| 551 |
+
const score = (data.score * 100).toFixed(1);
|
| 552 |
+
const labelClass = label.toLowerCase();
|
| 553 |
+
|
| 554 |
+
let html = '<div class="result-box">';
|
| 555 |
+
html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Sentiment Analysis Result</h3>';
|
| 556 |
+
html += `<div style="margin-bottom: 15px;">`;
|
| 557 |
+
html += `<span class="badge badge-${labelClass}">${label.toUpperCase()}</span>`;
|
| 558 |
+
html += `<span style="font-size: 1.3rem; font-weight: 700; color: #e2e8f0;">${score}%</span>`;
|
| 559 |
+
html += `</div>`;
|
| 560 |
+
|
| 561 |
+
if (data.model) {
|
| 562 |
+
html += `<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 15px;">Model: ${data.model}</p>`;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
if (data.details && data.details.labels && data.details.scores) {
|
| 566 |
+
html += '<div class="score-bar">';
|
| 567 |
+
for (let i = 0; i < data.details.labels.length; i++) {
|
| 568 |
+
const lbl = data.details.labels[i];
|
| 569 |
+
const scr = (data.details.scores[i] * 100).toFixed(1);
|
| 570 |
+
html += '<div class="score-item">';
|
| 571 |
+
html += `<span class="score-label">${lbl}</span>`;
|
| 572 |
+
html += '<div class="score-progress">';
|
| 573 |
+
html += `<div class="score-fill" style="width: ${scr}%"></div>`;
|
| 574 |
+
html += '</div>';
|
| 575 |
+
html += `<span class="score-value">${scr}%</span>`;
|
| 576 |
+
html += '</div>';
|
| 577 |
+
}
|
| 578 |
+
html += '</div>';
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
html += '</div>';
|
| 582 |
+
container.innerHTML = html;
|
| 583 |
+
container.classList.remove('hidden');
|
| 584 |
+
},
|
| 585 |
+
|
| 586 |
+
// Text Summarization
|
| 587 |
+
async summarizeText() {
|
| 588 |
+
const text = document.getElementById('summary-input').value.trim();
|
| 589 |
+
const maxSentences = parseInt(document.getElementById('max-sentences').value);
|
| 590 |
+
const btn = document.getElementById('summarize-btn');
|
| 591 |
+
const resultDiv = document.getElementById('summary-result');
|
| 592 |
+
|
| 593 |
+
if (!text) {
|
| 594 |
+
this.showError(resultDiv, 'Please enter text to summarize');
|
| 595 |
+
return;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
btn.disabled = true;
|
| 599 |
+
btn.innerHTML = '<span class="loading"></span> Summarizing...';
|
| 600 |
+
resultDiv.classList.add('hidden');
|
| 601 |
+
|
| 602 |
+
try {
|
| 603 |
+
const response = await fetch('/api/ai/summarize', {
|
| 604 |
+
method: 'POST',
|
| 605 |
+
headers: { 'Content-Type': 'application/json' },
|
| 606 |
+
body: JSON.stringify({ text, max_sentences: maxSentences })
|
| 607 |
+
});
|
| 608 |
+
|
| 609 |
+
const data = await response.json();
|
| 610 |
+
|
| 611 |
+
if (!response.ok || !data.ok) {
|
| 612 |
+
throw new Error(data.error || 'Summarization failed');
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
this.displaySummaryResult(resultDiv, data);
|
| 616 |
+
} catch (error) {
|
| 617 |
+
this.showError(resultDiv, error.message);
|
| 618 |
+
} finally {
|
| 619 |
+
btn.disabled = false;
|
| 620 |
+
btn.innerHTML = 'Summarize';
|
| 621 |
+
}
|
| 622 |
+
},
|
| 623 |
+
|
| 624 |
+
displaySummaryResult(container, data) {
|
| 625 |
+
let html = '<div class="result-box">';
|
| 626 |
+
html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Summary</h3>';
|
| 627 |
+
|
| 628 |
+
if (data.summary) {
|
| 629 |
+
html += `<div class="summary-text">${this.escapeHtml(data.summary)}</div>`;
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
if (data.sentences && data.sentences.length > 0) {
|
| 633 |
+
html += '<h4 style="margin: 20px 0 10px 0; color: #cbd5e1; font-size: 1.1rem;">Key Sentences</h4>';
|
| 634 |
+
html += '<ul class="sentences-list">';
|
| 635 |
+
data.sentences.forEach(sentence => {
|
| 636 |
+
html += `<li>${this.escapeHtml(sentence)}</li>`;
|
| 637 |
+
});
|
| 638 |
+
html += '</ul>';
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
html += '</div>';
|
| 642 |
+
container.innerHTML = html;
|
| 643 |
+
container.classList.remove('hidden');
|
| 644 |
+
},
|
| 645 |
+
|
| 646 |
+
// Model Status & Diagnostics
|
| 647 |
+
async loadModelStatus() {
|
| 648 |
+
const statusDiv = document.getElementById('registry-status');
|
| 649 |
+
const tableDiv = document.getElementById('models-table');
|
| 650 |
+
const btn = document.getElementById('refresh-status-btn');
|
| 651 |
+
|
| 652 |
+
btn.disabled = true;
|
| 653 |
+
btn.innerHTML = '<span class="loading"></span> Loading...';
|
| 654 |
+
|
| 655 |
+
try {
|
| 656 |
+
const [statusRes, listRes] = await Promise.all([
|
| 657 |
+
fetch('/api/models/status'),
|
| 658 |
+
fetch('/api/models/list')
|
| 659 |
+
]);
|
| 660 |
+
|
| 661 |
+
const statusData = await statusRes.json();
|
| 662 |
+
const listData = await listRes.json();
|
| 663 |
+
|
| 664 |
+
this.displayRegistryStatus(statusDiv, statusData);
|
| 665 |
+
this.displayModelsTable(tableDiv, listData);
|
| 666 |
+
} catch (error) {
|
| 667 |
+
this.showError(statusDiv, 'Failed to load model status: ' + error.message);
|
| 668 |
+
} finally {
|
| 669 |
+
btn.disabled = false;
|
| 670 |
+
btn.innerHTML = 'Refresh';
|
| 671 |
+
}
|
| 672 |
+
},
|
| 673 |
+
|
| 674 |
+
displayRegistryStatus(container, data) {
|
| 675 |
+
let html = '<div class="status-grid">';
|
| 676 |
+
|
| 677 |
+
html += '<div class="status-item">';
|
| 678 |
+
html += '<div class="status-label">HF Mode</div>';
|
| 679 |
+
html += `<div class="status-value">${data.hf_mode || 'unknown'}</div>`;
|
| 680 |
+
html += '</div>';
|
| 681 |
+
|
| 682 |
+
html += '<div class="status-item">';
|
| 683 |
+
html += '<div class="status-label">Overall Status</div>';
|
| 684 |
+
html += `<div class="status-value">${data.status || 'unknown'}</div>`;
|
| 685 |
+
html += '</div>';
|
| 686 |
+
|
| 687 |
+
html += '<div class="status-item">';
|
| 688 |
+
html += '<div class="status-label">Models Loaded</div>';
|
| 689 |
+
html += `<div class="status-value">${data.models_loaded || 0}</div>`;
|
| 690 |
+
html += '</div>';
|
| 691 |
+
|
| 692 |
+
html += '<div class="status-item">';
|
| 693 |
+
html += '<div class="status-label">Models Failed</div>';
|
| 694 |
+
html += `<div class="status-value">${data.models_failed || 0}</div>`;
|
| 695 |
+
html += '</div>';
|
| 696 |
+
|
| 697 |
+
html += '</div>';
|
| 698 |
+
|
| 699 |
+
if (data.status === 'disabled' || data.hf_mode === 'off') {
|
| 700 |
+
html += '<div class="info-box">';
|
| 701 |
+
html += '<strong>Note:</strong> HF models are disabled. To enable them, set HF_MODE=public or HF_MODE=auth in the environment.';
|
| 702 |
+
html += '</div>';
|
| 703 |
+
} else if (data.models_loaded === 0 && data.status !== 'disabled') {
|
| 704 |
+
html += '<div class="warning-box">';
|
| 705 |
+
html += '<strong>Warning:</strong> No models could be loaded. Check model IDs or HF credentials.';
|
| 706 |
+
html += '</div>';
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
if (data.error) {
|
| 710 |
+
html += '<div class="error-box" style="margin-top: 15px;">';
|
| 711 |
+
html += `<strong>Error:</strong> ${this.escapeHtml(data.error)}`;
|
| 712 |
+
html += '</div>';
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
if (data.failed && data.failed.length > 0) {
|
| 716 |
+
html += '<div style="margin-top: 20px;">';
|
| 717 |
+
html += '<h4 style="color: #cbd5e1; margin-bottom: 10px;">Failed Models</h4>';
|
| 718 |
+
html += '<div style="background: rgba(30, 41, 59, 0.6); border-radius: 8px; padding: 15px;">';
|
| 719 |
+
data.failed.forEach(([key, error]) => {
|
| 720 |
+
html += `<div style="margin-bottom: 8px; padding: 8px; background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; border-radius: 4px;">`;
|
| 721 |
+
html += `<strong style="color: #fca5a5;">${key}:</strong> `;
|
| 722 |
+
html += `<span style="color: #cbd5e1;">${this.escapeHtml(error)}</span>`;
|
| 723 |
+
html += `</div>`;
|
| 724 |
+
});
|
| 725 |
+
html += '</div>';
|
| 726 |
+
html += '</div>';
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
container.innerHTML = html;
|
| 730 |
+
},
|
| 731 |
+
|
| 732 |
+
displayModelsTable(container, data) {
|
| 733 |
+
if (!data.models || data.models.length === 0) {
|
| 734 |
+
container.innerHTML = '<div class="info-box">No models configured</div>';
|
| 735 |
+
return;
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
let html = '<div class="table-container">';
|
| 739 |
+
html += '<table>';
|
| 740 |
+
html += '<thead><tr>';
|
| 741 |
+
html += '<th>Key</th>';
|
| 742 |
+
html += '<th>Task</th>';
|
| 743 |
+
html += '<th>Model ID</th>';
|
| 744 |
+
html += '<th>Loaded</th>';
|
| 745 |
+
html += '<th>Error</th>';
|
| 746 |
+
html += '</tr></thead>';
|
| 747 |
+
html += '<tbody>';
|
| 748 |
+
|
| 749 |
+
data.models.forEach(model => {
|
| 750 |
+
html += '<tr>';
|
| 751 |
+
html += `<td><strong>${model.key || 'N/A'}</strong></td>`;
|
| 752 |
+
html += `<td>${model.task || 'N/A'}</td>`;
|
| 753 |
+
html += `<td style="font-family: monospace; font-size: 0.85rem;">${model.model_id || 'N/A'}</td>`;
|
| 754 |
+
html += '<td>';
|
| 755 |
+
if (model.loaded) {
|
| 756 |
+
html += '<span class="badge badge-success">Yes</span>';
|
| 757 |
+
} else {
|
| 758 |
+
html += '<span class="badge badge-danger">No</span>';
|
| 759 |
+
}
|
| 760 |
+
html += '</td>';
|
| 761 |
+
html += `<td style="color: #f87171; font-size: 0.85rem;">${model.error ? this.escapeHtml(model.error) : '-'}</td>`;
|
| 762 |
+
html += '</tr>';
|
| 763 |
+
});
|
| 764 |
+
|
| 765 |
+
html += '</tbody>';
|
| 766 |
+
html += '</table>';
|
| 767 |
+
html += '</div>';
|
| 768 |
+
|
| 769 |
+
container.innerHTML = html;
|
| 770 |
+
},
|
| 771 |
+
|
| 772 |
+
// Utility functions
|
| 773 |
+
showError(container, message) {
|
| 774 |
+
container.innerHTML = `<div class="error-box"><strong>Error:</strong> ${this.escapeHtml(message)}</div>`;
|
| 775 |
+
container.classList.remove('hidden');
|
| 776 |
+
},
|
| 777 |
+
|
| 778 |
+
escapeHtml(text) {
|
| 779 |
+
const div = document.createElement('div');
|
| 780 |
+
div.textContent = text;
|
| 781 |
+
return div.innerHTML;
|
| 782 |
+
},
|
| 783 |
+
|
| 784 |
+
// Initialize
|
| 785 |
+
init() {
|
| 786 |
+
document.getElementById('analyze-sentiment-btn').addEventListener('click', () => this.analyzeSentiment());
|
| 787 |
+
document.getElementById('summarize-btn').addEventListener('click', () => this.summarizeText());
|
| 788 |
+
document.getElementById('refresh-status-btn').addEventListener('click', () => this.loadModelStatus());
|
| 789 |
+
|
| 790 |
+
this.loadModelStatus();
|
| 791 |
+
}
|
| 792 |
+
};
|
| 793 |
+
|
| 794 |
+
if (document.readyState === 'loading') {
|
| 795 |
+
document.addEventListener('DOMContentLoaded', () => AITools.init());
|
| 796 |
+
} else {
|
| 797 |
+
AITools.init();
|
| 798 |
+
}
|
| 799 |
+
})();
|
| 800 |
+
</script>
|
| 801 |
+
</body>
|
| 802 |
+
</html>
|
api_server_extended.py
CHANGED
|
@@ -379,6 +379,32 @@ async def index():
|
|
| 379 |
headers={"Content-Type": "text/html; charset=utf-8"}
|
| 380 |
)
|
| 381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
# ===== Health & Status Endpoints =====
|
| 384 |
@app.get("/health")
|
|
@@ -1256,6 +1282,8 @@ async def analyze_sentiment(request: Dict[str, Any]):
|
|
| 1256 |
raise HTTPException(status_code=400, detail="Text is required")
|
| 1257 |
|
| 1258 |
mode = request.get("mode", "auto").lower()
|
|
|
|
|
|
|
| 1259 |
symbol = request.get("symbol")
|
| 1260 |
|
| 1261 |
try:
|
|
@@ -1271,19 +1299,39 @@ async def analyze_sentiment(request: Dict[str, Any]):
|
|
| 1271 |
# Check if models are available
|
| 1272 |
if not result.get("available", True):
|
| 1273 |
return {
|
| 1274 |
-
"
|
| 1275 |
-
"available": False,
|
| 1276 |
-
"sentiment": "neutral",
|
| 1277 |
-
"confidence": 0.0,
|
| 1278 |
-
"mode": mode,
|
| 1279 |
"error": result.get("error", "Models not available"),
|
| 1280 |
-
"
|
|
|
|
| 1281 |
}
|
| 1282 |
|
| 1283 |
sentiment_label = result.get("label", "neutral")
|
| 1284 |
confidence = result.get("confidence", result.get("score", 0.5))
|
| 1285 |
model_used = result.get("model_count", result.get("model", "unknown"))
|
| 1286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1287 |
# Save to database
|
| 1288 |
try:
|
| 1289 |
conn = sqlite3.connect(str(DB_PATH))
|
|
@@ -1303,30 +1351,17 @@ async def analyze_sentiment(request: Dict[str, Any]):
|
|
| 1303 |
))
|
| 1304 |
conn.commit()
|
| 1305 |
conn.close()
|
| 1306 |
-
saved_to_db = True
|
| 1307 |
except Exception as db_error:
|
| 1308 |
logger.warning(f"Failed to save to database: {db_error}")
|
| 1309 |
-
saved_to_db = False
|
| 1310 |
|
| 1311 |
-
return
|
| 1312 |
-
"success": True,
|
| 1313 |
-
"available": True,
|
| 1314 |
-
"sentiment": sentiment_label,
|
| 1315 |
-
"confidence": confidence,
|
| 1316 |
-
"mode": mode,
|
| 1317 |
-
"result": result,
|
| 1318 |
-
"saved_to_db": saved_to_db
|
| 1319 |
-
}
|
| 1320 |
|
| 1321 |
except ModelNotAvailable as e:
|
| 1322 |
return {
|
| 1323 |
-
"
|
| 1324 |
-
"available": False,
|
| 1325 |
-
"sentiment": "neutral",
|
| 1326 |
-
"confidence": 0.0,
|
| 1327 |
-
"mode": mode,
|
| 1328 |
"error": str(e),
|
| 1329 |
-
"
|
|
|
|
| 1330 |
}
|
| 1331 |
|
| 1332 |
except HTTPException:
|
|
@@ -1335,6 +1370,120 @@ async def analyze_sentiment(request: Dict[str, Any]):
|
|
| 1335 |
raise HTTPException(status_code=500, detail=f"Sentiment analysis failed: {str(e)}")
|
| 1336 |
|
| 1337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1338 |
@app.post("/api/news/analyze")
|
| 1339 |
async def analyze_news(request: Dict[str, Any]):
|
| 1340 |
"""Analyze news article sentiment using HF models"""
|
|
@@ -1561,6 +1710,11 @@ async def get_models_status():
|
|
| 1561 |
status = "unknown"
|
| 1562 |
status_message = "Unknown status"
|
| 1563 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1564 |
return {
|
| 1565 |
"success": True,
|
| 1566 |
"status": status,
|
|
@@ -1572,7 +1726,8 @@ async def get_models_status():
|
|
| 1572 |
"initialized": _registry._initialized,
|
| 1573 |
"models": model_info,
|
| 1574 |
"registry": registry_info,
|
| 1575 |
-
"
|
|
|
|
| 1576 |
"loaded_models": list(_registry._pipelines.keys()),
|
| 1577 |
"database": {
|
| 1578 |
"path": str(DB_PATH),
|
|
@@ -1585,7 +1740,10 @@ async def get_models_status():
|
|
| 1585 |
"success": False,
|
| 1586 |
"status": "error",
|
| 1587 |
"status_message": f"Error retrieving model status: {str(e)}",
|
| 1588 |
-
"error": str(e)
|
|
|
|
|
|
|
|
|
|
| 1589 |
}
|
| 1590 |
|
| 1591 |
|
|
@@ -1612,18 +1770,26 @@ async def initialize_ai_models():
|
|
| 1612 |
async def list_available_models():
|
| 1613 |
"""List all available Hugging Face models as data sources"""
|
| 1614 |
try:
|
| 1615 |
-
from ai_models import get_model_info, MODEL_SPECS, CRYPTO_SENTIMENT_MODELS, SOCIAL_SENTIMENT_MODELS, FINANCIAL_SENTIMENT_MODELS, NEWS_SENTIMENT_MODELS
|
| 1616 |
|
| 1617 |
model_info = get_model_info()
|
| 1618 |
|
| 1619 |
models_list = []
|
| 1620 |
for key, spec in MODEL_SPECS.items():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1621 |
models_list.append({
|
| 1622 |
-
"
|
|
|
|
| 1623 |
"model_id": spec.model_id,
|
| 1624 |
"task": spec.task,
|
| 1625 |
"category": spec.category,
|
| 1626 |
"requires_auth": spec.requires_auth,
|
|
|
|
|
|
|
| 1627 |
"endpoint": f"/api/models/{key}/predict"
|
| 1628 |
})
|
| 1629 |
|
|
|
|
| 379 |
headers={"Content-Type": "text/html; charset=utf-8"}
|
| 380 |
)
|
| 381 |
|
| 382 |
+
@app.get("/ai-tools", response_class=HTMLResponse)
|
| 383 |
+
async def ai_tools_page(request: Request):
|
| 384 |
+
"""
|
| 385 |
+
Serve the standalone AI Tools page.
|
| 386 |
+
|
| 387 |
+
This page provides:
|
| 388 |
+
- Sentiment Playground: POST /api/sentiment/analyze
|
| 389 |
+
- Text Summarizer: POST /api/ai/summarize
|
| 390 |
+
- Model Status & Diagnostics: GET /api/models/status, /api/models/list
|
| 391 |
+
"""
|
| 392 |
+
ai_tools_path = WORKSPACE_ROOT / "templates" / "ai_tools.html"
|
| 393 |
+
if ai_tools_path.exists():
|
| 394 |
+
content = ai_tools_path.read_text(encoding="utf-8", errors="ignore")
|
| 395 |
+
return HTMLResponse(
|
| 396 |
+
content=content,
|
| 397 |
+
media_type="text/html",
|
| 398 |
+
headers={
|
| 399 |
+
"Content-Type": "text/html; charset=utf-8",
|
| 400 |
+
"X-Content-Type-Options": "nosniff"
|
| 401 |
+
}
|
| 402 |
+
)
|
| 403 |
+
return HTMLResponse(
|
| 404 |
+
"<h1>AI Tools page not found</h1>",
|
| 405 |
+
headers={"Content-Type": "text/html; charset=utf-8"}
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
|
| 409 |
# ===== Health & Status Endpoints =====
|
| 410 |
@app.get("/health")
|
|
|
|
| 1282 |
raise HTTPException(status_code=400, detail="Text is required")
|
| 1283 |
|
| 1284 |
mode = request.get("mode", "auto").lower()
|
| 1285 |
+
source = request.get("source", "user")
|
| 1286 |
+
model_key = request.get("model_key")
|
| 1287 |
symbol = request.get("symbol")
|
| 1288 |
|
| 1289 |
try:
|
|
|
|
| 1299 |
# Check if models are available
|
| 1300 |
if not result.get("available", True):
|
| 1301 |
return {
|
| 1302 |
+
"ok": False,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1303 |
"error": result.get("error", "Models not available"),
|
| 1304 |
+
"label": "neutral",
|
| 1305 |
+
"score": 0.0
|
| 1306 |
}
|
| 1307 |
|
| 1308 |
sentiment_label = result.get("label", "neutral")
|
| 1309 |
confidence = result.get("confidence", result.get("score", 0.5))
|
| 1310 |
model_used = result.get("model_count", result.get("model", "unknown"))
|
| 1311 |
|
| 1312 |
+
# Prepare response compatible with ai_tools.html format
|
| 1313 |
+
response_data = {
|
| 1314 |
+
"ok": True,
|
| 1315 |
+
"label": sentiment_label.lower(),
|
| 1316 |
+
"score": float(confidence),
|
| 1317 |
+
"model": f"{model_used} models" if isinstance(model_used, int) else str(model_used)
|
| 1318 |
+
}
|
| 1319 |
+
|
| 1320 |
+
# Add details if available for score bars
|
| 1321 |
+
if result.get("scores"):
|
| 1322 |
+
scores_dict = result.get("scores", {})
|
| 1323 |
+
if isinstance(scores_dict, dict):
|
| 1324 |
+
labels_list = []
|
| 1325 |
+
scores_list = []
|
| 1326 |
+
for lbl, scr in scores_dict.items():
|
| 1327 |
+
labels_list.append(lbl)
|
| 1328 |
+
scores_list.append(float(scr))
|
| 1329 |
+
if labels_list:
|
| 1330 |
+
response_data["details"] = {
|
| 1331 |
+
"labels": labels_list,
|
| 1332 |
+
"scores": scores_list
|
| 1333 |
+
}
|
| 1334 |
+
|
| 1335 |
# Save to database
|
| 1336 |
try:
|
| 1337 |
conn = sqlite3.connect(str(DB_PATH))
|
|
|
|
| 1351 |
))
|
| 1352 |
conn.commit()
|
| 1353 |
conn.close()
|
|
|
|
| 1354 |
except Exception as db_error:
|
| 1355 |
logger.warning(f"Failed to save to database: {db_error}")
|
|
|
|
| 1356 |
|
| 1357 |
+
return response_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1358 |
|
| 1359 |
except ModelNotAvailable as e:
|
| 1360 |
return {
|
| 1361 |
+
"ok": False,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1362 |
"error": str(e),
|
| 1363 |
+
"label": "neutral",
|
| 1364 |
+
"score": 0.0
|
| 1365 |
}
|
| 1366 |
|
| 1367 |
except HTTPException:
|
|
|
|
| 1370 |
raise HTTPException(status_code=500, detail=f"Sentiment analysis failed: {str(e)}")
|
| 1371 |
|
| 1372 |
|
| 1373 |
+
@app.post("/api/ai/summarize")
|
| 1374 |
+
async def summarize_text(request: Dict[str, Any]):
|
| 1375 |
+
"""
|
| 1376 |
+
Summarize text using Hugging Face models or simple text processing.
|
| 1377 |
+
|
| 1378 |
+
Expects: { "text": "string", "max_sentences": 3 }
|
| 1379 |
+
Returns: { "ok": true, "summary": "...", "sentences": ["...", "..."] }
|
| 1380 |
+
"""
|
| 1381 |
+
try:
|
| 1382 |
+
text = request.get("text", "").strip()
|
| 1383 |
+
max_sentences = request.get("max_sentences", 3)
|
| 1384 |
+
|
| 1385 |
+
if not text:
|
| 1386 |
+
return {
|
| 1387 |
+
"ok": False,
|
| 1388 |
+
"error": "Text is required"
|
| 1389 |
+
}
|
| 1390 |
+
|
| 1391 |
+
# Try to use Hugging Face summarization model if available
|
| 1392 |
+
try:
|
| 1393 |
+
from ai_models import MODEL_SPECS, _registry, ModelNotAvailable
|
| 1394 |
+
|
| 1395 |
+
# Check if summarization model is available
|
| 1396 |
+
summarization_key = None
|
| 1397 |
+
for key, spec in MODEL_SPECS.items():
|
| 1398 |
+
if spec.task == "summarization":
|
| 1399 |
+
summarization_key = key
|
| 1400 |
+
break
|
| 1401 |
+
|
| 1402 |
+
if summarization_key:
|
| 1403 |
+
try:
|
| 1404 |
+
pipeline = _registry.get_pipeline(summarization_key)
|
| 1405 |
+
# Use HF model for summarization
|
| 1406 |
+
# Try with parameters first, then fallback to simple call
|
| 1407 |
+
try:
|
| 1408 |
+
summary_result = pipeline(text, max_length=max_sentences * 50, min_length=max_sentences * 20, do_sample=False)
|
| 1409 |
+
except TypeError:
|
| 1410 |
+
# Some pipelines don't accept these parameters
|
| 1411 |
+
summary_result = pipeline(text)
|
| 1412 |
+
|
| 1413 |
+
if isinstance(summary_result, list) and summary_result:
|
| 1414 |
+
summary_text = summary_result[0].get("summary_text", summary_result[0].get("generated_text", str(summary_result[0])))
|
| 1415 |
+
elif isinstance(summary_result, dict):
|
| 1416 |
+
summary_text = summary_result.get("summary_text", summary_result.get("generated_text", str(summary_result)))
|
| 1417 |
+
else:
|
| 1418 |
+
summary_text = str(summary_result)
|
| 1419 |
+
|
| 1420 |
+
# Split into sentences
|
| 1421 |
+
sentences = [s.strip() + ("." if not s.strip().endswith((".", "!", "?")) else "") for s in summary_text.split(". ") if s.strip()]
|
| 1422 |
+
sentences = sentences[:max_sentences]
|
| 1423 |
+
|
| 1424 |
+
return {
|
| 1425 |
+
"ok": True,
|
| 1426 |
+
"summary": summary_text,
|
| 1427 |
+
"sentences": sentences
|
| 1428 |
+
}
|
| 1429 |
+
except ModelNotAvailable:
|
| 1430 |
+
# Fall through to simple summarizer
|
| 1431 |
+
pass
|
| 1432 |
+
except Exception as e:
|
| 1433 |
+
logger.warning(f"HF summarization failed: {e}, using fallback")
|
| 1434 |
+
# Fall through to simple summarizer
|
| 1435 |
+
pass
|
| 1436 |
+
except Exception as e:
|
| 1437 |
+
logger.warning(f"HF summarization model not available: {e}")
|
| 1438 |
+
# Fall through to simple summarizer
|
| 1439 |
+
|
| 1440 |
+
# Simple placeholder summarizer: split by sentences and take first N
|
| 1441 |
+
sentences = []
|
| 1442 |
+
current_sentence = ""
|
| 1443 |
+
|
| 1444 |
+
for char in text:
|
| 1445 |
+
current_sentence += char
|
| 1446 |
+
if char in ".!?":
|
| 1447 |
+
sentence = current_sentence.strip()
|
| 1448 |
+
if sentence:
|
| 1449 |
+
sentences.append(sentence)
|
| 1450 |
+
current_sentence = ""
|
| 1451 |
+
if len(sentences) >= max_sentences:
|
| 1452 |
+
break
|
| 1453 |
+
|
| 1454 |
+
# If we didn't get enough sentences, add the rest
|
| 1455 |
+
if len(sentences) < max_sentences and current_sentence.strip():
|
| 1456 |
+
sentences.append(current_sentence.strip())
|
| 1457 |
+
|
| 1458 |
+
# If still no sentences, just truncate
|
| 1459 |
+
if not sentences:
|
| 1460 |
+
words = text.split()
|
| 1461 |
+
chunk_size = len(words) // max_sentences
|
| 1462 |
+
sentences = []
|
| 1463 |
+
for i in range(max_sentences):
|
| 1464 |
+
start_idx = i * chunk_size
|
| 1465 |
+
end_idx = start_idx + chunk_size if i < max_sentences - 1 else len(words)
|
| 1466 |
+
if start_idx < len(words):
|
| 1467 |
+
sentence = " ".join(words[start_idx:end_idx])
|
| 1468 |
+
if sentence:
|
| 1469 |
+
sentences.append(sentence)
|
| 1470 |
+
|
| 1471 |
+
summary = " ".join(sentences)
|
| 1472 |
+
|
| 1473 |
+
return {
|
| 1474 |
+
"ok": True,
|
| 1475 |
+
"summary": summary,
|
| 1476 |
+
"sentences": sentences[:max_sentences]
|
| 1477 |
+
}
|
| 1478 |
+
|
| 1479 |
+
except Exception as e:
|
| 1480 |
+
logger.error(f"Summarization failed: {e}")
|
| 1481 |
+
return {
|
| 1482 |
+
"ok": False,
|
| 1483 |
+
"error": f"Summarization failed: {str(e)}"
|
| 1484 |
+
}
|
| 1485 |
+
|
| 1486 |
+
|
| 1487 |
@app.post("/api/news/analyze")
|
| 1488 |
async def analyze_news(request: Dict[str, Any]):
|
| 1489 |
"""Analyze news article sentiment using HF models"""
|
|
|
|
| 1710 |
status = "unknown"
|
| 1711 |
status_message = "Unknown status"
|
| 1712 |
|
| 1713 |
+
# Format failed models as list of [key, error] tuples for ai_tools.html
|
| 1714 |
+
failed_list = []
|
| 1715 |
+
for key, error in list(_registry._failed_models.items())[:10]:
|
| 1716 |
+
failed_list.append([key, str(error)])
|
| 1717 |
+
|
| 1718 |
return {
|
| 1719 |
"success": True,
|
| 1720 |
"status": status,
|
|
|
|
| 1726 |
"initialized": _registry._initialized,
|
| 1727 |
"models": model_info,
|
| 1728 |
"registry": registry_info,
|
| 1729 |
+
"failed": failed_list, # Format: [[key, error], ...] for ai_tools.html
|
| 1730 |
+
"failed_models": list(_registry._failed_models.keys())[:10], # Keep for backward compatibility
|
| 1731 |
"loaded_models": list(_registry._pipelines.keys()),
|
| 1732 |
"database": {
|
| 1733 |
"path": str(DB_PATH),
|
|
|
|
| 1740 |
"success": False,
|
| 1741 |
"status": "error",
|
| 1742 |
"status_message": f"Error retrieving model status: {str(e)}",
|
| 1743 |
+
"error": str(e),
|
| 1744 |
+
"hf_mode": "unknown",
|
| 1745 |
+
"models_loaded": 0,
|
| 1746 |
+
"models_failed": 0
|
| 1747 |
}
|
| 1748 |
|
| 1749 |
|
|
|
|
| 1770 |
async def list_available_models():
|
| 1771 |
"""List all available Hugging Face models as data sources"""
|
| 1772 |
try:
|
| 1773 |
+
from ai_models import get_model_info, MODEL_SPECS, _registry, CRYPTO_SENTIMENT_MODELS, SOCIAL_SENTIMENT_MODELS, FINANCIAL_SENTIMENT_MODELS, NEWS_SENTIMENT_MODELS
|
| 1774 |
|
| 1775 |
model_info = get_model_info()
|
| 1776 |
|
| 1777 |
models_list = []
|
| 1778 |
for key, spec in MODEL_SPECS.items():
|
| 1779 |
+
is_loaded = key in _registry._pipelines
|
| 1780 |
+
error_msg = None
|
| 1781 |
+
if key in _registry._failed_models:
|
| 1782 |
+
error_msg = str(_registry._failed_models[key])
|
| 1783 |
+
|
| 1784 |
models_list.append({
|
| 1785 |
+
"key": key, # ai_tools.html expects "key"
|
| 1786 |
+
"id": key, # Keep for backward compatibility
|
| 1787 |
"model_id": spec.model_id,
|
| 1788 |
"task": spec.task,
|
| 1789 |
"category": spec.category,
|
| 1790 |
"requires_auth": spec.requires_auth,
|
| 1791 |
+
"loaded": is_loaded, # ai_tools.html expects "loaded" boolean
|
| 1792 |
+
"error": error_msg, # ai_tools.html expects "error" string or null
|
| 1793 |
"endpoint": f"/api/models/{key}/predict"
|
| 1794 |
})
|
| 1795 |
|
templates/ai_tools.html
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>AI Tools - Crypto Intelligence Hub</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
| 16 |
+
background: linear-gradient(135deg, #050816 0%, #0a1128 100%);
|
| 17 |
+
color: #e2e8f0;
|
| 18 |
+
min-height: 100vh;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.header {
|
| 23 |
+
text-align: center;
|
| 24 |
+
margin-bottom: 40px;
|
| 25 |
+
padding: 30px 20px;
|
| 26 |
+
background: rgba(15, 23, 42, 0.6);
|
| 27 |
+
border-radius: 16px;
|
| 28 |
+
backdrop-filter: blur(10px);
|
| 29 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.header h1 {
|
| 33 |
+
font-size: 2.5rem;
|
| 34 |
+
font-weight: 700;
|
| 35 |
+
margin-bottom: 10px;
|
| 36 |
+
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
|
| 37 |
+
-webkit-background-clip: text;
|
| 38 |
+
-webkit-text-fill-color: transparent;
|
| 39 |
+
background-clip: text;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.header p {
|
| 43 |
+
color: #94a3b8;
|
| 44 |
+
font-size: 1.1rem;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.container {
|
| 48 |
+
max-width: 1400px;
|
| 49 |
+
margin: 0 auto;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.card {
|
| 53 |
+
background: rgba(15, 23, 42, 0.8);
|
| 54 |
+
border-radius: 16px;
|
| 55 |
+
padding: 30px;
|
| 56 |
+
margin-bottom: 30px;
|
| 57 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 58 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
| 59 |
+
backdrop-filter: blur(10px);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.card-title {
|
| 63 |
+
font-size: 1.8rem;
|
| 64 |
+
font-weight: 600;
|
| 65 |
+
margin-bottom: 25px;
|
| 66 |
+
color: #f1f5f9;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.form-group {
|
| 70 |
+
margin-bottom: 20px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.form-label {
|
| 74 |
+
display: block;
|
| 75 |
+
margin-bottom: 8px;
|
| 76 |
+
color: #cbd5e1;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
font-size: 0.95rem;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.form-input,
|
| 82 |
+
.form-textarea,
|
| 83 |
+
.form-select {
|
| 84 |
+
width: 100%;
|
| 85 |
+
padding: 12px 16px;
|
| 86 |
+
background: rgba(30, 41, 59, 0.8);
|
| 87 |
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
| 88 |
+
border-radius: 8px;
|
| 89 |
+
color: #e2e8f0;
|
| 90 |
+
font-size: 1rem;
|
| 91 |
+
transition: all 0.3s ease;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.form-input:focus,
|
| 95 |
+
.form-textarea:focus,
|
| 96 |
+
.form-select:focus {
|
| 97 |
+
outline: none;
|
| 98 |
+
border-color: #60a5fa;
|
| 99 |
+
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.form-textarea {
|
| 103 |
+
min-height: 120px;
|
| 104 |
+
resize: vertical;
|
| 105 |
+
font-family: inherit;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.btn {
|
| 109 |
+
padding: 12px 24px;
|
| 110 |
+
font-size: 1rem;
|
| 111 |
+
font-weight: 600;
|
| 112 |
+
border: none;
|
| 113 |
+
border-radius: 8px;
|
| 114 |
+
cursor: pointer;
|
| 115 |
+
transition: all 0.3s ease;
|
| 116 |
+
display: inline-flex;
|
| 117 |
+
align-items: center;
|
| 118 |
+
gap: 8px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.btn-primary {
|
| 122 |
+
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
| 123 |
+
color: white;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.btn-primary:hover:not(:disabled) {
|
| 127 |
+
transform: translateY(-2px);
|
| 128 |
+
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.btn-primary:disabled {
|
| 132 |
+
opacity: 0.6;
|
| 133 |
+
cursor: not-allowed;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.btn-secondary {
|
| 137 |
+
background: rgba(71, 85, 105, 0.8);
|
| 138 |
+
color: #e2e8f0;
|
| 139 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.btn-secondary:hover:not(:disabled) {
|
| 143 |
+
background: rgba(100, 116, 139, 0.9);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.result-box {
|
| 147 |
+
margin-top: 25px;
|
| 148 |
+
padding: 20px;
|
| 149 |
+
background: rgba(30, 41, 59, 0.6);
|
| 150 |
+
border-radius: 12px;
|
| 151 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.error-box {
|
| 155 |
+
margin-top: 25px;
|
| 156 |
+
padding: 16px;
|
| 157 |
+
background: rgba(239, 68, 68, 0.1);
|
| 158 |
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 159 |
+
border-radius: 8px;
|
| 160 |
+
color: #fca5a5;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.success-box {
|
| 164 |
+
margin-top: 25px;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
background: rgba(34, 197, 94, 0.1);
|
| 167 |
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
| 168 |
+
border-radius: 12px;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.badge {
|
| 172 |
+
display: inline-block;
|
| 173 |
+
padding: 6px 14px;
|
| 174 |
+
border-radius: 20px;
|
| 175 |
+
font-size: 0.9rem;
|
| 176 |
+
font-weight: 600;
|
| 177 |
+
margin-right: 10px;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.badge-positive {
|
| 181 |
+
background: rgba(34, 197, 94, 0.2);
|
| 182 |
+
color: #4ade80;
|
| 183 |
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.badge-negative {
|
| 187 |
+
background: rgba(239, 68, 68, 0.2);
|
| 188 |
+
color: #f87171;
|
| 189 |
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.badge-neutral {
|
| 193 |
+
background: rgba(148, 163, 184, 0.2);
|
| 194 |
+
color: #94a3b8;
|
| 195 |
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.badge-success {
|
| 199 |
+
background: rgba(34, 197, 94, 0.2);
|
| 200 |
+
color: #4ade80;
|
| 201 |
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.badge-danger {
|
| 205 |
+
background: rgba(239, 68, 68, 0.2);
|
| 206 |
+
color: #f87171;
|
| 207 |
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.score-bar {
|
| 211 |
+
margin-top: 15px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.score-item {
|
| 215 |
+
display: flex;
|
| 216 |
+
align-items: center;
|
| 217 |
+
margin-bottom: 8px;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.score-label {
|
| 221 |
+
min-width: 80px;
|
| 222 |
+
font-size: 0.9rem;
|
| 223 |
+
color: #cbd5e1;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.score-progress {
|
| 227 |
+
flex: 1;
|
| 228 |
+
height: 8px;
|
| 229 |
+
background: rgba(30, 41, 59, 0.8);
|
| 230 |
+
border-radius: 4px;
|
| 231 |
+
overflow: hidden;
|
| 232 |
+
margin: 0 12px;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.score-fill {
|
| 236 |
+
height: 100%;
|
| 237 |
+
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
|
| 238 |
+
border-radius: 4px;
|
| 239 |
+
transition: width 0.5s ease;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.score-value {
|
| 243 |
+
min-width: 50px;
|
| 244 |
+
text-align: right;
|
| 245 |
+
font-weight: 600;
|
| 246 |
+
color: #e2e8f0;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.table-container {
|
| 250 |
+
overflow-x: auto;
|
| 251 |
+
margin-top: 20px;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
table {
|
| 255 |
+
width: 100%;
|
| 256 |
+
border-collapse: collapse;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
th {
|
| 260 |
+
background: rgba(30, 41, 59, 0.8);
|
| 261 |
+
padding: 12px;
|
| 262 |
+
text-align: left;
|
| 263 |
+
font-weight: 600;
|
| 264 |
+
color: #f1f5f9;
|
| 265 |
+
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
td {
|
| 269 |
+
padding: 12px;
|
| 270 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| 271 |
+
color: #cbd5e1;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
tr:hover {
|
| 275 |
+
background: rgba(30, 41, 59, 0.4);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.info-box {
|
| 279 |
+
padding: 16px;
|
| 280 |
+
background: rgba(59, 130, 246, 0.1);
|
| 281 |
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
| 282 |
+
border-radius: 8px;
|
| 283 |
+
margin: 15px 0;
|
| 284 |
+
color: #93c5fd;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.warning-box {
|
| 288 |
+
padding: 16px;
|
| 289 |
+
background: rgba(251, 191, 36, 0.1);
|
| 290 |
+
border: 1px solid rgba(251, 191, 36, 0.3);
|
| 291 |
+
border-radius: 8px;
|
| 292 |
+
margin: 15px 0;
|
| 293 |
+
color: #fcd34d;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.status-grid {
|
| 297 |
+
display: grid;
|
| 298 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 299 |
+
gap: 15px;
|
| 300 |
+
margin: 20px 0;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.status-item {
|
| 304 |
+
padding: 15px;
|
| 305 |
+
background: rgba(30, 41, 59, 0.6);
|
| 306 |
+
border-radius: 8px;
|
| 307 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.status-label {
|
| 311 |
+
font-size: 0.85rem;
|
| 312 |
+
color: #94a3b8;
|
| 313 |
+
margin-bottom: 5px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.status-value {
|
| 317 |
+
font-size: 1.3rem;
|
| 318 |
+
font-weight: 700;
|
| 319 |
+
color: #f1f5f9;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.summary-text {
|
| 323 |
+
padding: 20px;
|
| 324 |
+
background: rgba(30, 41, 59, 0.8);
|
| 325 |
+
border-radius: 8px;
|
| 326 |
+
border-left: 4px solid #60a5fa;
|
| 327 |
+
font-size: 1.05rem;
|
| 328 |
+
line-height: 1.7;
|
| 329 |
+
color: #e2e8f0;
|
| 330 |
+
margin-bottom: 20px;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.sentences-list {
|
| 334 |
+
list-style: none;
|
| 335 |
+
padding: 0;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.sentences-list li {
|
| 339 |
+
padding: 12px 15px;
|
| 340 |
+
background: rgba(30, 41, 59, 0.6);
|
| 341 |
+
border-radius: 8px;
|
| 342 |
+
margin-bottom: 10px;
|
| 343 |
+
border-left: 3px solid #8b5cf6;
|
| 344 |
+
color: #cbd5e1;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.sentences-list li:before {
|
| 348 |
+
content: "→";
|
| 349 |
+
margin-right: 10px;
|
| 350 |
+
color: #8b5cf6;
|
| 351 |
+
font-weight: bold;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.loading {
|
| 355 |
+
display: inline-block;
|
| 356 |
+
width: 16px;
|
| 357 |
+
height: 16px;
|
| 358 |
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
| 359 |
+
border-top-color: #fff;
|
| 360 |
+
border-radius: 50%;
|
| 361 |
+
animation: spin 0.6s linear infinite;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
@keyframes spin {
|
| 365 |
+
to { transform: rotate(360deg); }
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.two-column {
|
| 369 |
+
display: grid;
|
| 370 |
+
grid-template-columns: 1fr 1fr;
|
| 371 |
+
gap: 20px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
@media (max-width: 768px) {
|
| 375 |
+
.header h1 {
|
| 376 |
+
font-size: 1.8rem;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.header p {
|
| 380 |
+
font-size: 0.95rem;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.card {
|
| 384 |
+
padding: 20px;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.card-title {
|
| 388 |
+
font-size: 1.4rem;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.two-column {
|
| 392 |
+
grid-template-columns: 1fr;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.status-grid {
|
| 396 |
+
grid-template-columns: 1fr;
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.hidden {
|
| 401 |
+
display: none;
|
| 402 |
+
}
|
| 403 |
+
</style>
|
| 404 |
+
</head>
|
| 405 |
+
<body>
|
| 406 |
+
<div class="container">
|
| 407 |
+
<div class="header">
|
| 408 |
+
<h1>AI Tools – Crypto Intelligence Hub</h1>
|
| 409 |
+
<p>Sentiment, Summaries, and Model Diagnostics</p>
|
| 410 |
+
</div>
|
| 411 |
+
|
| 412 |
+
<!-- Sentiment Playground -->
|
| 413 |
+
<div class="card">
|
| 414 |
+
<h2 class="card-title">Sentiment Playground</h2>
|
| 415 |
+
|
| 416 |
+
<div class="form-group">
|
| 417 |
+
<label class="form-label" for="sentiment-input">Enter Text</label>
|
| 418 |
+
<textarea
|
| 419 |
+
id="sentiment-input"
|
| 420 |
+
class="form-textarea"
|
| 421 |
+
placeholder="Enter text to analyze sentiment (tweets, news, or any text)..."
|
| 422 |
+
></textarea>
|
| 423 |
+
</div>
|
| 424 |
+
|
| 425 |
+
<div class="two-column">
|
| 426 |
+
<div class="form-group">
|
| 427 |
+
<label class="form-label" for="sentiment-source">Source Type</label>
|
| 428 |
+
<select id="sentiment-source" class="form-select">
|
| 429 |
+
<option value="user">User Input</option>
|
| 430 |
+
<option value="tweet">Tweet</option>
|
| 431 |
+
<option value="news">News</option>
|
| 432 |
+
</select>
|
| 433 |
+
</div>
|
| 434 |
+
|
| 435 |
+
<div class="form-group">
|
| 436 |
+
<label class="form-label" for="sentiment-model-key">Model Key (Optional)</label>
|
| 437 |
+
<input
|
| 438 |
+
type="text"
|
| 439 |
+
id="sentiment-model-key"
|
| 440 |
+
class="form-input"
|
| 441 |
+
placeholder="Leave empty for default model"
|
| 442 |
+
/>
|
| 443 |
+
</div>
|
| 444 |
+
</div>
|
| 445 |
+
|
| 446 |
+
<button id="analyze-sentiment-btn" class="btn btn-primary">
|
| 447 |
+
Analyze Sentiment
|
| 448 |
+
</button>
|
| 449 |
+
|
| 450 |
+
<div id="sentiment-result" class="hidden"></div>
|
| 451 |
+
</div>
|
| 452 |
+
|
| 453 |
+
<!-- Text Summarizer -->
|
| 454 |
+
<div class="card">
|
| 455 |
+
<h2 class="card-title">Text Summarizer</h2>
|
| 456 |
+
|
| 457 |
+
<div class="form-group">
|
| 458 |
+
<label class="form-label" for="summary-input">Enter Long Text</label>
|
| 459 |
+
<textarea
|
| 460 |
+
id="summary-input"
|
| 461 |
+
class="form-textarea"
|
| 462 |
+
placeholder="Paste article or long text to summarize..."
|
| 463 |
+
style="min-height: 180px;"
|
| 464 |
+
></textarea>
|
| 465 |
+
</div>
|
| 466 |
+
|
| 467 |
+
<div class="form-group">
|
| 468 |
+
<label class="form-label" for="max-sentences">Maximum Sentences</label>
|
| 469 |
+
<select id="max-sentences" class="form-select">
|
| 470 |
+
<option value="2">2 sentences</option>
|
| 471 |
+
<option value="3" selected>3 sentences</option>
|
| 472 |
+
<option value="4">4 sentences</option>
|
| 473 |
+
<option value="5">5 sentences</option>
|
| 474 |
+
</select>
|
| 475 |
+
</div>
|
| 476 |
+
|
| 477 |
+
<button id="summarize-btn" class="btn btn-primary">
|
| 478 |
+
Summarize
|
| 479 |
+
</button>
|
| 480 |
+
|
| 481 |
+
<div id="summary-result" class="hidden"></div>
|
| 482 |
+
</div>
|
| 483 |
+
|
| 484 |
+
<!-- Model Status & Diagnostics -->
|
| 485 |
+
<div class="card">
|
| 486 |
+
<h2 class="card-title">Model Status & Diagnostics</h2>
|
| 487 |
+
|
| 488 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
| 489 |
+
<h3 style="color: #cbd5e1; font-size: 1.2rem;">Registry Status</h3>
|
| 490 |
+
<button id="refresh-status-btn" class="btn btn-secondary">
|
| 491 |
+
Refresh
|
| 492 |
+
</button>
|
| 493 |
+
</div>
|
| 494 |
+
|
| 495 |
+
<div id="registry-status"></div>
|
| 496 |
+
|
| 497 |
+
<h3 style="color: #cbd5e1; font-size: 1.2rem; margin: 30px 0 15px 0;">Models Table</h3>
|
| 498 |
+
<div id="models-table"></div>
|
| 499 |
+
</div>
|
| 500 |
+
</div>
|
| 501 |
+
|
| 502 |
+
<script>
|
| 503 |
+
(function() {
|
| 504 |
+
'use strict';
|
| 505 |
+
|
| 506 |
+
const AITools = {
|
| 507 |
+
// Sentiment Analysis
|
| 508 |
+
async analyzeSentiment() {
|
| 509 |
+
const text = document.getElementById('sentiment-input').value.trim();
|
| 510 |
+
const source = document.getElementById('sentiment-source').value;
|
| 511 |
+
const modelKey = document.getElementById('sentiment-model-key').value.trim();
|
| 512 |
+
const btn = document.getElementById('analyze-sentiment-btn');
|
| 513 |
+
const resultDiv = document.getElementById('sentiment-result');
|
| 514 |
+
|
| 515 |
+
if (!text) {
|
| 516 |
+
this.showError(resultDiv, 'Please enter text to analyze');
|
| 517 |
+
return;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
btn.disabled = true;
|
| 521 |
+
btn.innerHTML = '<span class="loading"></span> Analyzing...';
|
| 522 |
+
resultDiv.classList.add('hidden');
|
| 523 |
+
|
| 524 |
+
try {
|
| 525 |
+
const payload = { text, source };
|
| 526 |
+
if (modelKey) payload.model_key = modelKey;
|
| 527 |
+
|
| 528 |
+
const response = await fetch('/api/sentiment/analyze', {
|
| 529 |
+
method: 'POST',
|
| 530 |
+
headers: { 'Content-Type': 'application/json' },
|
| 531 |
+
body: JSON.stringify(payload)
|
| 532 |
+
});
|
| 533 |
+
|
| 534 |
+
const data = await response.json();
|
| 535 |
+
|
| 536 |
+
if (!response.ok || !data.ok) {
|
| 537 |
+
throw new Error(data.error || 'Sentiment analysis failed');
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
this.displaySentimentResult(resultDiv, data);
|
| 541 |
+
} catch (error) {
|
| 542 |
+
this.showError(resultDiv, error.message);
|
| 543 |
+
} finally {
|
| 544 |
+
btn.disabled = false;
|
| 545 |
+
btn.innerHTML = 'Analyze Sentiment';
|
| 546 |
+
}
|
| 547 |
+
},
|
| 548 |
+
|
| 549 |
+
displaySentimentResult(container, data) {
|
| 550 |
+
const label = data.label || 'unknown';
|
| 551 |
+
const score = (data.score * 100).toFixed(1);
|
| 552 |
+
const labelClass = label.toLowerCase();
|
| 553 |
+
|
| 554 |
+
let html = '<div class="result-box">';
|
| 555 |
+
html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Sentiment Analysis Result</h3>';
|
| 556 |
+
html += `<div style="margin-bottom: 15px;">`;
|
| 557 |
+
html += `<span class="badge badge-${labelClass}">${label.toUpperCase()}</span>`;
|
| 558 |
+
html += `<span style="font-size: 1.3rem; font-weight: 700; color: #e2e8f0;">${score}%</span>`;
|
| 559 |
+
html += `</div>`;
|
| 560 |
+
|
| 561 |
+
if (data.model) {
|
| 562 |
+
html += `<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 15px;">Model: ${data.model}</p>`;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
if (data.details && data.details.labels && data.details.scores) {
|
| 566 |
+
html += '<div class="score-bar">';
|
| 567 |
+
for (let i = 0; i < data.details.labels.length; i++) {
|
| 568 |
+
const lbl = data.details.labels[i];
|
| 569 |
+
const scr = (data.details.scores[i] * 100).toFixed(1);
|
| 570 |
+
html += '<div class="score-item">';
|
| 571 |
+
html += `<span class="score-label">${lbl}</span>`;
|
| 572 |
+
html += '<div class="score-progress">';
|
| 573 |
+
html += `<div class="score-fill" style="width: ${scr}%"></div>`;
|
| 574 |
+
html += '</div>';
|
| 575 |
+
html += `<span class="score-value">${scr}%</span>`;
|
| 576 |
+
html += '</div>';
|
| 577 |
+
}
|
| 578 |
+
html += '</div>';
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
html += '</div>';
|
| 582 |
+
container.innerHTML = html;
|
| 583 |
+
container.classList.remove('hidden');
|
| 584 |
+
},
|
| 585 |
+
|
| 586 |
+
// Text Summarization
|
| 587 |
+
async summarizeText() {
|
| 588 |
+
const text = document.getElementById('summary-input').value.trim();
|
| 589 |
+
const maxSentences = parseInt(document.getElementById('max-sentences').value);
|
| 590 |
+
const btn = document.getElementById('summarize-btn');
|
| 591 |
+
const resultDiv = document.getElementById('summary-result');
|
| 592 |
+
|
| 593 |
+
if (!text) {
|
| 594 |
+
this.showError(resultDiv, 'Please enter text to summarize');
|
| 595 |
+
return;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
btn.disabled = true;
|
| 599 |
+
btn.innerHTML = '<span class="loading"></span> Summarizing...';
|
| 600 |
+
resultDiv.classList.add('hidden');
|
| 601 |
+
|
| 602 |
+
try {
|
| 603 |
+
const response = await fetch('/api/ai/summarize', {
|
| 604 |
+
method: 'POST',
|
| 605 |
+
headers: { 'Content-Type': 'application/json' },
|
| 606 |
+
body: JSON.stringify({ text, max_sentences: maxSentences })
|
| 607 |
+
});
|
| 608 |
+
|
| 609 |
+
const data = await response.json();
|
| 610 |
+
|
| 611 |
+
if (!response.ok || !data.ok) {
|
| 612 |
+
throw new Error(data.error || 'Summarization failed');
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
this.displaySummaryResult(resultDiv, data);
|
| 616 |
+
} catch (error) {
|
| 617 |
+
this.showError(resultDiv, error.message);
|
| 618 |
+
} finally {
|
| 619 |
+
btn.disabled = false;
|
| 620 |
+
btn.innerHTML = 'Summarize';
|
| 621 |
+
}
|
| 622 |
+
},
|
| 623 |
+
|
| 624 |
+
displaySummaryResult(container, data) {
|
| 625 |
+
let html = '<div class="result-box">';
|
| 626 |
+
html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Summary</h3>';
|
| 627 |
+
|
| 628 |
+
if (data.summary) {
|
| 629 |
+
html += `<div class="summary-text">${this.escapeHtml(data.summary)}</div>`;
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
if (data.sentences && data.sentences.length > 0) {
|
| 633 |
+
html += '<h4 style="margin: 20px 0 10px 0; color: #cbd5e1; font-size: 1.1rem;">Key Sentences</h4>';
|
| 634 |
+
html += '<ul class="sentences-list">';
|
| 635 |
+
data.sentences.forEach(sentence => {
|
| 636 |
+
html += `<li>${this.escapeHtml(sentence)}</li>`;
|
| 637 |
+
});
|
| 638 |
+
html += '</ul>';
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
html += '</div>';
|
| 642 |
+
container.innerHTML = html;
|
| 643 |
+
container.classList.remove('hidden');
|
| 644 |
+
},
|
| 645 |
+
|
| 646 |
+
// Model Status & Diagnostics
|
| 647 |
+
async loadModelStatus() {
|
| 648 |
+
const statusDiv = document.getElementById('registry-status');
|
| 649 |
+
const tableDiv = document.getElementById('models-table');
|
| 650 |
+
const btn = document.getElementById('refresh-status-btn');
|
| 651 |
+
|
| 652 |
+
btn.disabled = true;
|
| 653 |
+
btn.innerHTML = '<span class="loading"></span> Loading...';
|
| 654 |
+
|
| 655 |
+
try {
|
| 656 |
+
const [statusRes, listRes] = await Promise.all([
|
| 657 |
+
fetch('/api/models/status'),
|
| 658 |
+
fetch('/api/models/list')
|
| 659 |
+
]);
|
| 660 |
+
|
| 661 |
+
const statusData = await statusRes.json();
|
| 662 |
+
const listData = await listRes.json();
|
| 663 |
+
|
| 664 |
+
this.displayRegistryStatus(statusDiv, statusData);
|
| 665 |
+
this.displayModelsTable(tableDiv, listData);
|
| 666 |
+
} catch (error) {
|
| 667 |
+
this.showError(statusDiv, 'Failed to load model status: ' + error.message);
|
| 668 |
+
} finally {
|
| 669 |
+
btn.disabled = false;
|
| 670 |
+
btn.innerHTML = 'Refresh';
|
| 671 |
+
}
|
| 672 |
+
},
|
| 673 |
+
|
| 674 |
+
displayRegistryStatus(container, data) {
|
| 675 |
+
let html = '<div class="status-grid">';
|
| 676 |
+
|
| 677 |
+
html += '<div class="status-item">';
|
| 678 |
+
html += '<div class="status-label">HF Mode</div>';
|
| 679 |
+
html += `<div class="status-value">${data.hf_mode || 'unknown'}</div>`;
|
| 680 |
+
html += '</div>';
|
| 681 |
+
|
| 682 |
+
html += '<div class="status-item">';
|
| 683 |
+
html += '<div class="status-label">Overall Status</div>';
|
| 684 |
+
html += `<div class="status-value">${data.status || 'unknown'}</div>`;
|
| 685 |
+
html += '</div>';
|
| 686 |
+
|
| 687 |
+
html += '<div class="status-item">';
|
| 688 |
+
html += '<div class="status-label">Models Loaded</div>';
|
| 689 |
+
html += `<div class="status-value">${data.models_loaded || 0}</div>`;
|
| 690 |
+
html += '</div>';
|
| 691 |
+
|
| 692 |
+
html += '<div class="status-item">';
|
| 693 |
+
html += '<div class="status-label">Models Failed</div>';
|
| 694 |
+
html += `<div class="status-value">${data.models_failed || 0}</div>`;
|
| 695 |
+
html += '</div>';
|
| 696 |
+
|
| 697 |
+
html += '</div>';
|
| 698 |
+
|
| 699 |
+
if (data.status === 'disabled' || data.hf_mode === 'off') {
|
| 700 |
+
html += '<div class="info-box">';
|
| 701 |
+
html += '<strong>Note:</strong> HF models are disabled. To enable them, set HF_MODE=public or HF_MODE=auth in the environment.';
|
| 702 |
+
html += '</div>';
|
| 703 |
+
} else if (data.models_loaded === 0 && data.status !== 'disabled') {
|
| 704 |
+
html += '<div class="warning-box">';
|
| 705 |
+
html += '<strong>Warning:</strong> No models could be loaded. Check model IDs or HF credentials.';
|
| 706 |
+
html += '</div>';
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
if (data.error) {
|
| 710 |
+
html += '<div class="error-box" style="margin-top: 15px;">';
|
| 711 |
+
html += `<strong>Error:</strong> ${this.escapeHtml(data.error)}`;
|
| 712 |
+
html += '</div>';
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
if (data.failed && data.failed.length > 0) {
|
| 716 |
+
html += '<div style="margin-top: 20px;">';
|
| 717 |
+
html += '<h4 style="color: #cbd5e1; margin-bottom: 10px;">Failed Models</h4>';
|
| 718 |
+
html += '<div style="background: rgba(30, 41, 59, 0.6); border-radius: 8px; padding: 15px;">';
|
| 719 |
+
data.failed.forEach(([key, error]) => {
|
| 720 |
+
html += `<div style="margin-bottom: 8px; padding: 8px; background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; border-radius: 4px;">`;
|
| 721 |
+
html += `<strong style="color: #fca5a5;">${key}:</strong> `;
|
| 722 |
+
html += `<span style="color: #cbd5e1;">${this.escapeHtml(error)}</span>`;
|
| 723 |
+
html += `</div>`;
|
| 724 |
+
});
|
| 725 |
+
html += '</div>';
|
| 726 |
+
html += '</div>';
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
container.innerHTML = html;
|
| 730 |
+
},
|
| 731 |
+
|
| 732 |
+
displayModelsTable(container, data) {
|
| 733 |
+
if (!data.models || data.models.length === 0) {
|
| 734 |
+
container.innerHTML = '<div class="info-box">No models configured</div>';
|
| 735 |
+
return;
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
let html = '<div class="table-container">';
|
| 739 |
+
html += '<table>';
|
| 740 |
+
html += '<thead><tr>';
|
| 741 |
+
html += '<th>Key</th>';
|
| 742 |
+
html += '<th>Task</th>';
|
| 743 |
+
html += '<th>Model ID</th>';
|
| 744 |
+
html += '<th>Loaded</th>';
|
| 745 |
+
html += '<th>Error</th>';
|
| 746 |
+
html += '</tr></thead>';
|
| 747 |
+
html += '<tbody>';
|
| 748 |
+
|
| 749 |
+
data.models.forEach(model => {
|
| 750 |
+
html += '<tr>';
|
| 751 |
+
html += `<td><strong>${model.key || 'N/A'}</strong></td>`;
|
| 752 |
+
html += `<td>${model.task || 'N/A'}</td>`;
|
| 753 |
+
html += `<td style="font-family: monospace; font-size: 0.85rem;">${model.model_id || 'N/A'}</td>`;
|
| 754 |
+
html += '<td>';
|
| 755 |
+
if (model.loaded) {
|
| 756 |
+
html += '<span class="badge badge-success">Yes</span>';
|
| 757 |
+
} else {
|
| 758 |
+
html += '<span class="badge badge-danger">No</span>';
|
| 759 |
+
}
|
| 760 |
+
html += '</td>';
|
| 761 |
+
html += `<td style="color: #f87171; font-size: 0.85rem;">${model.error ? this.escapeHtml(model.error) : '-'}</td>`;
|
| 762 |
+
html += '</tr>';
|
| 763 |
+
});
|
| 764 |
+
|
| 765 |
+
html += '</tbody>';
|
| 766 |
+
html += '</table>';
|
| 767 |
+
html += '</div>';
|
| 768 |
+
|
| 769 |
+
container.innerHTML = html;
|
| 770 |
+
},
|
| 771 |
+
|
| 772 |
+
// Utility functions
|
| 773 |
+
showError(container, message) {
|
| 774 |
+
container.innerHTML = `<div class="error-box"><strong>Error:</strong> ${this.escapeHtml(message)}</div>`;
|
| 775 |
+
container.classList.remove('hidden');
|
| 776 |
+
},
|
| 777 |
+
|
| 778 |
+
escapeHtml(text) {
|
| 779 |
+
const div = document.createElement('div');
|
| 780 |
+
div.textContent = text;
|
| 781 |
+
return div.innerHTML;
|
| 782 |
+
},
|
| 783 |
+
|
| 784 |
+
// Initialize
|
| 785 |
+
init() {
|
| 786 |
+
document.getElementById('analyze-sentiment-btn').addEventListener('click', () => this.analyzeSentiment());
|
| 787 |
+
document.getElementById('summarize-btn').addEventListener('click', () => this.summarizeText());
|
| 788 |
+
document.getElementById('refresh-status-btn').addEventListener('click', () => this.loadModelStatus());
|
| 789 |
+
|
| 790 |
+
this.loadModelStatus();
|
| 791 |
+
}
|
| 792 |
+
};
|
| 793 |
+
|
| 794 |
+
if (document.readyState === 'loading') {
|
| 795 |
+
document.addEventListener('DOMContentLoaded', () => AITools.init());
|
| 796 |
+
} else {
|
| 797 |
+
AITools.init();
|
| 798 |
+
}
|
| 799 |
+
})();
|
| 800 |
+
</script>
|
| 801 |
+
</body>
|
| 802 |
+
</html>
|
templates/index.html
CHANGED
|
@@ -1961,6 +1961,10 @@
|
|
| 1961 |
<span class="tab-icon"><svg><use href="#icon-reports"></use></svg></span>
|
| 1962 |
Reports
|
| 1963 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1964 |
</div>
|
| 1965 |
</div>
|
| 1966 |
|
|
|
|
| 1961 |
<span class="tab-icon"><svg><use href="#icon-reports"></use></svg></span>
|
| 1962 |
Reports
|
| 1963 |
</button>
|
| 1964 |
+
<a href="/ai-tools" class="tab" style="text-decoration: none; display: inline-flex; align-items: center; gap: 8px;">
|
| 1965 |
+
<span class="tab-icon"><svg><use href="#icon-hf"></use></svg></span>
|
| 1966 |
+
AI Tools
|
| 1967 |
+
</a>
|
| 1968 |
</div>
|
| 1969 |
</div>
|
| 1970 |
|