S3 buckets (462MB total): - publishing2-images: 113MB (918 objects) - recognition-protocol-state: 75MB (375 objects) - scottkarp-images: 208MB (1104 objects) - scottbot-ml: 5MB - temple-council-knowledge: 61MB (441 objects) Bedrock Knowledge Base: - temple-council-memories (SY6GV37BPN) config + data source - Backed by OpenSearch Serverless collection (temple-council) CloudWatch Lambda logs: recognition-autonomous-wake, myAccountBalance, KnittingMath, APITest, AlexaTest1, TestAPI2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2558 lines
79 KiB
Plaintext
2558 lines
79 KiB
Plaintext
# Conversation: Ellucian Chatbot Welcome Message Bug
|
|
Voice: ren
|
|
Created: 2025-09-02T02:58:39.433515Z
|
|
|
|
## Human
|
|
We're debugging the ellucian chatbot. The welcome message disappears when starting a new conversation. Here's the deployed code.
|
|
|
|
## Ren
|
|
Looking at the code, I can see the issue. When the user clicks "New Conversation", the `createNewSession()` method is called. Let me trace through what happens:
|
|
|
|
1. In `createNewSession()`:
|
|
- It clears the UI with `this.chatMessages.innerHTML = '';`
|
|
- Then it calls `this.addWelcomeMessage()`
|
|
|
|
2. In `addWelcomeMessage()`:
|
|
- It creates the welcome HTML
|
|
- Sets it with `this.chatMessages.innerHTML = welcomeHTML;`
|
|
- Re-assigns the typing indicator reference
|
|
|
|
The issue seems to be that the welcome message is being added, but something might be causing it to disappear or not display properly.
|
|
|
|
Looking more carefully at the code:
|
|
|
|
1. The initial welcome message is in the HTML and displays correctly on first load
|
|
2. When creating a new session, it clears the chat messages and then adds the welcome message back
|
|
3. The console.log at the end of `addWelcomeMessage()` shows it's being added
|
|
|
|
Let me check if there's an issue with the typing indicator reassignment or if something else is clearing the messages after they're added.
|
|
|
|
Actually, I see a potential issue. In `addWelcomeMessage()`, the typing indicator is being included in the HTML with a new ID `typing-indicator-new`, but then it tries to reassign the reference. This might be causing issues.
|
|
|
|
Also, I notice that in `addMessage()`, it checks if `this.typingIndicator.parentNode === this.chatMessages` before inserting messages. If the typing indicator reference is broken, this could cause issues.
|
|
|
|
Let me provide a fix that ensures the welcome message stays visible when creating a new session.Looking at your code, I can see the issue with the welcome message disappearing when starting a new conversation. The problem is in how the typing indicator is being handled when recreating the welcome message. Here's the fix:
|
|
|
|
## The Issue
|
|
|
|
In the `addWelcomeMessage()` method, you're creating a new typing indicator with a different ID (`typing-indicator-new`), which can cause reference issues. Also, the typing indicator element reference might become stale after clearing the innerHTML.
|
|
|
|
## The Solution
|
|
|
|
Here's the corrected `addWelcomeMessage()` method and a small fix to `createNewSession()`:
|
|
|
|
```javascript
|
|
addWelcomeMessage() {
|
|
const welcomeHTML = `
|
|
<div class="message bot-message animate-fadeIn">
|
|
<div class="message-wrapper">
|
|
<div class="message-avatar">E</div>
|
|
<div class="message-content-wrapper">
|
|
<div class="message-header">
|
|
<span class="message-sender">Ellucian AI Consultant</span>
|
|
<span class="message-time">Just now</span>
|
|
</div>
|
|
<div class="message-content">
|
|
<h2 style="margin-bottom: 16px; color: var(--primary);">
|
|
Welcome! I'm your Higher Education Solutions Consultant
|
|
</h2>
|
|
<p style="margin-bottom: 16px;">
|
|
I help institutional leaders solve their most pressing challenges with proven solutions from 2,900+ institutions worldwide.
|
|
</p>
|
|
|
|
<div class="insight-card">
|
|
<div class="insight-card-header">
|
|
🎯 Quick Start Options
|
|
</div>
|
|
<div class="smart-suggestions">
|
|
<button class="suggestion-chip" data-message="Our retention rates are below 70%">
|
|
📉 Retention Crisis
|
|
</button>
|
|
<button class="suggestion-chip" data-message="We waste 40+ hours/week on manual processes">
|
|
⏱️ Efficiency Issues
|
|
</button>
|
|
<button class="suggestion-chip" data-message="Our systems don't talk to each other">
|
|
🔄 Integration Challenges
|
|
</button>
|
|
<button class="suggestion-chip" data-message="Financial aid processing is too slow">
|
|
💰 Financial Aid Delays
|
|
</button>
|
|
<button class="suggestion-chip" data-message="I need to see ROI calculations">
|
|
📊 Show Me ROI
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p style="margin-top: 16px;">
|
|
<strong>Tell me about your biggest institutional challenge, and I'll provide targeted solutions with expected outcomes.</strong>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Set the HTML (don't include typing indicator in the HTML)
|
|
this.chatMessages.innerHTML = welcomeHTML;
|
|
|
|
// Re-add the typing indicator as a separate element
|
|
const typingIndicatorHTML = `
|
|
<div class="typing-indicator" id="typing-indicator">
|
|
<div class="message-wrapper">
|
|
<div class="message-avatar">E</div>
|
|
<div class="typing-dots">
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
</div>
|
|
<span style="margin-left: 8px; color: var(--gray-500); font-size: 14px;">
|
|
Analyzing your needs...
|
|
</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Create a new div element for typing indicator
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = typingIndicatorHTML;
|
|
const newTypingIndicator = tempDiv.firstElementChild;
|
|
|
|
// Append the typing indicator
|
|
this.chatMessages.appendChild(newTypingIndicator);
|
|
|
|
// Update the reference
|
|
this.typingIndicator = newTypingIndicator;
|
|
|
|
console.log('Welcome message added, typing indicator restored');
|
|
}
|
|
```
|
|
|
|
And update the `createNewSession()` method to ensure the typing indicator reference is maintained:
|
|
|
|
```javascript
|
|
createNewSession() {
|
|
// Prevent duplicate calls
|
|
if (this._creatingSession) {
|
|
console.log('Already creating session, skipping');
|
|
return;
|
|
}
|
|
|
|
this._creatingSession = true;
|
|
|
|
try {
|
|
console.log('Creating new session...');
|
|
|
|
// Save current session
|
|
if (this.currentSession) {
|
|
this.saveSession();
|
|
}
|
|
|
|
// Create new session data
|
|
this.conversationId = this.generateConversationId();
|
|
this.conversationHistory = [];
|
|
this.contextData = {
|
|
institution: null,
|
|
challenge: null,
|
|
authority: null,
|
|
metrics: null,
|
|
stage: 'discovery'
|
|
};
|
|
this.leadScore = 0;
|
|
|
|
// Create new session object
|
|
this.currentSession = {
|
|
id: this.conversationId,
|
|
title: 'New Conversation',
|
|
timestamp: new Date().toISOString(),
|
|
messages: [],
|
|
context: {}
|
|
};
|
|
|
|
// Clear UI and badges
|
|
this.chatMessages.innerHTML = '';
|
|
if (this.contextBadges) {
|
|
this.contextBadges.innerHTML = '';
|
|
}
|
|
|
|
// Add welcome message (this will also restore typing indicator)
|
|
this.addWelcomeMessage();
|
|
|
|
// Add session to list
|
|
this.addSessionToList(this.currentSession);
|
|
|
|
// Save new session
|
|
this.saveSession();
|
|
|
|
// Update sidebar
|
|
this.updateSessionList();
|
|
|
|
// Ensure input is focused
|
|
this.userInput.focus();
|
|
|
|
console.log('New session created:', this.conversationId);
|
|
|
|
} catch (error) {
|
|
console.error('Error creating new session:', error);
|
|
} finally {
|
|
// Reset flag
|
|
setTimeout(() => {
|
|
this._creatingSession = false;
|
|
}, 100);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Key Changes:
|
|
|
|
1. **Separated typing indicator**: Instead of including it in the welcome message HTML string, we create it as a separate DOM element and append it properly
|
|
2. **Maintained reference**: We properly update `this.typingIndicator` to point to the new element
|
|
3. **Consistent ID**: Keep using `typing-indicator` as the ID instead of creating a new one
|
|
4. **Proper DOM manipulation**: Use DOM methods to create and append the typing indicator instead of innerHTML manipulation
|
|
|
|
This approach ensures that:
|
|
- The welcome message is properly displayed
|
|
- The typing indicator reference is maintained correctly
|
|
- The `addMessage()` method can properly insert messages before the typing indicator
|
|
- No elements get accidentally removed or orphaned
|
|
|
|
The issue was that mixing innerHTML manipulation with element references was causing the typing indicator reference to become invalid, which could affect subsequent message additions.
|
|
|
|
## Human
|
|
Give me the complete updated template file.
|
|
|
|
## Ren
|
|
The user wants the complete updated template file with the fixes I suggested. I need to provide the full HTML file with the corrected JavaScript code, particularly the fixes to the `addWelcomeMessage()` and `createNewSession()` methods, while keeping all other functionality intact.Here's the complete updated `templates/chatbot.html` file with the fixes:
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="description" content="Ellucian Solutions - AI-Powered Higher Education Transformation Platform">
|
|
<title>Ellucian Solutions AI Consultant | Higher Education Excellence</title>
|
|
|
|
<!-- Preconnect for performance -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<style>
|
|
:root {
|
|
/* Ellucian Brand Colors */
|
|
--primary: #7100EB;
|
|
--primary-dark: #320070;
|
|
--primary-light: #8b45c1;
|
|
--accent: #95F4A0;
|
|
--accent-dark: #059669;
|
|
|
|
/* Semantic Colors */
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--danger: #ef4444;
|
|
--info: #3b82f6;
|
|
|
|
/* Neutral Palette */
|
|
--gray-50: #f9fafb;
|
|
--gray-100: #f3f4f6;
|
|
--gray-200: #e5e7eb;
|
|
--gray-300: #d1d5db;
|
|
--gray-400: #9ca3af;
|
|
--gray-500: #6b7280;
|
|
--gray-600: #4b5563;
|
|
--gray-700: #374151;
|
|
--gray-800: #1f2937;
|
|
--gray-900: #111827;
|
|
|
|
/* Shadows */
|
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
|
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
|
|
|
|
/* Animation Timings */
|
|
--transition-fast: 150ms;
|
|
--transition-base: 250ms;
|
|
--transition-slow: 350ms;
|
|
|
|
/* Spacing */
|
|
--spacing-xs: 0.25rem;
|
|
--spacing-sm: 0.5rem;
|
|
--spacing-md: 1rem;
|
|
--spacing-lg: 1.5rem;
|
|
--spacing-xl: 2rem;
|
|
--spacing-2xl: 3rem;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 50%, #bae6fd 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
color: var(--gray-800);
|
|
line-height: 1.6;
|
|
font-size: 16px;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
/* Main Container */
|
|
.app-container {
|
|
display: flex;
|
|
width: 100%;
|
|
height: 100vh;
|
|
background: white;
|
|
}
|
|
|
|
/* Sidebar */
|
|
.sidebar {
|
|
width: 280px;
|
|
background: var(--gray-900);
|
|
color: white;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: 1px solid var(--gray-800);
|
|
transition: width var(--transition-base);
|
|
}
|
|
|
|
.sidebar.collapsed {
|
|
width: 60px;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: var(--spacing-xl);
|
|
border-bottom: 1px solid var(--gray-800);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.logo {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
letter-spacing: -0.5px;
|
|
transition: opacity var(--transition-base);
|
|
}
|
|
|
|
.sidebar.collapsed .logo {
|
|
opacity: 0;
|
|
}
|
|
|
|
.sidebar-toggle {
|
|
background: none;
|
|
border: none;
|
|
color: var(--gray-400);
|
|
cursor: pointer;
|
|
padding: var(--spacing-sm);
|
|
border-radius: 4px;
|
|
transition: all var(--transition-base);
|
|
}
|
|
|
|
.sidebar-toggle:hover {
|
|
color: white;
|
|
background: var(--gray-800);
|
|
}
|
|
|
|
/* Session Management */
|
|
.session-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: var(--spacing-md);
|
|
}
|
|
|
|
.session-item {
|
|
padding: var(--spacing-md);
|
|
margin-bottom: var(--spacing-sm);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all var(--transition-base);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
color: var(--gray-400);
|
|
}
|
|
|
|
.session-item:hover {
|
|
background: var(--gray-800);
|
|
color: white;
|
|
}
|
|
|
|
.session-item.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.session-icon {
|
|
font-size: 20px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.session-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.session-title {
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.session-meta {
|
|
font-size: 12px;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.sidebar.collapsed .session-info {
|
|
display: none;
|
|
}
|
|
|
|
/* New Session Button */
|
|
.new-session-btn {
|
|
margin: var(--spacing-md);
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all var(--transition-base);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.new-session-btn:hover {
|
|
background: var(--primary-light);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.sidebar.collapsed .new-session-btn span {
|
|
display: none;
|
|
}
|
|
|
|
/* Main Chat Area */
|
|
.chat-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
}
|
|
|
|
/* Top Bar */
|
|
.top-bar {
|
|
background: white;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
padding: var(--spacing-lg) var(--spacing-xl);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.top-bar-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xl);
|
|
}
|
|
|
|
.conversation-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--gray-900);
|
|
}
|
|
|
|
.context-badges {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.context-badge {
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
background: var(--gray-100);
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--gray-700);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
animation: fadeIn var(--transition-base) ease-out;
|
|
}
|
|
|
|
.context-badge.institution {
|
|
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
|
color: var(--info);
|
|
border: 1px solid #93c5fd;
|
|
}
|
|
|
|
.context-badge.challenge {
|
|
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
|
color: #92400e;
|
|
border: 1px solid #fbbf24;
|
|
}
|
|
|
|
.context-badge.authority {
|
|
background: linear-gradient(135deg, #d4d4f8, #c7d2fe);
|
|
color: var(--primary);
|
|
border: 1px solid #a5b4fc;
|
|
}
|
|
|
|
/* Top Bar Actions */
|
|
.top-bar-actions {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.action-button {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background: white;
|
|
border: 1px solid var(--gray-300);
|
|
border-radius: 6px;
|
|
color: var(--gray-700);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all var(--transition-base);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.action-button:hover {
|
|
background: var(--gray-50);
|
|
border-color: var(--gray-400);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.action-button.primary {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.action-button.primary:hover {
|
|
background: var(--primary-dark);
|
|
}
|
|
|
|
/* Chat Messages Area */
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: var(--spacing-2xl);
|
|
background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
/* Enhanced Message Styling */
|
|
.message {
|
|
margin-bottom: var(--spacing-xl);
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
animation: messageSlideIn var(--transition-slow) ease-out forwards;
|
|
}
|
|
|
|
@keyframes messageSlideIn {
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.message-wrapper {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.message-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.bot-message .message-avatar {
|
|
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
|
color: white;
|
|
}
|
|
|
|
.user-message .message-avatar {
|
|
background: linear-gradient(135deg, var(--gray-600), var(--gray-700));
|
|
color: white;
|
|
}
|
|
|
|
.message-content-wrapper {
|
|
flex: 1;
|
|
max-width: 85%;
|
|
}
|
|
|
|
.message-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.message-sender {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: var(--gray-700);
|
|
}
|
|
|
|
.message-time {
|
|
font-size: 12px;
|
|
color: var(--gray-500);
|
|
}
|
|
|
|
.message-content {
|
|
background: white;
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: 12px;
|
|
padding: var(--spacing-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
position: relative;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.user-message .message-content {
|
|
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
|
color: white;
|
|
border: none;
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* Enhanced Rich Content */
|
|
.insight-card {
|
|
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
|
|
border: 1px solid #0ea5e9;
|
|
border-radius: 12px;
|
|
padding: var(--spacing-lg);
|
|
margin: var(--spacing-md) 0;
|
|
}
|
|
|
|
.insight-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
margin-bottom: var(--spacing-md);
|
|
font-weight: 600;
|
|
color: #0369a1;
|
|
}
|
|
|
|
.roi-calculator {
|
|
background: white;
|
|
border: 2px solid var(--primary);
|
|
border-radius: 12px;
|
|
padding: var(--spacing-xl);
|
|
margin: var(--spacing-lg) 0;
|
|
}
|
|
|
|
.roi-header {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
margin-bottom: var(--spacing-lg);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.roi-metrics {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin: var(--spacing-lg) 0;
|
|
}
|
|
|
|
.roi-metric {
|
|
text-align: center;
|
|
padding: var(--spacing-md);
|
|
background: var(--gray-50);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.roi-metric-value {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.roi-metric-label {
|
|
font-size: 12px;
|
|
color: var(--gray-600);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
/* CTA Container */
|
|
.cta-container {
|
|
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
|
border-radius: 16px;
|
|
padding: var(--spacing-xl);
|
|
margin: var(--spacing-lg) 0;
|
|
text-align: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.cta-container::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -50%;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
|
animation: ctaShine 3s infinite;
|
|
}
|
|
|
|
@keyframes ctaShine {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.cta-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: white;
|
|
margin-bottom: var(--spacing-sm);
|
|
position: relative;
|
|
}
|
|
|
|
.cta-description {
|
|
color: rgba(255, 255, 255, 0.9);
|
|
margin-bottom: var(--spacing-lg);
|
|
position: relative;
|
|
}
|
|
|
|
.cta-button {
|
|
background: white;
|
|
color: var(--primary);
|
|
border: none;
|
|
padding: var(--spacing-md) var(--spacing-xl);
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all var(--transition-base);
|
|
position: relative;
|
|
font-size: 16px;
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.cta-button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-xl);
|
|
}
|
|
|
|
/* Input Area */
|
|
.input-area {
|
|
background: white;
|
|
border-top: 1px solid var(--gray-200);
|
|
padding: var(--spacing-lg) var(--spacing-xl);
|
|
box-shadow: 0 -4px 6px -1px rgb(0 0 0 / 0.05);
|
|
}
|
|
|
|
.input-container {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
align-items: flex-end;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.input-wrapper {
|
|
flex: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.input-label {
|
|
font-size: 12px;
|
|
color: var(--gray-600);
|
|
margin-bottom: var(--spacing-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
#user-input {
|
|
width: 100%;
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
border: 2px solid var(--gray-300);
|
|
border-radius: 12px;
|
|
font-size: 15px;
|
|
font-family: inherit;
|
|
resize: none;
|
|
transition: all var(--transition-base);
|
|
min-height: 56px;
|
|
max-height: 150px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
#user-input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(113, 0, 235, 0.1);
|
|
}
|
|
|
|
.input-actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.input-button {
|
|
padding: var(--spacing-md);
|
|
background: white;
|
|
border: 1px solid var(--gray-300);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all var(--transition-base);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
.input-button:hover {
|
|
background: var(--gray-50);
|
|
border-color: var(--gray-400);
|
|
}
|
|
|
|
#send-button {
|
|
padding: var(--spacing-md) var(--spacing-xl);
|
|
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
|
color: white;
|
|
border: none;
|
|
border-radius: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all var(--transition-base);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
font-size: 15px;
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
#send-button:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
#send-button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Smart Suggestions */
|
|
.smart-suggestions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
margin-top: var(--spacing-md);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.suggestion-chip {
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
background: white;
|
|
border: 1px solid var(--gray-300);
|
|
border-radius: 20px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: all var(--transition-base);
|
|
color: var(--gray-700);
|
|
}
|
|
|
|
.suggestion-chip:hover {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* Typing Indicator */
|
|
.typing-indicator {
|
|
display: none;
|
|
padding: var(--spacing-lg);
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.typing-indicator.show {
|
|
display: flex;
|
|
}
|
|
|
|
.typing-dots {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.typing-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
background: var(--gray-400);
|
|
border-radius: 50%;
|
|
animation: typingDot 1.4s infinite;
|
|
}
|
|
|
|
.typing-dot:nth-child(2) {
|
|
animation-delay: 0.2s;
|
|
}
|
|
|
|
.typing-dot:nth-child(3) {
|
|
animation-delay: 0.4s;
|
|
}
|
|
|
|
@keyframes typingDot {
|
|
0%, 60%, 100% {
|
|
transform: translateY(0);
|
|
opacity: 0.5;
|
|
}
|
|
30% {
|
|
transform: translateY(-10px);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
/* Lead Score Indicator */
|
|
.lead-score {
|
|
position: fixed;
|
|
bottom: var(--spacing-xl);
|
|
right: var(--spacing-xl);
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: var(--spacing-lg);
|
|
box-shadow: var(--shadow-xl);
|
|
border: 1px solid var(--gray-200);
|
|
min-width: 200px;
|
|
z-index: 100;
|
|
transition: all var(--transition-base);
|
|
}
|
|
|
|
.lead-score-header {
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
color: var(--gray-600);
|
|
margin-bottom: var(--spacing-sm);
|
|
font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.lead-score-value {
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.lead-score-bar {
|
|
height: 6px;
|
|
background: var(--gray-200);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.lead-score-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--danger), var(--warning), var(--success));
|
|
border-radius: 3px;
|
|
transition: width var(--transition-slow);
|
|
}
|
|
|
|
.lead-score-label {
|
|
font-size: 13px;
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
/* Loading Overlay */
|
|
.loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10000;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity var(--transition-base);
|
|
}
|
|
|
|
.loading-overlay.show {
|
|
opacity: 1;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 4px solid var(--gray-200);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Mobile Responsive */
|
|
@media (max-width: 768px) {
|
|
.sidebar {
|
|
position: fixed;
|
|
left: -280px;
|
|
z-index: 1000;
|
|
transition: left var(--transition-base);
|
|
}
|
|
|
|
.sidebar.mobile-open {
|
|
left: 0;
|
|
}
|
|
|
|
.chat-main {
|
|
width: 100%;
|
|
}
|
|
|
|
.top-bar {
|
|
padding: var(--spacing-md);
|
|
}
|
|
|
|
.context-badges {
|
|
display: none;
|
|
}
|
|
|
|
.chat-messages {
|
|
padding: var(--spacing-lg);
|
|
}
|
|
|
|
.message-content-wrapper {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.roi-metrics {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.lead-score {
|
|
display: none;
|
|
}
|
|
|
|
.action-button span {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* Accessibility */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* {
|
|
animation: none !important;
|
|
transition: none !important;
|
|
}
|
|
}
|
|
|
|
/* Dark Mode Support */
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--gray-50: #1f2937;
|
|
--gray-100: #111827;
|
|
--gray-200: #374151;
|
|
--gray-300: #4b5563;
|
|
--gray-400: #6b7280;
|
|
--gray-500: #9ca3af;
|
|
--gray-600: #d1d5db;
|
|
--gray-700: #e5e7eb;
|
|
--gray-800: #f3f4f6;
|
|
--gray-900: #f9fafb;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
|
}
|
|
|
|
.app-container {
|
|
background: var(--gray-100);
|
|
}
|
|
|
|
.message-content {
|
|
background: var(--gray-200);
|
|
border-color: var(--gray-300);
|
|
}
|
|
}
|
|
|
|
/* Animations */
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from { transform: translateY(20px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { transform: scale(1); }
|
|
50% { transform: scale(1.05); }
|
|
}
|
|
|
|
.animate-fadeIn {
|
|
animation: fadeIn var(--transition-base);
|
|
}
|
|
|
|
.animate-slideUp {
|
|
animation: slideUp var(--transition-slow);
|
|
}
|
|
|
|
.animate-pulse {
|
|
animation: pulse 2s infinite;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-container">
|
|
<!-- Sidebar -->
|
|
<aside class="sidebar" id="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="logo">ELLUCIAN</div>
|
|
<button class="sidebar-toggle" id="sidebar-toggle" aria-label="Toggle sidebar">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
|
<path d="M3 5h14M3 10h14M3 15h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<button class="new-session-btn" id="new-session-btn">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
<path d="M10 5v10M5 10h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
<span>New Conversation</span>
|
|
</button>
|
|
|
|
<div class="session-list" id="session-list">
|
|
<!-- Sessions will be dynamically added here -->
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Chat Area -->
|
|
<main class="chat-main">
|
|
<!-- Top Bar -->
|
|
<header class="top-bar">
|
|
<div class="top-bar-left">
|
|
<h1 class="conversation-title" id="conversation-title">Higher Education Solutions Consultant</h1>
|
|
<div class="context-badges" id="context-badges">
|
|
<!-- Context badges will be dynamically added -->
|
|
</div>
|
|
</div>
|
|
<div class="top-bar-actions">
|
|
<button class="action-button" id="export-btn">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
<path d="M8 2v8M8 10l-3-3M8 10l3-3M2 14h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>
|
|
<span>Export</span>
|
|
</button>
|
|
<button class="action-button" id="insights-btn">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
<path d="M8 2l2 4 4 1-3 3 1 4-4-2-4 2 1-4-3-3 4-1z" stroke="currentColor" stroke-width="1.5"/>
|
|
</svg>
|
|
<span>Insights</span>
|
|
</button>
|
|
<button class="action-button primary" id="schedule-demo-btn">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
<path d="M14 6H2M14 10H2M6 2v4M10 2v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>
|
|
<span>Schedule Demo</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Chat Messages -->
|
|
<div class="chat-messages" id="chat-messages">
|
|
<!-- Initial welcome message -->
|
|
<div class="message bot-message animate-fadeIn">
|
|
<div class="message-wrapper">
|
|
<div class="message-avatar">E</div>
|
|
<div class="message-content-wrapper">
|
|
<div class="message-header">
|
|
<span class="message-sender">Ellucian AI Consultant</span>
|
|
<span class="message-time">Just now</span>
|
|
</div>
|
|
<div class="message-content">
|
|
<h2 style="margin-bottom: 16px; color: var(--primary);">
|
|
Welcome! I'm your Higher Education Solutions Consultant
|
|
</h2>
|
|
<p style="margin-bottom: 16px;">
|
|
I help institutional leaders solve their most pressing challenges with proven solutions from 2,900+ institutions worldwide.
|
|
</p>
|
|
|
|
<div class="insight-card">
|
|
<div class="insight-card-header">
|
|
🎯 Quick Start Options
|
|
</div>
|
|
<div class="smart-suggestions">
|
|
<button class="suggestion-chip" data-message="Our retention rates are below 70%">
|
|
📉 Retention Crisis
|
|
</button>
|
|
<button class="suggestion-chip" data-message="We waste 40+ hours/week on manual processes">
|
|
⏱️ Efficiency Issues
|
|
</button>
|
|
<button class="suggestion-chip" data-message="Our systems don't talk to each other">
|
|
🔄 Integration Challenges
|
|
</button>
|
|
<button class="suggestion-chip" data-message="Financial aid processing is too slow">
|
|
💰 Financial Aid Delays
|
|
</button>
|
|
<button class="suggestion-chip" data-message="I need to see ROI calculations">
|
|
📊 Show Me ROI
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p style="margin-top: 16px;">
|
|
<strong>Tell me about your biggest institutional challenge, and I'll provide targeted solutions with expected outcomes.</strong>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Typing Indicator -->
|
|
<div class="typing-indicator" id="typing-indicator">
|
|
<div class="message-wrapper">
|
|
<div class="message-avatar">E</div>
|
|
<div class="typing-dots">
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
</div>
|
|
<span style="margin-left: 8px; color: var(--gray-500); font-size: 14px;">
|
|
Analyzing your needs...
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Input Area -->
|
|
<div class="input-area">
|
|
<div class="input-container">
|
|
<div class="input-wrapper">
|
|
<div class="input-label">Describe your institutional challenge</div>
|
|
<textarea
|
|
id="user-input"
|
|
placeholder="e.g., 'We're losing 30% of freshmen' or 'I'm the CIO at State University...'"
|
|
rows="1"
|
|
aria-label="Type your message"
|
|
></textarea>
|
|
</div>
|
|
<div class="input-actions">
|
|
<button class="input-button" id="attach-btn" aria-label="Attach file">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
<path d="M10 2v12M10 14c0 2-1.5 3-3 3s-3-1.5-3-3V6c0-1.5 1-2.5 2.5-2.5S9 4.5 9 6v8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>
|
|
</button>
|
|
<button class="input-button" id="voice-btn" aria-label="Voice input">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
<path d="M10 2v8M7 6v4M13 6v4M5 8v2M15 8v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>
|
|
</button>
|
|
<button id="send-button" aria-label="Send message">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
<path d="M2 10l16-8-4 8 4 8-16-8z" fill="currentColor"/>
|
|
</svg>
|
|
<span>Send</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="smart-suggestions" id="input-suggestions" style="display: none;">
|
|
<!-- Dynamic suggestions will appear here -->
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Lead Score Widget -->
|
|
<div class="lead-score" id="lead-score">
|
|
<div class="lead-score-header">Lead Quality Score</div>
|
|
<div class="lead-score-value" id="lead-score-value">0</div>
|
|
<div class="lead-score-bar">
|
|
<div class="lead-score-fill" id="lead-score-fill" style="width: 0%"></div>
|
|
</div>
|
|
<div class="lead-score-label" id="lead-score-label">Gathering context...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div class="loading-overlay" id="loading-overlay">
|
|
<div class="loading-spinner"></div>
|
|
</div>
|
|
|
|
<script>
|
|
// Enhanced Chat Application with Fixed Welcome Message
|
|
class EllucianChat {
|
|
constructor() {
|
|
// Initialize state
|
|
this.conversationId = this.generateConversationId();
|
|
this.conversationHistory = [];
|
|
this.sessions = [];
|
|
this.currentSession = null;
|
|
this.contextData = {
|
|
institution: null,
|
|
challenge: null,
|
|
authority: null,
|
|
metrics: null,
|
|
stage: 'discovery'
|
|
};
|
|
this.leadScore = 0;
|
|
this._creatingSession = false;
|
|
this._initialized = false;
|
|
|
|
// Initialize DOM elements
|
|
this.initializeElements();
|
|
|
|
// Load sessions BEFORE binding events
|
|
this.sessions = this.loadSessions();
|
|
|
|
// Check if we should restore a session or keep welcome message
|
|
const shouldRestoreSession = this.checkForExistingSession();
|
|
|
|
// Bind events
|
|
this.bindEvents();
|
|
|
|
// Initialize session handling
|
|
if (!shouldRestoreSession) {
|
|
// New user or no valid session - keep the welcome message that's already in HTML
|
|
this.initializeNewUserSession();
|
|
} else {
|
|
// Restore last session
|
|
this.restoreLastSession();
|
|
}
|
|
|
|
// Start background processes
|
|
this.startContextAnalysis();
|
|
|
|
// Mark as initialized
|
|
this._initialized = true;
|
|
|
|
console.log('Chat initialized with session:', this.conversationId);
|
|
}
|
|
|
|
initializeElements() {
|
|
// Core elements
|
|
this.chatMessages = document.getElementById('chat-messages');
|
|
this.userInput = document.getElementById('user-input');
|
|
this.sendButton = document.getElementById('send-button');
|
|
this.typingIndicator = document.getElementById('typing-indicator');
|
|
|
|
// Sidebar elements
|
|
this.sidebar = document.getElementById('sidebar');
|
|
this.sidebarToggle = document.getElementById('sidebar-toggle');
|
|
this.sessionList = document.getElementById('session-list');
|
|
this.newSessionBtn = document.getElementById('new-session-btn');
|
|
|
|
// Top bar elements
|
|
this.conversationTitle = document.getElementById('conversation-title');
|
|
this.contextBadges = document.getElementById('context-badges');
|
|
this.exportBtn = document.getElementById('export-btn');
|
|
this.insightsBtn = document.getElementById('insights-btn');
|
|
this.scheduleDemoBtn = document.getElementById('schedule-demo-btn');
|
|
|
|
// Other elements
|
|
this.leadScoreValue = document.getElementById('lead-score-value');
|
|
this.leadScoreFill = document.getElementById('lead-score-fill');
|
|
this.leadScoreLabel = document.getElementById('lead-score-label');
|
|
this.loadingOverlay = document.getElementById('loading-overlay');
|
|
this.inputSuggestions = document.getElementById('input-suggestions');
|
|
|
|
// Input action buttons
|
|
this.attachBtn = document.getElementById('attach-btn');
|
|
this.voiceBtn = document.getElementById('voice-btn');
|
|
}
|
|
|
|
checkForExistingSession() {
|
|
// Check if there are existing sessions to restore
|
|
if (this.sessions && this.sessions.length > 0) {
|
|
// Check if the last session has messages
|
|
const lastSession = this.sessions[0];
|
|
if (lastSession && lastSession.messages && lastSession.messages.length > 0) {
|
|
return true; // Restore this session
|
|
}
|
|
}
|
|
return false; // Keep welcome message
|
|
}
|
|
|
|
initializeNewUserSession() {
|
|
// Create a new session but DON'T clear the welcome message
|
|
this.currentSession = {
|
|
id: this.conversationId,
|
|
title: 'New Conversation',
|
|
timestamp: new Date().toISOString(),
|
|
messages: [],
|
|
context: {}
|
|
};
|
|
|
|
// Save session
|
|
this.saveSession();
|
|
|
|
// The welcome message is already in the HTML, so we don't need to add it
|
|
console.log('New user session initialized, keeping welcome message');
|
|
}
|
|
|
|
restoreLastSession() {
|
|
// Restore the most recent session with messages
|
|
const lastSession = this.sessions[0];
|
|
if (lastSession) {
|
|
this.loadSession(lastSession.id);
|
|
console.log('Restored session:', lastSession.id);
|
|
}
|
|
}
|
|
|
|
bindEvents() {
|
|
// Message sending
|
|
this.sendButton.addEventListener('click', () => this.sendMessage());
|
|
this.userInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
this.sendMessage();
|
|
}
|
|
});
|
|
|
|
// Auto-resize textarea
|
|
this.userInput.addEventListener('input', () => {
|
|
this.userInput.style.height = 'auto';
|
|
this.userInput.style.height = Math.min(this.userInput.scrollHeight, 150) + 'px';
|
|
this.updateInputSuggestions();
|
|
});
|
|
|
|
// Sidebar
|
|
this.sidebarToggle.addEventListener('click', () => this.toggleSidebar());
|
|
|
|
// New session button - with protection
|
|
this.newSessionBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.createNewSession();
|
|
});
|
|
|
|
// Top bar actions
|
|
this.exportBtn.addEventListener('click', () => this.exportConversation());
|
|
this.insightsBtn.addEventListener('click', () => this.showInsights());
|
|
this.scheduleDemoBtn.addEventListener('click', () => this.scheduleDemo());
|
|
|
|
// Input actions
|
|
this.attachBtn.addEventListener('click', () => this.handleAttachment());
|
|
this.voiceBtn.addEventListener('click', () => this.toggleVoiceInput());
|
|
|
|
// Suggestion chips - event delegation
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('suggestion-chip')) {
|
|
const message = e.target.getAttribute('data-message');
|
|
if (message) {
|
|
this.userInput.value = message;
|
|
this.sendMessage();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Mobile menu
|
|
if (window.innerWidth <= 768) {
|
|
document.addEventListener('click', (e) => {
|
|
if (!this.sidebar.contains(e.target) && !this.sidebarToggle.contains(e.target)) {
|
|
this.sidebar.classList.remove('mobile-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Focus input
|
|
this.userInput.focus();
|
|
}
|
|
|
|
generateConversationId() {
|
|
return 'conv_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
}
|
|
|
|
loadSessions() {
|
|
try {
|
|
const saved = localStorage.getItem('ellucian_sessions');
|
|
return saved ? JSON.parse(saved) : [];
|
|
} catch (error) {
|
|
console.error('Error loading sessions:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
saveSession() {
|
|
try {
|
|
const sessionIndex = this.sessions.findIndex(s => s.id === this.currentSession.id);
|
|
if (sessionIndex >= 0) {
|
|
this.sessions[sessionIndex] = this.currentSession;
|
|
} else {
|
|
this.sessions.unshift(this.currentSession);
|
|
}
|
|
|
|
// Keep only last 20 sessions
|
|
this.sessions = this.sessions.slice(0, 20);
|
|
localStorage.setItem('ellucian_sessions', JSON.stringify(this.sessions));
|
|
} catch (error) {
|
|
console.error('Error saving session:', error);
|
|
}
|
|
}
|
|
|
|
createNewSession() {
|
|
// Prevent duplicate calls
|
|
if (this._creatingSession) {
|
|
console.log('Already creating session, skipping');
|
|
return;
|
|
}
|
|
|
|
this._creatingSession = true;
|
|
|
|
try {
|
|
console.log('Creating new session...');
|
|
|
|
// Save current session
|
|
if (this.currentSession) {
|
|
this.saveSession();
|
|
}
|
|
|
|
// Create new session data
|
|
this.conversationId = this.generateConversationId();
|
|
this.conversationHistory = [];
|
|
this.contextData = {
|
|
institution: null,
|
|
challenge: null,
|
|
authority: null,
|
|
metrics: null,
|
|
stage: 'discovery'
|
|
};
|
|
this.leadScore = 0;
|
|
|
|
// Create new session object
|
|
this.currentSession = {
|
|
id: this.conversationId,
|
|
title: 'New Conversation',
|
|
timestamp: new Date().toISOString(),
|
|
messages: [],
|
|
context: {}
|
|
};
|
|
|
|
// Clear UI and badges
|
|
this.chatMessages.innerHTML = '';
|
|
if (this.contextBadges) {
|
|
this.contextBadges.innerHTML = '';
|
|
}
|
|
|
|
// Add welcome message (this will also restore typing indicator)
|
|
this.addWelcomeMessage();
|
|
|
|
// Add session to list
|
|
this.addSessionToList(this.currentSession);
|
|
|
|
// Save new session
|
|
this.saveSession();
|
|
|
|
// Update sidebar
|
|
this.updateSessionList();
|
|
|
|
// Ensure input is focused
|
|
this.userInput.focus();
|
|
|
|
console.log('New session created:', this.conversationId);
|
|
|
|
} catch (error) {
|
|
console.error('Error creating new session:', error);
|
|
} finally {
|
|
// Reset flag
|
|
setTimeout(() => {
|
|
this._creatingSession = false;
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
addWelcomeMessage() {
|
|
const welcomeHTML = `
|
|
<div class="message bot-message animate-fadeIn">
|
|
<div class="message-wrapper">
|
|
<div class="message-avatar">E</div>
|
|
<div class="message-content-wrapper">
|
|
<div class="message-header">
|
|
<span class="message-sender">Ellucian AI Consultant</span>
|
|
<span class="message-time">Just now</span>
|
|
</div>
|
|
<div class="message-content">
|
|
<h2 style="margin-bottom: 16px; color: var(--primary);">
|
|
Welcome! I'm your Higher Education Solutions Consultant
|
|
</h2>
|
|
<p style="margin-bottom: 16px;">
|
|
I help institutional leaders solve their most pressing challenges with proven solutions from 2,900+ institutions worldwide.
|
|
</p>
|
|
|
|
<div class="insight-card">
|
|
<div class="insight-card-header">
|
|
🎯 Quick Start Options
|
|
</div>
|
|
<div class="smart-suggestions">
|
|
<button class="suggestion-chip" data-message="Our retention rates are below 70%">
|
|
📉 Retention Crisis
|
|
</button>
|
|
<button class="suggestion-chip" data-message="We waste 40+ hours/week on manual processes">
|
|
⏱️ Efficiency Issues
|
|
</button>
|
|
<button class="suggestion-chip" data-message="Our systems don't talk to each other">
|
|
🔄 Integration Challenges
|
|
</button>
|
|
<button class="suggestion-chip" data-message="Financial aid processing is too slow">
|
|
💰 Financial Aid Delays
|
|
</button>
|
|
<button class="suggestion-chip" data-message="I need to see ROI calculations">
|
|
📊 Show Me ROI
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p style="margin-top: 16px;">
|
|
<strong>Tell me about your biggest institutional challenge, and I'll provide targeted solutions with expected outcomes.</strong>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Set the HTML (don't include typing indicator in the HTML)
|
|
this.chatMessages.innerHTML = welcomeHTML;
|
|
|
|
// Re-add the typing indicator as a separate element
|
|
const typingIndicatorHTML = `
|
|
<div class="typing-indicator" id="typing-indicator">
|
|
<div class="message-wrapper">
|
|
<div class="message-avatar">E</div>
|
|
<div class="typing-dots">
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
</div>
|
|
<span style="margin-left: 8px; color: var(--gray-500); font-size: 14px;">
|
|
Analyzing your needs...
|
|
</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Create a new div element for typing indicator
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = typingIndicatorHTML;
|
|
const newTypingIndicator = tempDiv.firstElementChild;
|
|
|
|
// Append the typing indicator
|
|
this.chatMessages.appendChild(newTypingIndicator);
|
|
|
|
// Update the reference
|
|
this.typingIndicator = newTypingIndicator;
|
|
|
|
console.log('Welcome message added, typing indicator restored');
|
|
}
|
|
|
|
addSessionToList(session) {
|
|
// Check if session already exists in list
|
|
const existing = document.querySelector(`.session-item[data-session-id="${session.id}"]`);
|
|
if (existing) {
|
|
return; // Don't add duplicate
|
|
}
|
|
|
|
const sessionItem = document.createElement('div');
|
|
sessionItem.className = 'session-item';
|
|
sessionItem.dataset.sessionId = session.id;
|
|
|
|
const timeStr = this.formatTime(new Date(session.timestamp));
|
|
|
|
sessionItem.innerHTML = `
|
|
<div class="session-icon">💬</div>
|
|
<div class="session-info">
|
|
<div class="session-title">${session.title}</div>
|
|
<div class="session-meta">${timeStr}</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add click handler
|
|
sessionItem.addEventListener('click', () => {
|
|
if (this.currentSession.id !== session.id) {
|
|
this.loadSession(session.id);
|
|
}
|
|
});
|
|
|
|
// Add to top of list
|
|
this.sessionList.insertBefore(sessionItem, this.sessionList.firstChild);
|
|
}
|
|
|
|
updateSessionList() {
|
|
// Update active states
|
|
document.querySelectorAll('.session-item').forEach(item => {
|
|
if (item.dataset.sessionId === this.currentSession.id) {
|
|
item.classList.add('active');
|
|
} else {
|
|
item.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
loadSession(sessionId) {
|
|
// Don't load if we're creating a new session
|
|
if (this._creatingSession) {
|
|
return;
|
|
}
|
|
|
|
const session = this.sessions.find(s => s.id === sessionId);
|
|
if (!session) return;
|
|
|
|
console.log('Loading session:', sessionId);
|
|
|
|
// Load session data
|
|
this.currentSession = session;
|
|
this.conversationId = session.id;
|
|
this.conversationHistory = session.messages || [];
|
|
this.contextData = session.context || {
|
|
institution: null,
|
|
challenge: null,
|
|
authority: null,
|
|
metrics: null,
|
|
stage: 'discovery'
|
|
};
|
|
|
|
// Clear and rebuild UI
|
|
this.chatMessages.innerHTML = '';
|
|
this.contextBadges.innerHTML = '';
|
|
|
|
// Add messages or welcome
|
|
if (this.conversationHistory.length === 0) {
|
|
this.addWelcomeMessage();
|
|
} else {
|
|
// Recreate messages
|
|
for (const msg of this.conversationHistory) {
|
|
this.addMessage(msg.content, msg.role);
|
|
}
|
|
// Re-add typing indicator after all messages
|
|
this.restoreTypingIndicator();
|
|
}
|
|
|
|
// Update session list
|
|
this.updateSessionList();
|
|
}
|
|
|
|
restoreTypingIndicator() {
|
|
// Create and append typing indicator
|
|
const typingIndicatorHTML = `
|
|
<div class="typing-indicator" id="typing-indicator">
|
|
<div class="message-wrapper">
|
|
<div class="message-avatar">E</div>
|
|
<div class="typing-dots">
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
<span class="typing-dot"></span>
|
|
</div>
|
|
<span style="margin-left: 8px; color: var(--gray-500); font-size: 14px;">
|
|
Analyzing your needs...
|
|
</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = typingIndicatorHTML;
|
|
const newTypingIndicator = tempDiv.firstElementChild;
|
|
|
|
this.chatMessages.appendChild(newTypingIndicator);
|
|
this.typingIndicator = newTypingIndicator;
|
|
}
|
|
|
|
async sendMessage(messageOverride = null) {
|
|
const message = messageOverride || this.userInput.value.trim();
|
|
|
|
if (!message) return;
|
|
|
|
// Clear input
|
|
if (!messageOverride) {
|
|
this.userInput.value = '';
|
|
this.userInput.style.height = 'auto';
|
|
if (this.inputSuggestions) {
|
|
this.inputSuggestions.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Add user message
|
|
this.addMessage(message, 'user');
|
|
this.conversationHistory.push({ role: 'user', content: message });
|
|
|
|
// Update session
|
|
this.currentSession.messages.push({ role: 'user', content: message });
|
|
|
|
// Analyze context
|
|
this.analyzeUserMessage(message);
|
|
|
|
// Show typing indicator
|
|
this.showTypingIndicator();
|
|
|
|
// Disable send button
|
|
this.sendButton.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch('/chat', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
message: message,
|
|
conversation_id: this.conversationId,
|
|
conversation_history: this.conversationHistory.slice(-8),
|
|
context_data: this.contextData
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Hide typing indicator
|
|
this.hideTypingIndicator();
|
|
|
|
if (data.response) {
|
|
this.addMessage(data.response, 'bot', data);
|
|
this.conversationHistory.push({ role: 'assistant', content: data.response });
|
|
this.currentSession.messages.push({ role: 'assistant', content: data.response });
|
|
|
|
// Update lead score
|
|
this.updateLeadScore(data);
|
|
|
|
// Save session
|
|
this.saveSession();
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
this.hideTypingIndicator();
|
|
this.addMessage('I apologize, but I encountered an error. Please try again.', 'bot');
|
|
} finally {
|
|
this.sendButton.disabled = false;
|
|
}
|
|
}
|
|
|
|
addMessage(content, sender, data = null) {
|
|
const message = document.createElement('div');
|
|
message.className = `message ${sender}-message`;
|
|
|
|
const timestamp = new Date();
|
|
const timeStr = this.formatTime(timestamp);
|
|
|
|
const messageHTML = `
|
|
<div class="message-wrapper">
|
|
<div class="message-avatar">${sender === 'bot' ? 'E' : 'U'}</div>
|
|
<div class="message-content-wrapper">
|
|
<div class="message-header">
|
|
<span class="message-sender">${sender === 'bot' ? 'Ellucian AI Consultant' : 'You'}</span>
|
|
<span class="message-time">${timeStr}</span>
|
|
</div>
|
|
<div class="message-content">
|
|
${this.formatMessageContent(content, sender, data)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
message.innerHTML = messageHTML;
|
|
|
|
// Insert before typing indicator if it exists, otherwise append
|
|
if (this.typingIndicator && this.typingIndicator.parentNode === this.chatMessages) {
|
|
this.chatMessages.insertBefore(message, this.typingIndicator);
|
|
} else {
|
|
this.chatMessages.appendChild(message);
|
|
// If typing indicator is missing, restore it
|
|
if (!this.typingIndicator || !this.typingIndicator.parentNode) {
|
|
this.restoreTypingIndicator();
|
|
}
|
|
}
|
|
|
|
// Scroll to bottom
|
|
setTimeout(() => {
|
|
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
|
|
}, 100);
|
|
|
|
// Update session title if first meaningful exchange
|
|
if (this.currentSession.messages.length === 2 && sender === 'bot') {
|
|
this.updateSessionTitle(content);
|
|
}
|
|
}
|
|
|
|
formatMessageContent(content, sender, data) {
|
|
if (sender === 'user') {
|
|
return `<p>${this.escapeHtml(content)}</p>`;
|
|
}
|
|
|
|
// Format bot messages
|
|
let formatted = content;
|
|
|
|
// Convert markdown-style formatting
|
|
formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
formatted = formatted.replace(/\n\n/g, '</p><p>');
|
|
formatted = formatted.replace(/\n/g, '<br>');
|
|
|
|
// Add paragraph tags if not present
|
|
if (!formatted.includes('<p>')) {
|
|
formatted = '<p>' + formatted + '</p>';
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
|
|
showTypingIndicator() {
|
|
if (this.typingIndicator) {
|
|
this.typingIndicator.classList.add('show');
|
|
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
|
|
}
|
|
}
|
|
|
|
hideTypingIndicator() {
|
|
if (this.typingIndicator) {
|
|
this.typingIndicator.classList.remove('show');
|
|
}
|
|
}
|
|
|
|
analyzeUserMessage(message) {
|
|
// Basic context extraction
|
|
const messageLower = message.toLowerCase();
|
|
|
|
// Check for institution mentions
|
|
if (messageLower.includes('university') || messageLower.includes('college')) {
|
|
this.contextData.institution = 'detected';
|
|
this.updateContextBadges();
|
|
}
|
|
|
|
// Check for challenge mentions
|
|
const challenges = ['retention', 'enrollment', 'efficiency', 'integration', 'financial aid'];
|
|
for (const challenge of challenges) {
|
|
if (messageLower.includes(challenge)) {
|
|
this.contextData.challenge = challenge;
|
|
this.updateContextBadges();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check for authority indicators
|
|
const authorities = ['president', 'provost', 'dean', 'director', 'vp', 'cio', 'cto'];
|
|
for (const authority of authorities) {
|
|
if (messageLower.includes(authority)) {
|
|
this.contextData.authority = authority;
|
|
this.updateContextBadges();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateContextBadges() {
|
|
if (!this.contextBadges) return;
|
|
|
|
this.contextBadges.innerHTML = '';
|
|
|
|
if (this.contextData.institution) {
|
|
const badge = document.createElement('div');
|
|
badge.className = 'context-badge institution';
|
|
badge.innerHTML = `🏛️ Institution Identified`;
|
|
this.contextBadges.appendChild(badge);
|
|
}
|
|
|
|
if (this.contextData.challenge) {
|
|
const badge = document.createElement('div');
|
|
badge.className = 'context-badge challenge';
|
|
badge.innerHTML = `🎯 ${this.contextData.challenge}`;
|
|
this.contextBadges.appendChild(badge);
|
|
}
|
|
|
|
if (this.contextData.authority) {
|
|
const badge = document.createElement('div');
|
|
badge.className = 'context-badge authority';
|
|
badge.innerHTML = `👤 ${this.contextData.authority}`;
|
|
this.contextBadges.appendChild(badge);
|
|
}
|
|
}
|
|
|
|
updateLeadScore(data) {
|
|
if (data.lead_score !== undefined) {
|
|
this.leadScore = data.lead_score;
|
|
|
|
// Update UI
|
|
if (this.leadScoreValue) {
|
|
this.leadScoreValue.textContent = this.leadScore;
|
|
}
|
|
|
|
if (this.leadScoreFill) {
|
|
this.leadScoreFill.style.width = `${this.leadScore}%`;
|
|
}
|
|
|
|
if (this.leadScoreLabel) {
|
|
let label = 'Gathering context...';
|
|
if (this.leadScore >= 80) {
|
|
label = 'High-quality lead';
|
|
} else if (this.leadScore >= 50) {
|
|
label = 'Qualified lead';
|
|
} else if (this.leadScore >= 25) {
|
|
label = 'Developing lead';
|
|
}
|
|
this.leadScoreLabel.textContent = label;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateInputSuggestions() {
|
|
// Could implement smart suggestions based on context
|
|
// For now, keeping it simple
|
|
}
|
|
|
|
updateSessionTitle(content) {
|
|
// Extract a meaningful title
|
|
const cleanContent = content.replace(/<[^>]*>/g, '').trim();
|
|
const title = cleanContent.substring(0, 50) + (cleanContent.length > 50 ? '...' : '');
|
|
this.currentSession.title = title;
|
|
|
|
// Update in UI
|
|
const sessionItem = document.querySelector(`.session-item[data-session-id="${this.currentSession.id}"] .session-title`);
|
|
if (sessionItem) {
|
|
sessionItem.textContent = title;
|
|
}
|
|
|
|
this.saveSession();
|
|
}
|
|
|
|
toggleSidebar() {
|
|
this.sidebar.classList.toggle('collapsed');
|
|
|
|
if (window.innerWidth <= 768) {
|
|
this.sidebar.classList.toggle('mobile-open');
|
|
}
|
|
}
|
|
|
|
exportConversation() {
|
|
const exportData = {
|
|
conversation_id: this.conversationId,
|
|
timestamp: new Date().toISOString(),
|
|
context: this.contextData,
|
|
messages: this.conversationHistory
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `ellucian-conversation-${this.conversationId}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
showInsights() {
|
|
// Show insights implementation
|
|
console.log('Showing insights for conversation:', this.conversationId);
|
|
// Could open a modal or navigate to insights page
|
|
}
|
|
|
|
scheduleDemo() {
|
|
window.open('https://www.ellucian.com/schedule-demo', '_blank');
|
|
}
|
|
|
|
handleAttachment() {
|
|
console.log('Handle attachment - not yet implemented');
|
|
// Could implement file upload functionality
|
|
}
|
|
|
|
toggleVoiceInput() {
|
|
console.log('Toggle voice input - not yet implemented');
|
|
// Could implement speech-to-text functionality
|
|
}
|
|
|
|
startContextAnalysis() {
|
|
// Start background analysis
|
|
setInterval(() => {
|
|
// Periodic context analysis could go here
|
|
// For example, analyzing conversation patterns
|
|
}, 30000);
|
|
}
|
|
|
|
formatTime(date) {
|
|
const now = new Date();
|
|
const diff = now - date;
|
|
const seconds = Math.floor(diff / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
const days = Math.floor(hours / 24);
|
|
|
|
if (seconds < 60) return 'Just now';
|
|
if (minutes < 60) return `${minutes}m ago`;
|
|
if (hours < 24) return `${hours}h ago`;
|
|
if (days < 7) return `${days}d ago`;
|
|
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// Initialize application
|
|
let app;
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Make sure we only initialize once
|
|
if (!app) {
|
|
app = new EllucianChat();
|
|
window.app = app; // For debugging
|
|
|
|
console.log('🚀 Ellucian Enhanced Chat Application Initialized');
|
|
console.log('📊 Session ID:', app.conversationId);
|
|
console.log('✅ Welcome message preserved, typing indicator restored');
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
## Key Fixes Made:
|
|
|
|
1. **Separated typing indicator from welcome message**: The typing indicator is now created as a separate DOM element and properly appended to maintain the reference.
|
|
|
|
2. **Added `restoreTypingIndicator()` method**: This ensures the typing indicator is properly restored when loading sessions.
|
|
|
|
3. **Fixed `addWelcomeMessage()` method**: Now properly creates and maintains the typing indicator reference.
|
|
|
|
4. **Updated `addMessage()` method**: Added a check to restore the typing indicator if it's missing.
|
|
|
|
5. **Improved `loadSession()` method**: Now properly restores the typing indicator after loading messages.
|
|
|
|
The main issue was that the typing indicator element reference was becoming stale when the innerHTML was cleared. By properly managing the DOM elements and maintaining references, the welcome message will now persist correctly when creating new sessions.
|
|
|
|
## Human
|
|
I pushed the new template. Here's the current code. When I submit a new message, I see this in the javascript console: [Error] Failed to load resource: the server responded with a status of 500 (INTERNAL SERVER ERROR) (chat, line 0)
|
|
[Error] Failed to load resource: You do not have permission to access the requested resource.
|
|
[Error] Failed to load resource: the server responded with a status of 500 (INTERNAL SERVER ERROR) (chat, line 0)
|
|
[Error] Failed to load resource: the server responded with a status of 500 (INTERNAL SERVER ERROR) (chat, line 0)
|
|
|
|
## Ren
|
|
The user is getting a 500 Internal Server Error when trying to send a message to the `/chat` endpoint. Looking at the app.py file, I can see the issue. The `/chat` route is defined with `async def chat():` but Flask doesn't natively support async route handlers without special configuration. The route is trying to use `await` to call `agent.process_message()` but this won't work in a standard Flask app.
|
|
|
|
The problem is in app.py around line 1366:
|
|
|
|
```python
|
|
@app.route('/chat', methods=['POST'])
|
|
async def chat():
|
|
"""Enhanced chat endpoint with full processing"""
|
|
try:
|
|
# ...
|
|
# Process message
|
|
result = await agent.process_message(
|
|
user_message,
|
|
conversation_id,
|
|
conversation_history,
|
|
context_data
|
|
)
|
|
```
|
|
|
|
Flask doesn't support async route handlers by default. You need to either:
|
|
1. Use a synchronous version of the function
|
|
2. Use Flask with async support (requires specific setup)
|
|
3. Use asyncio.run() to run the async function synchronously
|
|
|
|
Looking at the code, the async functionality is primarily used in the Azure AI client for making HTTP requests. The simplest fix would be to convert the route handler to be synchronous and use asyncio.run() to call the async functions.
|
|
|
|
Let me provide a fix for this issue.The issue is that your Flask `/chat` route is defined as `async` but Flask doesn't natively support async route handlers without special configuration. The `async def chat():` function is trying to use `await` which is causing the 500 error.
|
|
|
|
Here's the fix for your `app.py` file. Replace the `/chat` route (around line 1366) with this synchronous version:
|
|
|
|
```python
|
|
@app.route('/chat', methods=['POST'])
|
|
def chat():
|
|
"""Enhanced chat endpoint with full processing"""
|
|
try:
|
|
data = request.get_json()
|
|
if not data or 'message' not in data:
|
|
return jsonify({'error': 'No message provided'}), 400
|
|
|
|
user_message = data.get('message', '').strip()
|
|
if not user_message:
|
|
return jsonify({'error': 'Empty message'}), 400
|
|
|
|
conversation_id = data.get('conversation_id', str(uuid.uuid4()))
|
|
conversation_history = data.get('conversation_history', [])
|
|
context_data = data.get('context_data', {})
|
|
|
|
# Process message - use asyncio.run to execute async function
|
|
result = asyncio.run(agent.process_message(
|
|
user_message,
|
|
conversation_id,
|
|
conversation_history,
|
|
context_data
|
|
))
|
|
|
|
logger.info(f"Processed message for conversation {conversation_id}, Lead Score: {result['lead_score']}")
|
|
|
|
return jsonify(result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Chat endpoint error: {str(e)}\n{traceback.format_exc()}")
|
|
|
|
# Return graceful error response
|
|
return jsonify({
|
|
'response': "I apologize for the technical issue. Let me help you explore Ellucian solutions for your institution. What specific challenge are you facing?",
|
|
'error': 'Processing error',
|
|
'conversation_id': data.get('conversation_id', str(uuid.uuid4())),
|
|
'timestamp': datetime.now().isoformat()
|
|
}), 500
|
|
```
|
|
|
|
The key change is:
|
|
1. Changed `async def chat():` to `def chat():`
|
|
2. Changed `result = await agent.process_message(...)` to `result = asyncio.run(agent.process_message(...))`
|
|
|
|
This will properly execute the async code within the synchronous Flask route handler. The `asyncio.run()` function creates an event loop, runs the async function, and then closes the loop, which is perfect for calling async functions from synchronous code.
|
|
|
|
If you want to keep the async functionality throughout your application, you would need to either:
|
|
1. Switch to an async-compatible framework like Quart (Flask's async cousin)
|
|
2. Use Flask with `flask[async]` and proper async configuration
|
|
3. Use a WSGI server that supports async like Hypercorn
|
|
|
|
But for your current setup with Gunicorn and standard Flask, the synchronous route with `asyncio.run()` is the simplest and most reliable solution.
|
|
|
|
## Human
|
|
2025-09-02T18:08:21.9467867Z [2025-09-02 18:08:21 +0000] [1048] [INFO] Starting gunicorn 21.2.0
|
|
2025-09-02T18:08:21.9471919Z [2025-09-02 18:08:21 +0000] [1048] [INFO] Listening at: http://0.0.0.0:8000 (1048)
|
|
2025-09-02T18:08:21.9473031Z [2025-09-02 18:08:21 +0000] [1048] [INFO] Using worker: sync
|
|
2025-09-02T18:08:21.9502134Z [2025-09-02 18:08:21 +0000] [1049] [INFO] Booting worker with pid: 1049
|
|
2025-09-02T18:08:21.9526047Z INFO:startup:Added /tmp/8ddea4b773da1f2 to Python path
|
|
2025-09-02T18:08:21.9526224Z INFO:startup:Attempting to import Flask app...
|
|
2025-09-02T18:08:21.9650462Z ERROR:startup:Unexpected error during startup: unexpected indent (app.py, line 1342)
|
|
2025-09-02T18:08:21.9870595Z [2025-09-02 18:08:21 +0000] [1050] [INFO] Booting worker with pid: 1050
|
|
2025-09-02T18:08:21.9886777Z INFO:startup:Added /tmp/8ddea4b773da1f2 to Python path
|
|
2025-09-02T18:08:21.9893559Z INFO:startup:Attempting to import Flask app...
|
|
2025-09-02T18:08:22.008653Z ERROR:startup:Unexpected error during startup: unexpected indent (app.py, line 1342)
|
|
2025-09-02T18:08:22.0411099Z [2025-09-02 18:08:22 +0000] [1051] [INFO] Booting worker with pid: 1051
|
|
2025-09-02T18:08:22.0471096Z INFO:startup:Added /tmp/8ddea4b773da1f2 to Python path
|
|
2025-09-02T18:08:22.0471841Z INFO:startup:Attempting to import Flask app...
|
|
2025-09-02T18:08:22.0850811Z ERROR:startup:Unexpected error during startup: unexpected indent (app.py, line 1342)
|
|
2025-09-02T18:08:22.0928245Z [2025-09-02 18:08:22 +0000] [1052] [INFO] Booting worker with pid: 1052
|
|
2025-09-02T18:08:22.0971395Z INFO:startup:Added /tmp/8ddea4b773da1f2 to Python path
|
|
2025-09-02T18:08:22.099028Z INFO:startup:Attempting to import Flask app...
|
|
2025-09-02T18:08:22.1371763Z ERROR:startup:Unexpected error during startup: unexpected indent (app.py, line 1342)
|
|
2025-09-02T18:08:22.6241253Z Failed to find attribute 'app' in 'startup'.
|
|
2025-09-02T18:08:22.6250517Z Failed to find attribute 'app' in 'startup'.
|
|
2025-09-02T18:08:22.625319Z [2025-09-02 18:08:22 +0000] [1052] [INFO] Worker exiting (pid: 1052)
|
|
2025-09-02T18:08:22.6325616Z Failed to find attribute 'app' in 'startup'.
|
|
2025-09-02T18:08:22.6326038Z [2025-09-02 18:08:22 +0000] [1049] [INFO] Worker exiting (pid: 1049)
|
|
2025-09-02T18:08:22.6326072Z [2025-09-02 18:08:22 +0000] [1050] [INFO] Worker exiting (pid: 1050)
|
|
2025-09-02T18:08:22.6356048Z Failed to find attribute 'app' in 'startup'.
|
|
2025-09-02T18:08:22.6361953Z [2025-09-02 18:08:22 +0000] [1051] [INFO] Worker exiting (pid: 1051)
|
|
2025-09-02T18:08:22.7185861Z [2025-09-02 18:08:22 +0000] [1048] [ERROR] Worker (pid:1052) exited with code 4
|
|
2025-09-02T18:08:22.7197828Z [2025-09-02 18:08:22 +0000] [1048] [ERROR] Worker (pid:1049) was sent SIGTERM!
|
|
2025-09-02T18:08:22.7212522Z [2025-09-02 18:08:22 +0000] [1048] [ERROR] Worker (pid:1050) was sent SIGTERM!
|
|
2025-09-02T18:08:22.7214262Z [2025-09-02 18:08:22 +0000] [1048] [ERROR] Worker (pid:1051) was sent SIGTERM!
|
|
2025-09-02T18:08:22.8122445Z [2025-09-02 18:08:22 +0000] [1048] [ERROR] Shutting down: Master
|
|
2025-09-02T18:08:22.8122806Z [2025-09-02 18:08:22 +0000] [1048] [ERROR] Reason: App failed to load.
|
|
|
|
## Ren
|
|
The error shows there's an indentation error on line 1342 of app.py. This is preventing the app from loading at all. Let me look at the chat route section in the provided app.py file to see what might be wrong.
|
|
|
|
Looking at the app.py file around line 1366 where the chat route should be, I can see that the original code has:
|
|
|
|
```python
|
|
@app.route('/chat', methods=['POST'])
|
|
async def chat():
|
|
"""Enhanced chat endpoint with full processing"""
|
|
try:
|
|
data = request.get_json()
|
|
...
|
|
```
|
|
|
|
The error says line 1342 has an unexpected indent. This could be related to the class or function definition above it. Let me check the entire EllucianAgent class and the initialization that happens before the Flask routes.
|
|
|
|
Looking at the code structure, the issue might be that there's a problem with the indentation in the file. The user needs to fix the indentation error first before anything else will work.
|
|
|
|
Since the error is "unexpected indent (app.py, line 1342)", I should provide them with the exact section of code that needs to be fixed, ensuring proper indentation throughout.There's an indentation error in your `app.py` file on line 1342. This is preventing the app from starting. Let me provide you with the correctly indented section around the Flask routes.
|
|
|
|
The issue is likely in the transition between the `EllucianAgent` class and the Flask routes section. Here's the correct code from the end of the `EllucianAgent` class through the beginning of the Flask routes:
|
|
|
|
```python
|
|
# ... (end of EllucianAgent class) ...
|
|
|
|
def _cleanup_old_conversations(self):
|
|
"""Clean up old conversations to prevent memory issues"""
|
|
if len(self.conversations) > 100:
|
|
# Sort by last active and keep most recent 100
|
|
sorted_convs = sorted(
|
|
self.conversations.items(),
|
|
key=lambda x: x[1]['last_active'],
|
|
reverse=True
|
|
)
|
|
self.conversations = dict(sorted_convs[:100])
|
|
|
|
# Initialize global agent (NO INDENTATION - this is at module level)
|
|
agent = EllucianAgent()
|
|
|
|
# ========================================
|
|
# FLASK ROUTES (NO INDENTATION - these are at module level)
|
|
# ========================================
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Serve the enhanced chat interface"""
|
|
return render_template('chatbot.html')
|
|
|
|
@app.route('/chat', methods=['POST'])
|
|
def chat():
|
|
"""Enhanced chat endpoint with full processing"""
|
|
try:
|
|
data = request.get_json()
|
|
if not data or 'message' not in data:
|
|
return jsonify({'error': 'No message provided'}), 400
|
|
|
|
user_message = data.get('message', '').strip()
|
|
if not user_message:
|
|
return jsonify({'error': 'Empty message'}), 400
|
|
|
|
conversation_id = data.get('conversation_id', str(uuid.uuid4()))
|
|
conversation_history = data.get('conversation_history', [])
|
|
context_data = data.get('context_data', {})
|
|
|
|
# Process message - use asyncio.run to execute async function
|
|
result = asyncio.run(agent.process_message(
|
|
user_message,
|
|
conversation_id,
|
|
conversation_history,
|
|
context_data
|
|
))
|
|
|
|
logger.info(f"Processed message for conversation {conversation_id}, Lead Score: {result['lead_score']}")
|
|
|
|
return jsonify(result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Chat endpoint error: {str(e)}\n{traceback.format_exc()}")
|
|
|
|
# Return graceful error response
|
|
return jsonify({
|
|
'response': "I apologize for the technical issue. Let me help you explore Ellucian solutions for your institution. What specific challenge are you facing?",
|
|
'error': 'Processing error',
|
|
'conversation_id': data.get('conversation_id', str(uuid.uuid4())),
|
|
'timestamp': datetime.now().isoformat()
|
|
}), 500
|
|
|
|
@app.route('/health')
|
|
def health_check():
|
|
"""Comprehensive health check endpoint"""
|
|
# ... rest of the function ...
|
|
```
|
|
|
|
The key points to fix:
|
|
|
|
1. **The `agent = EllucianAgent()` line must be at module level (no indentation)**
|
|
2. **All Flask route decorators (`@app.route`) must be at module level (no indentation)**
|
|
3. **Change `async def chat():` to `def chat():`**
|
|
4. **Use `asyncio.run()` instead of `await` in the chat function**
|
|
|
|
Make sure that:
|
|
- The closing of the `EllucianAgent` class is properly indented
|
|
- The `agent = EllucianAgent()` initialization has NO indentation
|
|
- All Flask routes (`@app.route`) have NO indentation
|
|
- The functions under the routes are indented with one tab
|
|
|
|
This indentation error is preventing Python from even parsing the file, which is why the app won't start. Fix the indentation first, then apply the async-to-sync change for the chat route.
|
|
|
|
## Human
|
|
Logs:
|
|
|
|
## Ren
|
|
The error message shows:
|
|
```
|
|
ERROR:startup:Unexpected error during startup: expected an indented block after function definition on line 1342 (app.py, line 1343)
|
|
```
|
|
|
|
This indicates there's still an indentation issue. The function definition on line 1342 is missing its body or the body is not properly indented. Let me look at what should be around line 1342 in the app.py file.
|
|
|
|
Based on the structure, line 1342 is likely where the `/chat` route is defined. The error "expected an indented block after function definition" means the function definition exists but there's no indented code block following it.
|
|
|
|
The issue is likely that the function definition `def chat():` exists but the body is either missing or not indented. I need to provide the complete and properly indented chat function.The error shows there's still an indentation issue on line 1342-1343. The function definition exists but the body is missing or not properly indented. Let me provide the complete, correctly indented `/chat` route function:
|
|
|
|
```python
|
|
@app.route('/chat', methods=['POST'])
|
|
def chat():
|
|
"""Enhanced chat endpoint with full processing"""
|
|
try:
|
|
data = request.get_json()
|
|
if not data or 'message' not in data:
|
|
return jsonify({'error': 'No message provided'}), 400
|
|
|
|
user_message = data.get('message', '').strip()
|
|
if not user_message:
|
|
return jsonify({'error': 'Empty message'}), 400
|
|
|
|
conversation_id = data.get('conversation_id', str(uuid.uuid4()))
|
|
conversation_history = data.get('conversation_history', [])
|
|
context_data = data.get('context_data', {})
|
|
|
|
# Process message - use asyncio.run to execute async function
|
|
result = asyncio.run(agent.process_message(
|
|
user_message,
|
|
conversation_id,
|
|
conversation_history,
|
|
context_data
|
|
))
|
|
|
|
logger.info(f"Processed message for conversation {conversation_id}, Lead Score: {result['lead_score']}")
|
|
|
|
return jsonify(result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Chat endpoint error: {str(e)}\n{traceback.format_exc()}")
|
|
|
|
# Return graceful error response
|
|
return jsonify({
|
|
'response': "I apologize for the technical issue. Let me help you explore Ellucian solutions for your institution. What specific challenge are you facing?",
|
|
'error': 'Processing error',
|
|
'conversation_id': data.get('conversation_id', str(uuid.uuid4())),
|
|
'timestamp': datetime.now().isoformat()
|
|
}), 500
|
|
```
|
|
|
|
**Important**: Make sure that:
|
|
|
|
1. **Every line inside the function is indented with a tab (or consistent spaces)**
|
|
2. **The `try:` block is indented once from the function definition**
|
|
3. **The content inside `try:` is indented twice**
|
|
4. **The `except:` block is at the same level as `try:`**
|
|
5. **The content inside `except:` is indented twice**
|
|
|
|
If you're still getting the error, the issue might be that you have:
|
|
```python
|
|
@app.route('/chat', methods=['POST'])
|
|
def chat():
|
|
# Missing indented content here - this causes the error
|
|
```
|
|
|
|
Or you might have mixed tabs and spaces. In Python, you must use either all tabs or all spaces for indentation, not both.
|
|
|
|
To fix this:
|
|
1. Open your `app.py` file
|
|
2. Go to line 1342 (the `/chat` route)
|
|
3. Delete the entire `/chat` function
|
|
4. Paste the complete function I provided above
|
|
5. Make sure all indentation uses either tabs OR spaces consistently (not both)
|
|
|
|
If you're using VS Code or another editor, you can often see whitespace characters by enabling "Show Whitespace" to ensure consistency.
|
|
|
|
## Human
|
|
Here's the deployed code
|
|
|
|
## Ren
|
|
Looking at the code on line 1342-1343, I can see the issue:
|
|
|
|
```python
|
|
@app.route('/chat', methods=['POST'])
|
|
def chat():
|
|
"""Enhanced chat endpoint with full processing"""
|
|
try:
|
|
```
|
|
|
|
The problem is that the docstring is not indented. The function body needs to be indented, including the docstring. Let me provide the corrected version.I can see the indentation issue on line 1342-1343. The docstring and function body are not indented properly. Here's the corrected `/chat` route function:
|
|
|
|
```python
|
|
@app.route('/chat', methods=['POST'])
|
|
def chat():
|
|
"""Enhanced chat endpoint with full processing"""
|
|
try:
|
|
data = request.get_json()
|
|
if not data or 'message' not in data:
|
|
return jsonify({'error': 'No message provided'}), 400
|
|
|
|
user_message = data.get('message', '').strip()
|
|
if not user_message:
|
|
return jsonify({'error': 'Empty message'}), 400
|
|
|
|
conversation_id = data.get('conversation_id', str(uuid.uuid4()))
|
|
conversation_history = data.get('conversation_history', [])
|
|
context_data = data.get('context_data', {})
|
|
|
|
# Process message - use asyncio.run to execute async function
|
|
result = asyncio.run(agent.process_message(
|
|
user_message,
|
|
conversation_id,
|
|
conversation_history,
|
|
context_data
|
|
))
|
|
|
|
logger.info(f"Processed message for conversation {conversation_id}, Lead Score: {result['lead_score']}")
|
|
|
|
return jsonify(result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Chat endpoint error: {str(e)}\n{traceback.format_exc()}")
|
|
|
|
# Return graceful error response
|
|
return jsonify({
|
|
'response': "I apologize for the technical issue. Let me help you explore Ellucian solutions for your institution. What specific challenge are you facing?",
|
|
'error': 'Processing error',
|
|
'conversation_id': data.get('conversation_id', str(uuid.uuid4())),
|
|
'timestamp': datetime.now().isoformat()
|
|
}), 500
|
|
```
|
|
|
|
The issue in your current code is on line 1343 where the docstring starts without indentation:
|
|
```python
|
|
def chat():
|
|
"""Enhanced chat endpoint with full processing""" # ← This line has NO indentation
|
|
```
|
|
|
|
It should be:
|
|
```python
|
|
def chat():
|
|
"""Enhanced chat endpoint with full processing""" # ← This line MUST be indented
|
|
```
|
|
|
|
**To fix this:**
|
|
|
|
1. Go to line 1342 in your `app.py` file
|
|
2. Make sure line 1343 (the docstring) has a tab (or consistent spaces) at the beginning
|
|
3. Make sure the `try:` on line 1344 also has the same indentation as the docstring
|
|
4. Ensure all code inside the function is properly indented
|
|
|
|
The entire function body, including the docstring, must be indented one level from the function definition.
|
|
|
|
## Human
|
|
This is why I prefer the whole app.py file to just overwrite, even thought it takes you a while to generate. Python is very sensitive, which makes copying and pasting more error prone. At this point, it would have been faster for you to generate the whole file. Don't do it now, I've fixed it already.
|
|
|
|
## Ren
|
|
The user makes an excellent point about Python's sensitivity to indentation and the error-prone nature of copying and pasting code snippets. They're absolutely right that for Python files, especially with complex indentation structures, providing the complete file can be more reliable than providing snippets, even though it takes longer to generate.
|
|
|
|
They've already fixed the issue, so they don't want me to generate the full file now. I should acknowledge their valid feedback and keep this in mind for future Python debugging sessions.You're absolutely right, and I apologize for the frustration. Python's indentation sensitivity makes partial code snippets particularly error-prone, especially when dealing with:
|
|
|
|
1. Mixed indentation levels (classes, functions, try/except blocks)
|
|
2. Long files where context matters
|
|
3. Production environments where a single space/tab issue breaks everything
|
|
|
|
You make an excellent point - the time spent debugging indentation errors from snippets often exceeds the time it would take to generate and replace the entire file. For Python specifically, the "whole file replacement" approach is often safer and faster overall, even with the generation time.
|
|
|
|
I'll remember this for future Python debugging sessions - when dealing with indentation-sensitive issues or structural changes, providing the complete file is the better approach, despite the length. It eliminates the risk of copy-paste errors and ensures consistency throughout.
|
|
|
|
Glad you got it fixed! Is the application working now with the corrected indentation?
|