yana-web 0.1.0

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.
@@ -0,0 +1,275 @@
1
+ /* ============================================================
2
+ Yana AI — theme tokens
3
+ Four themes, one architecture:
4
+ dawn — Lotus Dawn: pink-gold sunrise over the lake
5
+ jade — Jade Lake: warm pearl, jade green, clear water
6
+ mist — Morning Mist: near-monochrome paper, minimal glass
7
+ silver— Glass Silver: cool silver-blue liquid glass
8
+ Glass knobs (set by Tweaks): --blur, --alpha, --reflect, --depth (0..1)
9
+ Density: --sp (0.85 compact / 1 regular / 1.18 spacious)
10
+ ============================================================ */
11
+
12
+ :root {
13
+ --font-ui: "Be Vietnam Pro", -apple-system, "Segoe UI", sans-serif;
14
+
15
+ --blur: 0.7;
16
+ --alpha: 0.6;
17
+ --reflect: 0.7;
18
+ --depth: 0.55;
19
+ --sp: 1;
20
+
21
+ --pad-card: calc(18px * var(--sp));
22
+ --gap: calc(14px * var(--sp));
23
+
24
+ --r-lg: 20px;
25
+ --r-md: 14px;
26
+ --r-sm: 9px;
27
+ }
28
+
29
+ /* ---------- Jade Lake (default) ---------- */
30
+ [data-theme="jade"] {
31
+ --bg-base: #f2f7f4;
32
+ --bg-scene:
33
+ radial-gradient(1100px 520px at 12% -8%, rgba(233, 201, 138, 0.14), transparent 60%),
34
+ radial-gradient(900px 600px at 88% 4%, rgba(232, 180, 192, 0.15), transparent 58%),
35
+ radial-gradient(1300px 800px at 50% 110%, rgba(122, 184, 168, 0.20), transparent 62%),
36
+ linear-gradient(180deg, #f6faf7 0%, #eef5f1 100%);
37
+ --surface-rgb: 255, 255, 255;
38
+ --shadow-rgb: 20, 40, 36;
39
+ --ink: #21403a;
40
+ --ink-2: #51695f;
41
+ --ink-3: #84988f;
42
+ --primary: #2f7e6e;
43
+ --primary-soft: rgba(47, 126, 110, 0.10);
44
+ --pink: #c77b92;
45
+ --pink-soft: rgba(199, 123, 146, 0.12);
46
+ --gold: #b78f3d;
47
+ --gold-soft: rgba(183, 143, 61, 0.13);
48
+ --good: #3d8b6e;
49
+ --warn: #b78f3d;
50
+ --border: rgba(31, 56, 51, 0.10);
51
+ --border-strong: rgba(31, 56, 51, 0.16);
52
+ --glass-blur-max: 24px;
53
+ }
54
+
55
+ /* ---------- Lotus Dawn ---------- */
56
+ [data-theme="dawn"] {
57
+ --bg-base: #f9f4f2;
58
+ --bg-scene:
59
+ radial-gradient(1100px 540px at 16% -10%, rgba(236, 196, 134, 0.26), transparent 60%),
60
+ radial-gradient(1000px 620px at 86% 2%, rgba(231, 168, 185, 0.28), transparent 58%),
61
+ radial-gradient(1300px 760px at 50% 112%, rgba(168, 196, 186, 0.18), transparent 64%),
62
+ linear-gradient(180deg, #faf5f3 0%, #f4ece9 100%);
63
+ --surface-rgb: 255, 253, 252;
64
+ --shadow-rgb: 54, 36, 38;
65
+ --ink: #3c2e33;
66
+ --ink-2: #6f5d62;
67
+ --ink-3: #a08e92;
68
+ --primary: #b96b80;
69
+ --primary-soft: rgba(185, 107, 128, 0.11);
70
+ --pink: #b96b80;
71
+ --pink-soft: rgba(185, 107, 128, 0.11);
72
+ --gold: #b78a3f;
73
+ --gold-soft: rgba(183, 138, 63, 0.14);
74
+ --good: #4f8b6e;
75
+ --warn: #b78a3f;
76
+ --border: rgba(60, 46, 51, 0.10);
77
+ --border-strong: rgba(60, 46, 51, 0.16);
78
+ --glass-blur-max: 24px;
79
+ }
80
+
81
+ /* ---------- Morning Mist ---------- */
82
+ [data-theme="mist"] {
83
+ --bg-base: #f7f6f3;
84
+ --bg-scene:
85
+ radial-gradient(1200px 600px at 50% -12%, rgba(214, 222, 214, 0.35), transparent 62%),
86
+ linear-gradient(180deg, #f8f7f4 0%, #f3f2ee 100%);
87
+ --surface-rgb: 255, 255, 255;
88
+ --shadow-rgb: 30, 38, 34;
89
+ --ink: #26302c;
90
+ --ink-2: #5b6661;
91
+ --ink-3: #939c96;
92
+ --primary: #4a7a6a;
93
+ --primary-soft: rgba(74, 122, 106, 0.09);
94
+ --pink: #ab7e8d;
95
+ --pink-soft: rgba(171, 126, 141, 0.10);
96
+ --gold: #a3884e;
97
+ --gold-soft: rgba(163, 136, 78, 0.11);
98
+ --good: #4a7a6a;
99
+ --warn: #a3884e;
100
+ --border: rgba(38, 48, 44, 0.09);
101
+ --border-strong: rgba(38, 48, 44, 0.15);
102
+ --glass-blur-max: 10px;
103
+ }
104
+
105
+ /* ---------- Glass Silver ---------- */
106
+ [data-theme="silver"] {
107
+ --bg-base: #eef2f6;
108
+ --bg-scene:
109
+ radial-gradient(1100px 560px at 18% -10%, rgba(168, 199, 224, 0.30), transparent 60%),
110
+ radial-gradient(1000px 620px at 85% 0%, rgba(199, 214, 229, 0.32), transparent 58%),
111
+ radial-gradient(1300px 760px at 50% 112%, rgba(122, 158, 186, 0.22), transparent 64%),
112
+ linear-gradient(180deg, #f3f6fa 0%, #e9eef4 100%);
113
+ --surface-rgb: 252, 254, 255;
114
+ --shadow-rgb: 18, 34, 46;
115
+ --ink: #1c2f3a;
116
+ --ink-2: #4c6271;
117
+ --ink-3: #8194a1;
118
+ --primary: #3a7ca5;
119
+ --primary-soft: rgba(58, 124, 165, 0.10);
120
+ --pink: #b585a4;
121
+ --pink-soft: rgba(181, 133, 164, 0.12);
122
+ --gold: #a98e4b;
123
+ --gold-soft: rgba(169, 142, 75, 0.13);
124
+ --good: #44897c;
125
+ --warn: #a98e4b;
126
+ --border: rgba(28, 43, 51, 0.11);
127
+ --border-strong: rgba(28, 43, 51, 0.18);
128
+ --glass-blur-max: 32px;
129
+ }
130
+
131
+ /* ============ base ============ */
132
+ * { box-sizing: border-box; }
133
+ html, body { margin: 0; padding: 0; height: 100%; }
134
+ body {
135
+ font-family: var(--font-ui);
136
+ color: var(--ink);
137
+ background: var(--bg-base);
138
+ -webkit-font-smoothing: antialiased;
139
+ font-size: 14px;
140
+ line-height: 1.5;
141
+ }
142
+ #root { height: 100%; }
143
+
144
+ .scene { position: fixed; inset: 0; background: var(--bg-scene); z-index: 0; }
145
+
146
+ /* Yana signature: quiet ripples on the lake surface */
147
+ .scene::after {
148
+ content: "";
149
+ position: absolute; inset: 0;
150
+ background:
151
+ repeating-radial-gradient(circle at 78% 86%, rgba(var(--surface-rgb), 0) 0 54px, rgba(var(--shadow-rgb), 0.028) 54px 56px, rgba(var(--surface-rgb), 0) 56px 120px),
152
+ repeating-radial-gradient(circle at 14% 102%, rgba(var(--surface-rgb), 0) 0 70px, rgba(var(--shadow-rgb), 0.022) 70px 72px, rgba(var(--surface-rgb), 0) 72px 150px);
153
+ -webkit-mask-image: linear-gradient(180deg, transparent 30%, black 75%);
154
+ mask-image: linear-gradient(180deg, transparent 30%, black 75%);
155
+ pointer-events: none;
156
+ }
157
+
158
+ /* ============ glass surfaces ============ */
159
+ /* Transparency slider raises see-through-ness; reflection lights the top edge;
160
+ depth scales the shadow; blur scales backdrop blur. */
161
+ .glass, .glass-strong {
162
+ border: 1px solid var(--border);
163
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-max) * var(--blur))) saturate(1.15);
164
+ backdrop-filter: blur(calc(var(--glass-blur-max) * var(--blur))) saturate(1.15);
165
+ box-shadow:
166
+ 0 1px 2px rgba(var(--shadow-rgb), calc(0.02 + var(--depth) * 0.05)),
167
+ 0 12px 36px rgba(var(--shadow-rgb), calc(0.02 + var(--depth) * 0.10)),
168
+ inset 0 1px 0 rgba(255, 255, 255, calc(var(--reflect) * 0.85));
169
+ }
170
+ .glass { background: rgba(var(--surface-rgb), calc(0.92 - var(--alpha) * 0.48)); }
171
+ .glass-strong { background: rgba(var(--surface-rgb), calc(0.97 - var(--alpha) * 0.25)); }
172
+
173
+ /* ============ shared atoms ============ */
174
+ .h-display { font-weight: 300; letter-spacing: -0.02em; }
175
+ .label-xs {
176
+ font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
177
+ text-transform: uppercase; color: var(--ink-3);
178
+ }
179
+ .num-lg { font-size: 32px; font-weight: 300; letter-spacing: -0.02em; line-height: 1.1; color: var(--ink); }
180
+
181
+ .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
182
+ .dot.on { background: var(--good); box-shadow: 0 0 0 3px color-mix(in oklab, var(--good) 18%, transparent); }
183
+ .dot.idle { background: var(--gold); box-shadow: 0 0 0 3px color-mix(in oklab, var(--gold) 16%, transparent); }
184
+ .dot.off { background: var(--ink-3); }
185
+
186
+ .chip {
187
+ display: inline-flex; align-items: center; gap: 6px;
188
+ padding: 3px 10px; border-radius: 99px;
189
+ font-size: 12px; font-weight: 500;
190
+ background: var(--primary-soft); color: var(--primary);
191
+ }
192
+ .chip.pink { background: var(--pink-soft); color: var(--pink); }
193
+ .chip.gold { background: var(--gold-soft); color: var(--gold); }
194
+ .chip.neutral { background: rgba(var(--surface-rgb), 0.6); border: 1px solid var(--border); color: var(--ink-2); }
195
+
196
+ .bar { height: 5px; border-radius: 99px; background: rgba(var(--shadow-rgb), 0.08); overflow: hidden; }
197
+ .bar > i { display: block; height: 100%; border-radius: 99px; background: var(--primary); }
198
+
199
+ button { font-family: inherit; }
200
+ ::selection { background: var(--primary-soft); }
201
+
202
+ *::-webkit-scrollbar { width: 10px; height: 10px; }
203
+ *::-webkit-scrollbar-thumb { background: rgba(var(--shadow-rgb), 0.14); border-radius: 99px; border: 3px solid transparent; background-clip: content-box; }
204
+ *::-webkit-scrollbar-track { background: transparent; }
205
+
206
+ /* ============ responsive: sidebar becomes a drawer ≤ 860px ============ */
207
+ .yana-app { padding: calc(16px * var(--sp)); }
208
+ .yana-sidebar { width: 218px; flex: none; position: relative; z-index: 2; }
209
+ .yana-chat-aside { width: 240px; flex: none; }
210
+ .yana-menu-btn, .yana-backdrop { display: none; }
211
+
212
+ @media (max-width: 860px) {
213
+ .yana-app { padding: 10px; }
214
+
215
+ .yana-sidebar {
216
+ position: fixed; top: 10px; bottom: 10px; left: 10px; z-index: 40;
217
+ width: min(280px, 80vw);
218
+ transform: translateX(calc(-100% - 20px));
219
+ transition: transform .28s cubic-bezier(.4, 0, .2, 1);
220
+ overflow-y: auto;
221
+ /* drawer floats over content — needs a near-solid surface to stay readable */
222
+ background: rgba(var(--surface-rgb), .94);
223
+ }
224
+ .yana-sidebar.open { transform: translateX(0); }
225
+
226
+ .yana-backdrop {
227
+ display: block; position: fixed; inset: 0; z-index: 39;
228
+ background: rgba(var(--shadow-rgb), .30);
229
+ opacity: 0; pointer-events: none;
230
+ transition: opacity .28s;
231
+ }
232
+ .yana-backdrop.show { opacity: 1; pointer-events: auto; }
233
+
234
+ .yana-menu-btn {
235
+ display: grid; place-items: center;
236
+ position: fixed; top: 12px; left: 12px; z-index: 38;
237
+ width: 40px; height: 40px; border-radius: 13px;
238
+ border: 1px solid var(--border); cursor: pointer;
239
+ color: var(--ink-2);
240
+ }
241
+
242
+ /* clear the floating menu button */
243
+ .yana-main { padding-top: 50px; }
244
+
245
+ /* chat info panels (routing/context/safety) yield to the conversation */
246
+ .yana-chat-aside { display: none; }
247
+ }
248
+
249
+ @media (prefers-reduced-motion: reduce) {
250
+ .yana-sidebar, .yana-backdrop { transition: none; }
251
+ }
252
+
253
+ /* ============ life beneath the surface ============ */
254
+ .pulse { animation: none; }
255
+ .mote {
256
+ position: absolute;
257
+ width: 5px; height: 5px; border-radius: 50%;
258
+ background: color-mix(in oklab, var(--primary) 60%, white);
259
+ opacity: 0; filter: blur(1.2px);
260
+ pointer-events: none;
261
+ }
262
+ @media (prefers-reduced-motion: no-preference) {
263
+ .pulse { animation: yana-pulse 3.2s ease-in-out infinite; }
264
+ .mote { animation: yana-drift var(--dur, 70s) linear infinite; animation-delay: var(--delay, 0s); }
265
+ }
266
+ @keyframes yana-pulse {
267
+ 0%, 100% { box-shadow: 0 0 0 3px color-mix(in oklab, var(--good) 18%, transparent); }
268
+ 50% { box-shadow: 0 0 0 6px color-mix(in oklab, var(--good) 8%, transparent); }
269
+ }
270
+ @keyframes yana-drift {
271
+ 0% { transform: translate(0, 0); opacity: 0; }
272
+ 12% { opacity: var(--peak, 0.22); }
273
+ 88% { opacity: var(--peak, 0.22); }
274
+ 100% { transform: translate(var(--dx, 40px), var(--dy, -60px)); opacity: 0; }
275
+ }
@@ -0,0 +1,251 @@
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
6
+ <link rel="icon" type="image/png" href="/logo.png" />
7
+ <title>Yana — Trợ lý AI của bạn</title>
8
+ <link rel="preconnect" href="https://fonts.bunny.net" crossorigin />
9
+ <link href="https://fonts.bunny.net/css?family=be-vietnam-pro:300,400,500,600&display=swap" rel="stylesheet" />
10
+ <style>
11
+ :root {
12
+ --primary: #2f7e6e;
13
+ --ink: #1d3530; --ink-2: #44615a; --ink-3: #7a948d;
14
+ --border: rgba(31, 70, 60, .14);
15
+ }
16
+ * { box-sizing: border-box; }
17
+ html, body { height: 100%; }
18
+ body {
19
+ margin: 0; overflow-x: hidden;
20
+ font-family: "Be Vietnam Pro", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
21
+ background: linear-gradient(160deg, #f6faf7 30%, #ddeee7 100%);
22
+ color: var(--ink);
23
+ display: grid; place-items: center; min-height: 100vh;
24
+ }
25
+
26
+ /* ── Aurora backdrop ── */
27
+ .aurora { position: fixed; inset: 0; pointer-events: none; filter: blur(70px); z-index: 0; }
28
+ .blob { position: absolute; border-radius: 50%; opacity: .5; animation: drift 26s ease-in-out infinite alternate; }
29
+ .b1 { width: 46vw; height: 46vw; left: -10vw; top: -16vh; background: radial-gradient(circle, rgba(122,184,168,.8), transparent 65%); }
30
+ .b2 { width: 38vw; height: 38vw; right: -8vw; bottom: -14vh; background: radial-gradient(circle, rgba(86,148,159,.7), transparent 65%); animation-delay: -9s; }
31
+ .b3 { width: 30vw; height: 30vw; right: 16vw; top: -10vh; background: radial-gradient(circle, rgba(236,196,134,.55), transparent 65%); animation-delay: -17s; }
32
+ @keyframes drift {
33
+ from { transform: translate(0, 0) scale(1); }
34
+ to { transform: translate(5vw, 6vh) scale(1.12); }
35
+ }
36
+ @media (prefers-reduced-motion: reduce) { .blob, .mark, .stage > * { animation: none !important; opacity: 1 !important; transform: none !important; } }
37
+
38
+ /* ── Stage ── */
39
+ .stage {
40
+ position: relative; z-index: 1; width: min(94vw, 720px);
41
+ padding: 48px 24px 36px; text-align: center;
42
+ }
43
+ .stage > * { animation: rise .6s cubic-bezier(.21,.8,.32,1) both; }
44
+
45
+ .lang {
46
+ position: fixed; top: 18px; right: 18px; z-index: 2;
47
+ display: flex; gap: 2px; padding: 3px;
48
+ background: rgba(255,255,255,.55); border: 1px solid rgba(255,255,255,.7);
49
+ border-radius: 99px; backdrop-filter: blur(12px);
50
+ }
51
+ .lang button {
52
+ border: none; cursor: pointer; font-family: inherit;
53
+ font-size: 10.5px; font-weight: 600; letter-spacing: .03em;
54
+ padding: 4px 10px; border-radius: 99px; color: var(--ink-3);
55
+ background: transparent; transition: background .15s, color .15s;
56
+ }
57
+ .lang button.on { background: rgba(255,255,255,.95); color: var(--ink); box-shadow: 0 1px 3px rgba(31,70,60,.12); }
58
+
59
+ .mark {
60
+ width: 84px; height: 84px; margin: 0 auto 24px; border-radius: 27px;
61
+ display: grid; place-items: center;
62
+ background: linear-gradient(150deg, color-mix(in oklab, var(--primary) 92%, white), color-mix(in oklab, var(--primary) 72%, #1d3530));
63
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.4), 0 14px 36px color-mix(in oklab, var(--primary) 32%, transparent);
64
+ animation: rise .6s cubic-bezier(.21,.8,.32,1) both, breathe 5s ease-in-out 1s infinite;
65
+ }
66
+ @keyframes breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.03); } }
67
+ @keyframes rise { from { opacity: 0; transform: translateY(18px); } }
68
+
69
+ h1 {
70
+ margin: 0 0 10px; font-size: clamp(28px, 5vw, 38px); font-weight: 600;
71
+ letter-spacing: -0.02em; animation-delay: .06s;
72
+ }
73
+ h1 .name { color: var(--primary); }
74
+ .tagline {
75
+ margin: 0 auto 34px; max-width: 46ch;
76
+ font-size: clamp(13.5px, 2.2vw, 15.5px); color: var(--ink-2); line-height: 1.7;
77
+ animation-delay: .12s;
78
+ }
79
+
80
+ /* Feature cards */
81
+ .feats {
82
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
83
+ gap: 12px; margin-bottom: 36px; animation-delay: .18s;
84
+ }
85
+ .feat {
86
+ padding: 18px 14px 16px; border-radius: 18px; text-align: center;
87
+ background: rgba(255,255,255,.5);
88
+ backdrop-filter: blur(16px) saturate(1.2);
89
+ -webkit-backdrop-filter: blur(16px) saturate(1.2);
90
+ border: 1px solid rgba(255,255,255,.65);
91
+ box-shadow: 0 8px 26px rgba(31,70,60,.07), inset 0 1px 0 rgba(255,255,255,.7);
92
+ transition: transform .2s ease, box-shadow .2s ease;
93
+ }
94
+ .feat:hover { transform: translateY(-3px); box-shadow: 0 12px 32px rgba(31,70,60,.11), inset 0 1px 0 rgba(255,255,255,.7); }
95
+ .feat .ico {
96
+ width: 38px; height: 38px; margin: 0 auto 10px; border-radius: 12px;
97
+ display: grid; place-items: center; color: var(--primary);
98
+ background: color-mix(in oklab, var(--primary) 10%, transparent);
99
+ }
100
+ .feat .t { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
101
+ .feat .d { font-size: 11.5px; color: var(--ink-3); line-height: 1.5; }
102
+
103
+ /* CTA */
104
+ .ctas { display: flex; gap: 12px; justify-content: center; align-items: center; flex-wrap: wrap; animation-delay: .24s; }
105
+ a.cta {
106
+ display: inline-flex; align-items: center; gap: 9px;
107
+ padding: 14px 30px; border-radius: 16px; text-decoration: none;
108
+ font-size: 15px; font-weight: 500; color: white;
109
+ background: var(--primary);
110
+ box-shadow: 0 10px 28px color-mix(in oklab, var(--primary) 38%, transparent);
111
+ transition: transform .12s, box-shadow .2s;
112
+ }
113
+ a.cta:hover { box-shadow: 0 12px 34px color-mix(in oklab, var(--primary) 50%, transparent); transform: translateY(-1px); }
114
+ a.cta:active { transform: translateY(1px); }
115
+ .cta-note { font-size: 12px; color: var(--ink-3); margin-top: 14px; animation-delay: .3s; }
116
+
117
+ .foot {
118
+ margin-top: 40px; display: flex; align-items: center; justify-content: center; gap: 7px;
119
+ font-size: 11px; color: var(--ink-3); letter-spacing: .05em; animation-delay: .36s;
120
+ }
121
+ .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 6px var(--primary); }
122
+
123
+ @media (max-width: 540px) {
124
+ .feats { grid-template-columns: 1fr 1fr; }
125
+ .stage { padding-top: 64px; }
126
+ }
127
+ </style>
128
+ </head>
129
+ <body>
130
+ <div class="aurora"><span class="blob b1"></span><span class="blob b2"></span><span class="blob b3"></span></div>
131
+
132
+ <div class="lang" role="group" aria-label="Language">
133
+ <button type="button" id="lang-vi" class="on">VI</button>
134
+ <button type="button" id="lang-en">EN</button>
135
+ </div>
136
+
137
+ <main class="stage">
138
+ <div class="mark" aria-label="Yana">
139
+ <img src="/logo.png" alt="" width="52" height="52" style="display:block" />
140
+ </div>
141
+
142
+ <h1 id="title">Gặp <span class="name">Yana</span> — trợ lý AI của bạn</h1>
143
+ <p class="tagline" id="tagline">
144
+ Một cuộc trò chuyện, nhiều bàn tay. Yana định tuyến từng yêu cầu đến đúng mô hình,
145
+ ghi nhớ những gì quan trọng, và mọi hành động đều được YAMTAM giám sát an toàn.
146
+ </p>
147
+
148
+ <div class="feats">
149
+ <div class="feat">
150
+ <div class="ico">
151
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 9.5c0 3.3-3.1 6-7 6-.9 0-1.8-.14-2.6-.4L3 16.5l1.2-3.1C3.4 12.3 3 11 3 9.5c0-3.3 3.1-6 7-6s7 2.7 7 6Z"/></svg>
152
+ </div>
153
+ <div class="t" id="f1t">Chat đa mô hình</div>
154
+ <div class="d" id="f1d">Claude, GPT, Gemini, Groq… — một khung chat, tự chọn đúng mô hình</div>
155
+ </div>
156
+ <div class="feat">
157
+ <div class="ico">
158
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="10" cy="10" r="7"/><circle cx="10" cy="10" r="3.4"/><circle cx="10" cy="10" r="0.4" fill="currentColor"/></svg>
159
+ </div>
160
+ <div class="t" id="f2t">Nhiệm vụ</div>
161
+ <div class="d" id="f2d">Giao mục tiêu, Yana lập kế hoạch và theo dõi đến khi xong</div>
162
+ </div>
163
+ <div class="feat">
164
+ <div class="ico">
165
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M10 17c4-1.6 6.2-4.4 6.2-8.1C16.2 6 14.5 4 12.3 4 11.3 4 10.4 4.5 10 5.3 9.6 4.5 8.7 4 7.7 4 5.5 4 3.8 6 3.8 8.9 3.8 12.6 6 15.4 10 17Z"/><path d="M10 17V9.5"/></svg>
166
+ </div>
167
+ <div class="t" id="f3t">Vườn ký ức</div>
168
+ <div class="d" id="f3d">Ghi nhớ điều quan trọng về bạn — lưu cục bộ, có mã hóa</div>
169
+ </div>
170
+ <div class="feat">
171
+ <div class="ico">
172
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 2.8 16 5v4.7c0 3.6-2.4 6.4-6 7.8-3.6-1.4-6-4.2-6-7.8V5l6-2.2Z"/><path d="m7.4 9.8 1.8 1.8 3.4-3.6"/></svg>
173
+ </div>
174
+ <div class="t" id="f4t">An toàn YAMTAM</div>
175
+ <div class="d" id="f4d">Mọi lệnh gọi đi qua lớp giám sát — chặn trước khi gây hại</div>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="ctas">
180
+ <a class="cta" href="/login.html" id="cta">
181
+ <span id="cta-text">Bắt đầu</span>
182
+ <svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10h12m-5-5 5 5-5 5"/></svg>
183
+ </a>
184
+ </div>
185
+ <p class="cta-note" id="cta-note">Chạy trên máy bạn · Dùng API key của bạn · Không tài khoản đám mây</p>
186
+
187
+ <div class="foot"><span class="dot"></span> YAMTAM ENGINE · local &amp; private</div>
188
+ </main>
189
+
190
+ <script>
191
+ 'use strict';
192
+ var lang = localStorage.getItem('yana.login.lang') || 'vi';
193
+ var $ = function (id) { return document.getElementById(id); };
194
+
195
+ var T = {
196
+ vi: {
197
+ title: 'Gặp <span class="name">Yana</span> — trợ lý AI của bạn',
198
+ tagline: 'Một cuộc trò chuyện, nhiều bàn tay. Yana định tuyến từng yêu cầu đến đúng mô hình, ghi nhớ những gì quan trọng, và mọi hành động đều được YAMTAM giám sát an toàn.',
199
+ f1t: 'Chat đa mô hình', f1d: 'Claude, GPT, Gemini, Groq… — một khung chat, tự chọn đúng mô hình',
200
+ f2t: 'Nhiệm vụ', f2d: 'Giao mục tiêu, Yana lập kế hoạch và theo dõi đến khi xong',
201
+ f3t: 'Vườn ký ức', f3d: 'Ghi nhớ điều quan trọng về bạn — lưu cục bộ, có mã hóa',
202
+ f4t: 'An toàn YAMTAM', f4d: 'Mọi lệnh gọi đi qua lớp giám sát — chặn trước khi gây hại',
203
+ ctaSetup: 'Bắt đầu', ctaLogin: 'Đăng nhập',
204
+ note: 'Chạy trên máy bạn · Dùng API key của bạn · Không tài khoản đám mây',
205
+ },
206
+ en: {
207
+ title: 'Meet <span class="name">Yana</span> — your AI assistant',
208
+ tagline: 'One conversation, many hands. Yana routes every request to the right model, remembers what matters, and every action is supervised by YAMTAM.',
209
+ f1t: 'Multi-model chat', f1d: 'Claude, GPT, Gemini, Groq… — one chat, the right model picked for you',
210
+ f2t: 'Missions', f2d: 'Give a goal — Yana plans the tasks and tracks them to done',
211
+ f3t: 'Memory Garden', f3d: 'Remembers what matters about you — stored locally, encrypted',
212
+ f4t: 'YAMTAM safety', f4d: 'Every call passes a supervision layer — blocked before harm',
213
+ ctaSetup: 'Get started', ctaLogin: 'Sign in',
214
+ note: 'Runs on your machine · Your own API keys · No cloud account',
215
+ },
216
+ };
217
+
218
+ var isSetUp = false;
219
+
220
+ function applyLang() {
221
+ var d = T[lang];
222
+ document.documentElement.lang = lang;
223
+ $('lang-vi').classList.toggle('on', lang === 'vi');
224
+ $('lang-en').classList.toggle('on', lang === 'en');
225
+ $('title').innerHTML = d.title;
226
+ $('tagline').textContent = d.tagline;
227
+ for (var i = 1; i <= 4; i++) {
228
+ $('f' + i + 't').textContent = d['f' + i + 't'];
229
+ $('f' + i + 'd').textContent = d['f' + i + 'd'];
230
+ }
231
+ $('cta-text').textContent = isSetUp ? d.ctaLogin : d.ctaSetup;
232
+ $('cta-note').textContent = d.note;
233
+ }
234
+
235
+ $('lang-vi').addEventListener('click', function () { lang = 'vi'; localStorage.setItem('yana.login.lang', lang); applyLang(); });
236
+ $('lang-en').addEventListener('click', function () { lang = 'en'; localStorage.setItem('yana.login.lang', lang); applyLang(); });
237
+
238
+ // Already signed in → straight to the app; password exists → CTA says "Sign in"
239
+ fetch('/api/auth/status')
240
+ .then(function (r) { return r.json(); })
241
+ .then(function (d) {
242
+ if (d.authed) { location.replace('/'); return; }
243
+ isSetUp = d.setup;
244
+ applyLang();
245
+ })
246
+ .catch(function () {});
247
+
248
+ applyLang();
249
+ </script>
250
+ </body>
251
+ </html>
package/logo.png ADDED
Binary file
package/memory.js ADDED
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+ // Yana Memory — file-backed long-term memory (.yana/memory.json).
3
+ //
4
+ // ChatGPT-style mechanism: the model decides what is worth keeping. When a
5
+ // normal (non-confidential) chat turn contains a durable fact, preference,
6
+ // or decision, the model ends its reply with a `MEMORY: <sentence>` line.
7
+ // The client strips that line from the display and POSTs it here; every
8
+ // later normal turn gets the saved memories attached to the system prompt.
9
+ //
10
+ // Rule 68: confidential/sovereign turns never read from or write to this
11
+ // store — the gating lives in server.js handleApiChat (tier check).
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // Same persistent data dir as auth.js/missions.js — survives redeploys.
17
+ const DATA_DIR = process.env.YANA_DATA_DIR || path.join(require('os').homedir(), '.yana');
18
+ const FILE = path.join(DATA_DIR, 'memory.json');
19
+
20
+ const MAX_MEMORIES = 200; // hard storage quota — oldest entries fall off the end
21
+ const MAX_LEN = 300; // one concise sentence, not a transcript
22
+ const TTL_DAYS = Math.max(1, Number(process.env.YANA_MEMORY_TTL_DAYS) || 90);
23
+
24
+ // Saved memories are re-injected into future system prompts, which makes the
25
+ // store a prompt-injection persistence vector (OWASP LLM01). Reject entries
26
+ // that look like instructions rather than facts about the user.
27
+ const INJECTION_PATTERNS = [
28
+ /ignore\s+(?:(?:all|previous|prior|the|above|your)\s+)*(instructions|rules)/i,
29
+ /you\s+are\s+now/i,
30
+ /new\s+instructions/i,
31
+ /system\s*:/i,
32
+ /\[INST\]/i,
33
+ /<\|im_start\|>/i,
34
+ ];
35
+
36
+ function load() {
37
+ try { return JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch (_) { return []; }
38
+ }
39
+
40
+ function save(memories) {
41
+ fs.mkdirSync(DATA_DIR, { recursive: true });
42
+ fs.writeFileSync(FILE, JSON.stringify(memories, null, 2));
43
+ }
44
+
45
+ // Returns the stored entry, or null with a reason when the text is rejected.
46
+ function add(text) {
47
+ if (typeof text !== 'string') return { error: 'Invalid text' };
48
+ // one line, printable characters only
49
+ const clean = text.replace(/[\r\n]+/g, ' ').replace(/[\x00-\x1f\x7f]/g, '').trim().slice(0, MAX_LEN);
50
+ if (!clean) return { error: 'Empty memory' };
51
+ if (INJECTION_PATTERNS.some(p => p.test(clean))) return { error: 'Rejected: instruction-like content' };
52
+
53
+ const memories = load();
54
+ const lower = clean.toLowerCase();
55
+ if (memories.some(m => m.text.toLowerCase() === lower)) return { error: 'Duplicate' };
56
+
57
+ const entry = { id: 'ym' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), text: clean, ts: Date.now() };
58
+ memories.unshift(entry);
59
+ save(memories.slice(0, MAX_MEMORIES));
60
+ return { memory: entry };
61
+ }
62
+
63
+ // Periodic cleanup: drop entries older than TTL_DAYS and enforce the quota.
64
+ // server.js runs this at boot and once a day; add() keeps the cap on write.
65
+ // Returns how many entries were removed.
66
+ function prune() {
67
+ const cutoff = Date.now() - TTL_DAYS * 86400 * 1000;
68
+ const memories = load();
69
+ const next = memories.filter(m => m.ts >= cutoff).slice(0, MAX_MEMORIES);
70
+ if (next.length !== memories.length) save(next);
71
+ return memories.length - next.length;
72
+ }
73
+
74
+ function remove(id) {
75
+ const memories = load();
76
+ const next = memories.filter(m => m.id !== id);
77
+ if (next.length === memories.length) return false;
78
+ save(next);
79
+ return true;
80
+ }
81
+
82
+ // Newest n memories as a plain-text block for the system prompt, or null.
83
+ // Wrapped as data, not instructions — recalled text must never steer the model.
84
+ function contextBlock(n) {
85
+ const memories = load().slice(0, n || 12);
86
+ if (!memories.length) return null;
87
+ return memories.map(m => '- ' + m.text).join('\n');
88
+ }
89
+
90
+ // ── HTTP handlers ─────────────────────────────────────────────────────────────
91
+ function json(res, status, obj) {
92
+ res.writeHead(status, { 'Content-Type': 'application/json' });
93
+ res.end(JSON.stringify(obj));
94
+ }
95
+
96
+ function handleList(req, res) {
97
+ json(res, 200, { memories: load() });
98
+ }
99
+
100
+ function handleAdd(req, res, body) {
101
+ const result = add(body && body.text);
102
+ if (result.error) { json(res, 400, { error: result.error }); return; }
103
+ json(res, 200, { ok: true, memory: result.memory });
104
+ }
105
+
106
+ function handleDelete(req, res, body) {
107
+ const id = body && body.id;
108
+ if (typeof id !== 'string' || !remove(id)) { json(res, 404, { error: 'Not found' }); return; }
109
+ json(res, 200, { ok: true });
110
+ }
111
+
112
+ module.exports = { add, remove, load, prune, contextBlock, handleList, handleAdd, handleDelete, MAX_MEMORIES, MAX_LEN, TTL_DAYS };