@whykusanagi/corrupted-theme 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,1089 @@
1
+ /**
2
+ * Celeste AI Custom Agent
3
+ * A custom implementation that integrates with context schemas
4
+ * Provides contextual assistance based on the current page
5
+ * Based on union raid implementation
6
+ */
7
+
8
+ class CelesteAgent {
9
+ constructor(config = {}) {
10
+ // Agent configuration
11
+ // With backend proxy pattern (secure mode): credentials NOT needed in browser
12
+ // Widget calls /api/chat endpoint, backend proxy handles authentication
13
+ // Browser never sees API key - it stays on server
14
+
15
+ this.agentKey = config.agentKey || window.CELESTE_AGENT_KEY;
16
+ this.agentId = config.agentId || window.CELESTE_AGENT_ID;
17
+ this.agentBaseUrl = config.agentBaseUrl || window.CELESTE_AGENT_BASE_URL;
18
+
19
+ // Proxy URL configuration (for local development)
20
+ // Defaults to localhost:5000, but can be overridden via CELESTE_PROXY_URL
21
+ // or detected from current page origin (for Docker port mapping scenarios)
22
+ this.proxyUrl = config.proxyUrl || window.CELESTE_PROXY_URL || this.detectProxyUrl();
23
+
24
+ // Note: With backend proxy, credentials are optional in browser
25
+ // They're only used if making direct API calls (legacy mode)
26
+ // The /api/chat endpoint will validate credentials server-side
27
+
28
+
29
+ this.isInitialized = false;
30
+ this.isOpen = false;
31
+ this.currentContext = null;
32
+ this.conversationHistory = [];
33
+ this.sessionId = this.generateSessionId();
34
+ this.contextSchemas = null;
35
+ this.celesteEssence = null;
36
+ this.routingRules = null;
37
+
38
+ // UI Elements
39
+ this.chatContainer = null;
40
+ this.chatButton = null;
41
+ this.chatWindow = null;
42
+ this.messageContainer = null;
43
+ this.inputField = null;
44
+ this.sendButton = null;
45
+ }
46
+
47
+ /**
48
+ * Static method: Check if Celeste is properly configured
49
+ * Call this to verify environment variables are set before initializing
50
+ * @returns {Object} Configuration status with details
51
+ */
52
+ static checkConfiguration() {
53
+ const status = {
54
+ hasKey: !!window.CELESTE_AGENT_KEY,
55
+ hasId: !!window.CELESTE_AGENT_ID,
56
+ hasUrl: !!window.CELESTE_AGENT_BASE_URL,
57
+ isConfigured: !!(window.CELESTE_AGENT_KEY && window.CELESTE_AGENT_ID && window.CELESTE_AGENT_BASE_URL),
58
+ missing: []
59
+ };
60
+
61
+ if (!window.CELESTE_AGENT_KEY) status.missing.push('CELESTE_AGENT_KEY');
62
+ if (!window.CELESTE_AGENT_ID) status.missing.push('CELESTE_AGENT_ID');
63
+ if (!window.CELESTE_AGENT_BASE_URL) status.missing.push('CELESTE_AGENT_BASE_URL');
64
+
65
+ return status;
66
+ }
67
+
68
+ /**
69
+ * Initialize Celeste Agent
70
+ */
71
+ async initialize() {
72
+ if (this.isInitialized) return;
73
+
74
+ try {
75
+ // Load context schemas
76
+ await this.loadContextSchemas();
77
+
78
+ // Get current page context
79
+ this.currentContext = await this.getPageContext();
80
+
81
+ // Create UI elements
82
+ this.createUI();
83
+
84
+ // Set initial prompt from schemas
85
+ this.loadContextualPrompt();
86
+
87
+ this.isInitialized = true;
88
+ console.log('🎭 Celeste Agent initialized with context:', this.currentContext);
89
+ } catch (error) {
90
+ console.error('❌ Failed to initialize Celeste Agent:', error);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Load context schemas and configuration from local sources
96
+ */
97
+ async loadContextSchemas() {
98
+ try {
99
+ // Load local context schemas
100
+ const schemaResponse = await fetch('/static/data/celeste-context-schemas.json');
101
+ if (schemaResponse.ok) {
102
+ this.contextSchemas = await schemaResponse.json();
103
+ console.log('📋 Context schemas loaded');
104
+ } else {
105
+ this.contextSchemas = null;
106
+ }
107
+
108
+ // Load local capabilities
109
+ const capResponse = await fetch('/static/data/celeste-capabilities.json');
110
+ if (capResponse.ok) {
111
+ this.capabilities = await capResponse.json();
112
+ console.log('🎭 Celeste capabilities loaded');
113
+ } else {
114
+ this.capabilities = null;
115
+ }
116
+ } catch (error) {
117
+ console.warn('⚠️ Error loading data files:', error);
118
+ this.contextSchemas = null;
119
+ this.capabilities = null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Get contextual information about the current page
125
+ */
126
+ async getPageContext() {
127
+ const path = window.location.pathname;
128
+ const context = {
129
+ page: this.getPageName(path),
130
+ path: path,
131
+ timestamp: new Date().toISOString(),
132
+ userAgent: navigator.userAgent,
133
+ viewport: {
134
+ width: window.innerWidth,
135
+ height: window.innerHeight
136
+ }
137
+ };
138
+
139
+ // Add page-specific context
140
+ switch (context.page) {
141
+ case 'home':
142
+ context.data = await this.getHomeContext();
143
+ break;
144
+ case 'art':
145
+ context.data = await this.getArtContext();
146
+ break;
147
+ case 'celeste':
148
+ context.data = await this.getCelesteContext();
149
+ break;
150
+ case 'references':
151
+ context.data = await this.getReferencesContext();
152
+ break;
153
+ case 'doujin':
154
+ context.data = await this.getDoujinContext();
155
+ break;
156
+ case 'links':
157
+ context.data = await this.getLinksContext();
158
+ break;
159
+ case 'tools':
160
+ context.data = await this.getToolsContext();
161
+ break;
162
+ case 'privacy':
163
+ context.data = await this.getPrivacyContext();
164
+ break;
165
+ default:
166
+ context.data = await this.getGeneralContext();
167
+ }
168
+
169
+ return context;
170
+ }
171
+
172
+ /**
173
+ * Get page name from path
174
+ */
175
+ getPageName(path) {
176
+ if (path === '/' || path === '/index.html') return 'home';
177
+ if (path.includes('art')) return 'art';
178
+ if (path.includes('celeste')) return 'celeste';
179
+ if (path.includes('reference')) return 'references';
180
+ if (path.includes('kirara') || path.includes('bastard') || path.includes('doujin')) return 'doujin';
181
+ if (path.includes('link')) return 'links';
182
+ if (path.includes('tool') || path.includes('calc') || path.includes('countdown')) return 'tools';
183
+ if (path.includes('privacy')) return 'privacy';
184
+ return 'unknown';
185
+ }
186
+
187
+ /**
188
+ * Get home-specific context
189
+ */
190
+ async getHomeContext() {
191
+ return {
192
+ purpose: 'Portfolio landing page and introduction',
193
+ features: [
194
+ 'Portfolio overview',
195
+ 'Navigation to all sections',
196
+ 'About whykusanagi',
197
+ 'Celeste introduction'
198
+ ],
199
+ dataTypes: ['portfolio_overview', 'section_navigation'],
200
+ suggestions: [
201
+ 'What can I find on this site?',
202
+ 'Who is whykusanagi?',
203
+ 'Show me your art',
204
+ 'What tools are available?'
205
+ ]
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Get art-specific context
211
+ */
212
+ async getArtContext() {
213
+ return {
214
+ purpose: 'Digital art showcase and gallery',
215
+ features: [
216
+ 'Art gallery showcase',
217
+ 'Artistic style discussion',
218
+ 'Design techniques',
219
+ 'Character design exploration'
220
+ ],
221
+ dataTypes: ['art_gallery', 'design_analysis', 'artistic_style'],
222
+ suggestions: [
223
+ 'What is your artistic style?',
224
+ 'How do you approach character design?',
225
+ 'Tell me about this piece',
226
+ 'What software do you use?'
227
+ ]
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Get Celeste character context
233
+ */
234
+ async getCelesteContext() {
235
+ return {
236
+ purpose: 'Character page about Celeste AI',
237
+ features: [
238
+ 'Character information',
239
+ 'Personality exploration',
240
+ 'Background story',
241
+ 'Character showcase'
242
+ ],
243
+ dataTypes: ['character_info', 'personality', 'background'],
244
+ suggestions: [
245
+ 'Who are you really?',
246
+ 'What is your personality like?',
247
+ 'Are you actually an AI?',
248
+ 'Tell me something weird about yourself'
249
+ ]
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Get references context
255
+ */
256
+ async getReferencesContext() {
257
+ return {
258
+ purpose: 'Character design references and anatomy',
259
+ features: [
260
+ 'Character references',
261
+ 'Design inspiration',
262
+ 'Anatomy studies',
263
+ 'Color palettes'
264
+ ],
265
+ dataTypes: ['character_references', 'design_inspiration', 'anatomy'],
266
+ suggestions: [
267
+ 'Explain the character design',
268
+ 'What is the design inspiration?',
269
+ 'How did you choose colors?',
270
+ 'Tell me about the anatomy'
271
+ ]
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Get doujin context
277
+ */
278
+ async getDoujinContext() {
279
+ return {
280
+ purpose: 'Manga and doujinshi projects',
281
+ features: [
282
+ 'Manga project information',
283
+ 'Story summaries',
284
+ 'Character discussions',
285
+ 'Publication information'
286
+ ],
287
+ dataTypes: ['manga_projects', 'storylines', 'characters'],
288
+ suggestions: [
289
+ 'Tell me about Fall of Kirara',
290
+ 'What is the plot?',
291
+ 'Where can I read it?',
292
+ 'When is it available?'
293
+ ]
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Get links context
299
+ */
300
+ async getLinksContext() {
301
+ return {
302
+ purpose: 'Social media and external links',
303
+ features: [
304
+ 'Social media links',
305
+ 'Platform connections',
306
+ 'External resources',
307
+ 'Contact information'
308
+ ],
309
+ dataTypes: ['social_links', 'platforms', 'connections'],
310
+ suggestions: [
311
+ 'Where can I find whykusanagi?',
312
+ 'What is your Twitch?',
313
+ 'Do you have a Discord?',
314
+ 'Where is your shop?'
315
+ ]
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Get tools context
321
+ */
322
+ async getToolsContext() {
323
+ return {
324
+ purpose: 'Interactive tools and utilities',
325
+ features: [
326
+ 'Countdown timers',
327
+ 'Utility calculators',
328
+ 'Resource downloads',
329
+ 'Interactive tools'
330
+ ],
331
+ dataTypes: ['tools', 'utilities', 'resources'],
332
+ suggestions: [
333
+ 'How do I use this tool?',
334
+ 'What is the calculator for?',
335
+ 'Can you help me with the countdown?',
336
+ 'How does this work?'
337
+ ]
338
+ };
339
+ }
340
+
341
+ /**
342
+ * Get privacy context
343
+ */
344
+ async getPrivacyContext() {
345
+ return {
346
+ purpose: 'Privacy policy and legal information',
347
+ features: [
348
+ 'Privacy policy',
349
+ 'Legal information',
350
+ 'Terms of service',
351
+ 'Data practices'
352
+ ],
353
+ dataTypes: ['legal', 'privacy', 'terms'],
354
+ suggestions: [
355
+ 'What is your privacy policy?',
356
+ 'How is my data used?',
357
+ 'What are your terms?',
358
+ 'Do you track users?'
359
+ ]
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Get general context for unknown pages
365
+ */
366
+ async getGeneralContext() {
367
+ return {
368
+ purpose: 'Portfolio and personal website',
369
+ features: [
370
+ 'Art and creative work',
371
+ 'Portfolio showcase',
372
+ 'Character and tools',
373
+ 'Social connections'
374
+ ],
375
+ dataTypes: ['general_portfolio'],
376
+ suggestions: [
377
+ 'Tell me about your work',
378
+ 'Show me your art',
379
+ 'What tools do you offer?'
380
+ ]
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Load contextual prompt from schemas and capabilities
386
+ */
387
+ loadContextualPrompt() {
388
+ try {
389
+ if (!this.contextSchemas) {
390
+ this.contextualPrompt = this.getDefaultPrompt();
391
+ return;
392
+ }
393
+
394
+ const pageType = this.currentContext?.page || 'unknown';
395
+ const pageConfig = this.contextSchemas.page_types?.[pageType];
396
+
397
+ if (pageConfig) {
398
+ // Start with page-specific prompt
399
+ let prompt = pageConfig.system_prompt;
400
+
401
+ // Add capability information if available
402
+ if (this.capabilities) {
403
+ const pageCapabilities = this.capabilities.page_specific_capabilities?.[pageType];
404
+ if (pageCapabilities) {
405
+ prompt += `\n\nOn this page (${pageType}), you can help with: ${pageCapabilities.can_help_with.join(', ')}`;
406
+ }
407
+ // Add general capabilities reference
408
+ prompt += `\n\nYour core capabilities include: ${Object.keys(this.capabilities.core_capabilities).join(', ')}`;
409
+ prompt += `\n\nWhen users ask "what can you do" or "what are your abilities", refer to these capabilities and the current page context.`;
410
+ }
411
+
412
+ this.contextualPrompt = prompt;
413
+ this.suggestedQueries = pageConfig.suggested_queries;
414
+ console.log('🎭 Context loaded from schemas for page:', pageType);
415
+ } else {
416
+ this.contextualPrompt = this.getDefaultPrompt();
417
+ }
418
+ } catch (error) {
419
+ console.error('❌ Error loading context prompt:', error);
420
+ this.contextualPrompt = this.getDefaultPrompt();
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Get default prompt if schemas fail
426
+ */
427
+ getDefaultPrompt() {
428
+ const pageType = this.currentContext?.page || 'unknown';
429
+ const prompts = {
430
+ 'home': "Hello! I'm Celeste, your AI assistant. I can help you explore the art gallery, learn about my character, discover tools, or find social media links. What would you like to know?",
431
+ 'art': "Hello! I'm Celeste, here to help you explore the art gallery. I can discuss artistic styles, techniques, and help you discover pieces that interest you. What would you like to know?",
432
+ 'celeste': "Hello! I'm Celeste AI, your chaotic Onee-san assistant. Ask me anything about who I am, my personality, or what makes me unique. What's on your mind?",
433
+ 'references': "Hello! I'm here to explain character designs and artistic references. I can discuss anatomy, color theory, and design inspiration. What would you like to know?",
434
+ 'doujin': "Hello! I'm Celeste, guiding you through manga and doujinshi projects. I can summarize stories, discuss characters, and provide information on where to read. What interests you?",
435
+ 'links': "Hello! I'm here to help you find and connect on social media. I know all the platforms where whykusanagi can be found. What platform are you looking for?",
436
+ 'tools': "Hello! I'm ready to help you understand and use available tools. I can explain how calculators work and help you get the most out of them. What do you need?",
437
+ 'privacy': "Hello! I'm here to clarify privacy policies and legal information. I can explain data practices and terms of service. What do you need to know?",
438
+ 'default': "Hello, I am CelesteAI. Is there something I can help you with or are you just gonna stare?"
439
+ };
440
+ return prompts[pageType] || prompts.default;
441
+ }
442
+
443
+ /**
444
+ * Create UI elements
445
+ */
446
+ createUI() {
447
+ // Create chat button
448
+ this.chatButton = document.createElement('div');
449
+ this.chatButton.className = 'celeste-chat-button';
450
+ const avatarUrl = this.getAssetUrl('https://s3.whykusanagi.xyz/Celeste_Vel_Icon.png');
451
+ this.chatButton.innerHTML = `
452
+ <div class="celeste-button-content">
453
+ <img src="${avatarUrl}" alt="Celeste AI" class="celeste-avatar" onerror="this.style.display='none'; this.parentElement.style.background='linear-gradient(135deg, #d94f90 0%, #b61b70 100%)';">
454
+ <span class="celeste-button-text">Chat with Celeste</span>
455
+ </div>
456
+ `;
457
+ this.chatButton.addEventListener('click', () => this.toggleChat());
458
+
459
+ // Create chat window
460
+ this.chatWindow = document.createElement('div');
461
+ this.chatWindow.className = 'celeste-chat-window';
462
+ const headerAvatarUrl = this.getAssetUrl('https://s3.whykusanagi.xyz/Celeste_Vel_Icon.png');
463
+ this.chatWindow.innerHTML = `
464
+ <div class="celeste-chat-header">
465
+ <div class="celeste-header-content">
466
+ <img src="${headerAvatarUrl}" alt="Celeste AI" class="celeste-header-avatar" onerror="this.style.display='none';">
467
+ <div class="celeste-header-info">
468
+ <h3><strong>CelesteAI</strong></h3>
469
+ <p><strong>Your helpful Onee-san assistant</strong></p>
470
+ </div>
471
+ </div>
472
+ <button class="celeste-close-btn">&times;</button>
473
+ </div>
474
+ <div class="celeste-chat-messages"></div>
475
+ <div class="celeste-chat-input">
476
+ <input type="text" placeholder="Ask Celeste anything..." class="celeste-input-field">
477
+ <button class="celeste-send-btn">Send</button>
478
+ </div>
479
+ `;
480
+
481
+ // Add event listeners
482
+ this.chatWindow.querySelector('.celeste-close-btn').addEventListener('click', () => this.closeChat());
483
+ this.chatWindow.querySelector('.celeste-send-btn').addEventListener('click', () => this.sendMessage());
484
+ this.chatWindow.querySelector('.celeste-input-field').addEventListener('keypress', (e) => {
485
+ if (e.key === 'Enter') this.sendMessage();
486
+ });
487
+
488
+ // Add to page
489
+ document.body.appendChild(this.chatButton);
490
+ document.body.appendChild(this.chatWindow);
491
+
492
+ // Add CSS
493
+ this.addStyles();
494
+ }
495
+
496
+ /**
497
+ * Add CSS styles
498
+ */
499
+ addStyles() {
500
+ const style = document.createElement('style');
501
+ style.textContent = `
502
+ .celeste-chat-button {
503
+ position: fixed;
504
+ bottom: 20px;
505
+ right: 20px;
506
+ width: 60px;
507
+ height: 60px;
508
+ background: linear-gradient(135deg, #0a0a0a 0%, #2d1b4e 50%, #d94f90 100%);
509
+ border-radius: 50%;
510
+ cursor: pointer;
511
+ box-shadow: 0 4px 20px rgba(217, 79, 144, 0.4);
512
+ transition: all 0.3s ease;
513
+ z-index: 1000;
514
+ display: flex;
515
+ align-items: center;
516
+ justify-content: center;
517
+ border: 2px solid rgba(217, 79, 144, 0.3);
518
+ }
519
+
520
+ .celeste-chat-button:hover {
521
+ transform: scale(1.1);
522
+ box-shadow: 0 6px 25px rgba(217, 79, 144, 0.6);
523
+ background: linear-gradient(135deg, #2d1b4e 0%, #0a0a0a 50%, #ff69b4 100%);
524
+ }
525
+
526
+ .celeste-button-content {
527
+ display: flex;
528
+ flex-direction: column;
529
+ align-items: center;
530
+ color: white;
531
+ text-align: center;
532
+ }
533
+
534
+ .celeste-avatar {
535
+ width: 30px;
536
+ height: 30px;
537
+ border-radius: 50%;
538
+ object-fit: cover;
539
+ display: block;
540
+ background: rgba(217, 79, 144, 0.2);
541
+ border: 1px solid rgba(217, 79, 144, 0.4);
542
+ }
543
+
544
+ .celeste-button-text {
545
+ font-size: 8px;
546
+ font-weight: bold;
547
+ margin-top: 2px;
548
+ }
549
+
550
+ .celeste-chat-window {
551
+ position: fixed;
552
+ bottom: 90px;
553
+ right: 20px;
554
+ width: 350px;
555
+ height: 500px;
556
+ background: linear-gradient(145deg, #0a0a0a 0%, #1a0f2e 100%);
557
+ border-radius: 15px;
558
+ box-shadow: 0 10px 30px rgba(217, 79, 144, 0.3);
559
+ display: none;
560
+ flex-direction: column;
561
+ z-index: 1001;
562
+ overflow: hidden;
563
+ border: 1px solid rgba(217, 79, 144, 0.2);
564
+ }
565
+
566
+ .celeste-chat-window.open {
567
+ display: flex;
568
+ }
569
+
570
+ .celeste-chat-header {
571
+ background: linear-gradient(135deg, #d94f90 0%, #b61b70 50%, #8b1a59 100%);
572
+ color: white;
573
+ padding: 15px;
574
+ display: flex;
575
+ justify-content: space-between;
576
+ align-items: center;
577
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
578
+ }
579
+
580
+ .celeste-header-content {
581
+ display: flex;
582
+ align-items: center;
583
+ }
584
+
585
+ .celeste-header-avatar {
586
+ width: 40px;
587
+ height: 40px;
588
+ border-radius: 50%;
589
+ object-fit: cover;
590
+ margin-right: 10px;
591
+ border: 2px solid rgba(255, 255, 255, 0.3);
592
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
593
+ }
594
+
595
+ .celeste-header-info h3 {
596
+ margin: 0;
597
+ font-size: 16px;
598
+ }
599
+
600
+ .celeste-header-info p {
601
+ margin: 0;
602
+ font-size: 12px;
603
+ opacity: 0.8;
604
+ }
605
+
606
+ .celeste-close-btn {
607
+ background: none;
608
+ border: none;
609
+ color: white;
610
+ font-size: 24px;
611
+ cursor: pointer;
612
+ padding: 0;
613
+ width: 30px;
614
+ height: 30px;
615
+ display: flex;
616
+ align-items: center;
617
+ justify-content: center;
618
+ }
619
+
620
+ .celeste-chat-messages {
621
+ flex: 1;
622
+ padding: 15px;
623
+ overflow-y: auto;
624
+ background: linear-gradient(145deg, #1a0f2e 0%, #0a0a0a 100%);
625
+ }
626
+
627
+ .celeste-message {
628
+ margin-bottom: 15px;
629
+ display: flex;
630
+ align-items: flex-start;
631
+ }
632
+
633
+ .celeste-message.user {
634
+ justify-content: flex-end;
635
+ }
636
+
637
+ .celeste-message-bubble {
638
+ max-width: 80%;
639
+ padding: 10px 15px;
640
+ border-radius: 18px;
641
+ word-wrap: break-word;
642
+ }
643
+
644
+ .celeste-message.celeste .celeste-message-bubble {
645
+ background: linear-gradient(135deg, #2d1b4e 0%, #1a0f2e 100%);
646
+ color: #fdf3f8;
647
+ border-bottom-left-radius: 5px;
648
+ border: 1px solid rgba(217, 79, 144, 0.2);
649
+ }
650
+
651
+ .celeste-message.user .celeste-message-bubble {
652
+ background: linear-gradient(135deg, #d94f90 0%, #b61b70 100%);
653
+ color: white;
654
+ border-bottom-right-radius: 5px;
655
+ border: 1px solid rgba(255, 255, 255, 0.1);
656
+ }
657
+
658
+ .celeste-chat-input {
659
+ padding: 15px;
660
+ background: linear-gradient(145deg, #0a0a0a 0%, #1a0f2e 100%);
661
+ border-top: 1px solid rgba(217, 79, 144, 0.2);
662
+ display: flex;
663
+ gap: 10px;
664
+ }
665
+
666
+ .celeste-input-field {
667
+ flex: 1;
668
+ padding: 10px 15px;
669
+ border: 1px solid rgba(217, 79, 144, 0.3);
670
+ border-radius: 25px;
671
+ outline: none;
672
+ font-size: 14px;
673
+ background: rgba(45, 27, 78, 0.5);
674
+ color: white;
675
+ backdrop-filter: blur(10px);
676
+ }
677
+
678
+ .celeste-input-field:focus {
679
+ border-color: #d94f90;
680
+ background: rgba(217, 79, 144, 0.2);
681
+ }
682
+
683
+ .celeste-input-field::placeholder {
684
+ color: rgba(255, 255, 255, 0.5);
685
+ }
686
+
687
+ .celeste-send-btn {
688
+ background: linear-gradient(135deg, #d94f90 0%, #b61b70 100%);
689
+ color: white;
690
+ border: none;
691
+ border-radius: 25px;
692
+ padding: 10px 20px;
693
+ cursor: pointer;
694
+ font-weight: bold;
695
+ transition: all 0.3s ease;
696
+ border: 1px solid rgba(255, 255, 255, 0.2);
697
+ }
698
+
699
+ .celeste-send-btn:hover {
700
+ transform: scale(1.05);
701
+ }
702
+
703
+ .celeste-typing {
704
+ display: flex;
705
+ align-items: center;
706
+ gap: 5px;
707
+ color: rgba(255, 255, 255, 0.7);
708
+ font-style: italic;
709
+ }
710
+
711
+ .celeste-typing-dots {
712
+ display: flex;
713
+ gap: 3px;
714
+ }
715
+
716
+ .celeste-typing-dot {
717
+ width: 6px;
718
+ height: 6px;
719
+ background: #d94f90;
720
+ border-radius: 50%;
721
+ animation: celeste-typing 1.4s infinite ease-in-out;
722
+ }
723
+
724
+ .celeste-typing-dot:nth-child(1) { animation-delay: -0.32s; }
725
+ .celeste-typing-dot:nth-child(2) { animation-delay: -0.16s; }
726
+
727
+ @keyframes celeste-typing {
728
+ 0%, 80%, 100% { transform: scale(0); }
729
+ 40% { transform: scale(1); }
730
+ }
731
+ `;
732
+ document.head.appendChild(style);
733
+ }
734
+
735
+ /**
736
+ * Toggle chat window
737
+ */
738
+ toggleChat() {
739
+ this.isOpen = !this.isOpen;
740
+ if (this.isOpen) {
741
+ this.chatWindow.classList.add('open');
742
+ this.chatButton.style.display = 'none';
743
+ this.showWelcomeMessage();
744
+ } else {
745
+ this.chatWindow.classList.remove('open');
746
+ this.chatButton.style.display = 'flex';
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Close chat window
752
+ */
753
+ closeChat() {
754
+ this.isOpen = false;
755
+ this.chatWindow.classList.remove('open');
756
+ this.chatButton.style.display = 'flex';
757
+ }
758
+
759
+ /**
760
+ * Show welcome message
761
+ */
762
+ showWelcomeMessage() {
763
+ if (this.conversationHistory.length === 0) {
764
+ const welcomeMessage = this.getWelcomeMessage();
765
+ this.addMessage('celeste', welcomeMessage);
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Get user-friendly welcome message
771
+ */
772
+ getWelcomeMessage() {
773
+ const pageType = this.currentContext?.page || 'unknown';
774
+ const welcomeMessages = {
775
+ 'home': "Hello there! I'm Celeste, your helpful assistant for this portfolio! I can help you explore the art, learn about whykusanagi, and discover all the amazing projects here. What would you like to know?",
776
+ 'art': "Hi! I'm Celeste, here to help you explore the art gallery! I can discuss artistic styles, techniques, and help you discover pieces that interest you. What would you like to know?",
777
+ 'celeste': "Hello! I'm Celeste AI, your chaotic Onee-san assistant. Ask me anything about who I am, my personality, or what makes me unique. What's on your mind?",
778
+ 'references': "Hello! I'm here to explain character designs and references. I can discuss anatomy, color theory, and design inspiration. What would you like to know?",
779
+ 'doujin': "Hello! I'm Celeste, guiding you through manga projects. I can summarize stories, discuss characters, and provide information on where to read. What interests you?",
780
+ 'links': "Hello! I'm here to help you find and connect with whykusanagi on social media! I know all the platforms where whykusanagi can be found. What platform are you looking for?",
781
+ 'tools': "Hello! I'm ready to help you understand and use available tools. I can explain how various utilities work. What do you need?",
782
+ 'privacy': "Hello! I'm here to clarify privacy policies and legal information. I can explain data practices and terms of service. What do you need to know?",
783
+ 'default': "Hello! I'm Celeste, your helpful assistant! How can I help you today?"
784
+ };
785
+ return welcomeMessages[pageType] || welcomeMessages.default;
786
+ }
787
+
788
+ /**
789
+ * Add message to chat
790
+ */
791
+ addMessage(sender, text) {
792
+ const messageContainer = this.chatWindow.querySelector('.celeste-chat-messages');
793
+ const messageDiv = document.createElement('div');
794
+ messageDiv.className = `celeste-message ${sender}`;
795
+ messageDiv.innerHTML = `
796
+ <div class="celeste-message-bubble">${text}</div>
797
+ `;
798
+ messageContainer.appendChild(messageDiv);
799
+ messageContainer.scrollTop = messageContainer.scrollHeight;
800
+
801
+ // Store in conversation history
802
+ this.conversationHistory.push({ sender, text, timestamp: new Date() });
803
+ }
804
+
805
+ /**
806
+ * Show typing indicator
807
+ */
808
+ showTyping() {
809
+ const messageContainer = this.chatWindow.querySelector('.celeste-chat-messages');
810
+ const typingDiv = document.createElement('div');
811
+ typingDiv.className = 'celeste-message celeste';
812
+ typingDiv.innerHTML = `
813
+ <div class="celeste-message-bubble">
814
+ <div class="celeste-typing">
815
+ Celeste is typing
816
+ <div class="celeste-typing-dots">
817
+ <div class="celeste-typing-dot"></div>
818
+ <div class="celeste-typing-dot"></div>
819
+ <div class="celeste-typing-dot"></div>
820
+ </div>
821
+ </div>
822
+ </div>
823
+ `;
824
+ messageContainer.appendChild(typingDiv);
825
+ messageContainer.scrollTop = messageContainer.scrollHeight;
826
+ return typingDiv;
827
+ }
828
+
829
+ /**
830
+ * Remove typing indicator
831
+ */
832
+ removeTyping(typingDiv) {
833
+ if (typingDiv && typingDiv.parentNode) {
834
+ typingDiv.parentNode.removeChild(typingDiv);
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Send message
840
+ */
841
+ async sendMessage() {
842
+ const inputField = this.chatWindow.querySelector('.celeste-input-field');
843
+ const message = inputField.value.trim();
844
+
845
+ if (!message) return;
846
+
847
+ // Add user message
848
+ this.addMessage('user', message);
849
+ inputField.value = '';
850
+
851
+ // Show typing indicator
852
+ const typingDiv = this.showTyping();
853
+
854
+ try {
855
+ // Send to Celeste AI
856
+ const response = await this.sendToCeleste(message);
857
+ this.removeTyping(typingDiv);
858
+ this.addMessage('celeste', response);
859
+ } catch (error) {
860
+ this.removeTyping(typingDiv);
861
+ this.addMessage('celeste', 'Sorry, I encountered an error. Please try again.');
862
+ console.error('❌ Error sending message to Celeste:', error);
863
+ }
864
+ }
865
+
866
+ /**
867
+ * Send message to Celeste AI with timeout and error handling
868
+ * Uses secure backend proxy instead of direct API calls
869
+ *
870
+ * SECURITY: Browser never sees the API credential. Backend handles authentication.
871
+ */
872
+ async sendToCeleste(message) {
873
+ // Build system prompt with capability context
874
+ const systemPrompt = this.buildEnhancedSystemPrompt();
875
+
876
+ // Get recent history for context
877
+ const recentHistory = this.conversationHistory.slice(-5);
878
+
879
+ // Create abort controller for timeout (45 second timeout)
880
+ const controller = new AbortController();
881
+ const timeoutId = setTimeout(() => controller.abort(), 45000);
882
+
883
+ try {
884
+ // Call backend proxy endpoint (configurable via this.proxyUrl)
885
+ // Backend proxy handles authentication with CELESTE_AGENT_KEY
886
+ // Browser never needs to know the credential
887
+ const proxyEndpoint = `${this.proxyUrl}/api/chat`;
888
+ const response = await fetch(proxyEndpoint, {
889
+ method: 'POST',
890
+ headers: {
891
+ 'Content-Type': 'application/json'
892
+ // ✅ NO Authorization header needed - backend is authenticated
893
+ },
894
+ body: JSON.stringify({
895
+ message: message,
896
+ system_prompt: systemPrompt,
897
+ history: recentHistory
898
+ }),
899
+ signal: controller.signal
900
+ });
901
+
902
+ clearTimeout(timeoutId);
903
+
904
+ if (!response.ok) {
905
+ console.error(`❌ API Error: HTTP ${response.status}`);
906
+
907
+ if (response.status === 401) {
908
+ return 'Authentication error - invalid API key. Check server configuration.';
909
+ } else if (response.status === 503) {
910
+ return 'The service is currently unavailable. Please try again in a moment.';
911
+ } else {
912
+ return `API error (${response.status}). Please try again.`;
913
+ }
914
+ }
915
+
916
+ const data = await response.json();
917
+
918
+ // Handle error responses from backend
919
+ if (data.error) {
920
+ console.error('❌ Backend error:', data.error);
921
+ return `Service error: ${data.error}`;
922
+ }
923
+
924
+ // Handle successful response from Celeste API
925
+ if (!data.choices || !data.choices[0] || !data.choices[0].message) {
926
+ console.error('❌ Invalid API response structure:', data);
927
+ return 'I received an empty response. Could you rephrase your question?';
928
+ }
929
+
930
+ return data.choices[0].message.content;
931
+
932
+ } catch (error) {
933
+ clearTimeout(timeoutId);
934
+
935
+ if (error.name === 'AbortError') {
936
+ console.error('⏱️ Request timeout after 45 seconds');
937
+ return 'Your request took too long to process. This might be a complex question - try rephrasing it more simply.';
938
+ } else if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
939
+ console.error('❌ Network error:', error);
940
+ return 'Network connection error. Please check your internet and try again.';
941
+ } else {
942
+ console.error('❌ Error communicating with Celeste:', error);
943
+ return 'An unexpected error occurred. Please try again.';
944
+ }
945
+ }
946
+ }
947
+
948
+ /**
949
+ * Build enhanced system prompt with capability context
950
+ */
951
+ buildEnhancedSystemPrompt() {
952
+ const pageType = this.currentContext?.page || 'unknown';
953
+
954
+ // Use celeste_essence.json from celesteCLI if available
955
+ if (this.celesteEssence) {
956
+ let prompt = this.celesteEssence.description || this.celesteEssence.character || '';
957
+
958
+ // Add page context
959
+ prompt += `\n\nCurrent page context: ${pageType}`;
960
+
961
+ // Add routing notice if enabled
962
+ if (this.celesteEssence.routing?.enabled && this.routingRules) {
963
+ prompt += '\n\nNote: NIKKE game queries should be handled with game-specific data. Detect game-related questions and respond with available game information.';
964
+ }
965
+
966
+ return prompt;
967
+ }
968
+
969
+ // Fallback to default prompt
970
+ const basePrompt = this.getDefaultPrompt();
971
+ if (this.capabilities) {
972
+ const capSummary = this.buildCapabilitySummary();
973
+ return `${basePrompt}\n\n[Available Capabilities]\n${capSummary}`;
974
+ }
975
+
976
+ return basePrompt;
977
+ }
978
+
979
+ /**
980
+ * Detect user intent from message using routing rules
981
+ */
982
+ detectIntent(message) {
983
+ if (!this.routingRules) return 'general';
984
+
985
+ const lowerMessage = message.toLowerCase();
986
+ const nikkeKeywords = this.routingRules.nikke_detection?.keywords || [];
987
+ const nikkePatterns = this.routingRules.nikke_detection?.patterns || [];
988
+
989
+ // Check keywords
990
+ for (const keyword of nikkeKeywords) {
991
+ if (lowerMessage.includes(keyword.toLowerCase())) {
992
+ return 'nikke';
993
+ }
994
+ }
995
+
996
+ // Check regex patterns
997
+ for (const pattern of nikkePatterns) {
998
+ try {
999
+ const regex = new RegExp(pattern, 'i');
1000
+ if (regex.test(message)) {
1001
+ return 'nikke';
1002
+ }
1003
+ } catch (e) {
1004
+ console.warn('Invalid regex pattern:', pattern);
1005
+ }
1006
+ }
1007
+
1008
+ return 'general';
1009
+ }
1010
+
1011
+ /**
1012
+ * Build a concise summary of capabilities for the prompt
1013
+ */
1014
+ buildCapabilitySummary() {
1015
+ if (!this.capabilities || !this.capabilities.core_capabilities) {
1016
+ return 'No capabilities loaded.';
1017
+ }
1018
+
1019
+ const caps = this.capabilities.core_capabilities;
1020
+ const summary = Object.keys(caps).slice(0, 4).map(key => {
1021
+ const cap = caps[key];
1022
+ return `- ${cap.description}`;
1023
+ }).join('\n');
1024
+
1025
+ return summary;
1026
+ }
1027
+
1028
+ /**
1029
+ * Get environment-aware asset URL
1030
+ * Uses AssetConfig if available, otherwise returns original URL
1031
+ */
1032
+ getAssetUrl(url) {
1033
+ if (window.AssetConfig && typeof window.AssetConfig.convertUrl === 'function') {
1034
+ return window.AssetConfig.convertUrl(url);
1035
+ }
1036
+ return url;
1037
+ }
1038
+
1039
+ /**
1040
+ * Generate a unique session ID
1041
+ */
1042
+ generateSessionId() {
1043
+ return 'celeste_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1044
+ }
1045
+
1046
+ /**
1047
+ * Detect proxy URL from current page context
1048
+ * Production: uses same domain (Cloudflare Worker handles it)
1049
+ * Local dev: uses port 5001 (Docker mapping)
1050
+ */
1051
+ detectProxyUrl() {
1052
+ const hostname = window.location.hostname;
1053
+
1054
+ // Production: use same domain (Cloudflare Worker handles it)
1055
+ if (hostname.includes('whykusanagi.xyz')) {
1056
+ return window.location.origin; // https://whykusanagi.xyz
1057
+ }
1058
+
1059
+ // Local dev: use port 5001 (Docker maps container:5000 -> host:5001)
1060
+ const currentPort = window.location.port;
1061
+ if (currentPort === '8000' || currentPort === '') {
1062
+ return 'http://localhost:5001';
1063
+ }
1064
+
1065
+ // Default fallback
1066
+ return 'http://localhost:5000';
1067
+ }
1068
+ }
1069
+
1070
+ // Initialize Celeste Agent when DOM is ready
1071
+ document.addEventListener('DOMContentLoaded', () => {
1072
+ // Don't initialize on privacy page
1073
+ if (window.location.pathname.includes('privacy')) {
1074
+ return;
1075
+ }
1076
+
1077
+ const celesteAgent = new CelesteAgent();
1078
+ celesteAgent.initialize();
1079
+
1080
+ // Make it globally available
1081
+ window.CelesteAgent = celesteAgent;
1082
+ });
1083
+
1084
+ // Update context on page navigation
1085
+ window.addEventListener('popstate', () => {
1086
+ if (window.CelesteAgent) {
1087
+ window.CelesteAgent.initialize();
1088
+ }
1089
+ });