1207 lines
43 KiB
HTML
1207 lines
43 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SkyTalk - AI Interview Chat</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
background: white;
|
|
border-radius: 20px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
width: 100%;
|
|
max-width: 800px;
|
|
height: 80vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.container.fullscreen {
|
|
max-width: 95vw;
|
|
height: 95vh;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 24px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.status {
|
|
font-size: 14px;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
background: rgba(255,255,255,0.2);
|
|
margin-left: 10px;
|
|
}
|
|
|
|
.start-section {
|
|
padding: 20px;
|
|
background: #f7f7f7;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.start-form {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.start-form input {
|
|
flex: 1;
|
|
padding: 12px;
|
|
border: 2px solid #667eea;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
outline: none;
|
|
}
|
|
|
|
.start-form button {
|
|
padding: 12px 24px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.start-form button:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.start-form button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.chat-container {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
background: #fafafa;
|
|
}
|
|
|
|
.message {
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
gap: 12px;
|
|
animation: fadeIn 0.3s ease-in;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.message-avatar {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
color: white;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.message.user .message-avatar {
|
|
background: #667eea;
|
|
}
|
|
|
|
.message.assistant .message-avatar {
|
|
background: #764ba2;
|
|
}
|
|
|
|
.message-content {
|
|
flex: 1;
|
|
background: white;
|
|
padding: 12px 16px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.message-role {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #666;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.message-text {
|
|
font-size: 15px;
|
|
line-height: 1.5;
|
|
color: #333;
|
|
}
|
|
|
|
.input-section {
|
|
padding: 20px;
|
|
background: white;
|
|
border-top: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.input-form {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.input-form input {
|
|
flex: 1;
|
|
padding: 12px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.input-form input:focus {
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.input-form button {
|
|
padding: 12px 24px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.input-form button:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.input-form button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.processing-indicator {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #666;
|
|
}
|
|
|
|
.processing-indicator h3 {
|
|
margin-bottom: 20px;
|
|
color: #764ba2;
|
|
}
|
|
|
|
.spinner {
|
|
border: 3px solid #f3f3f3;
|
|
border-top: 3px solid #764ba2;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 20px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.error {
|
|
background: #fee;
|
|
color: #c00;
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.suggestions {
|
|
padding: 10px 20px;
|
|
background: #f0f7ff;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.suggestions-title {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.suggestion-chips {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.suggestion-chip {
|
|
padding: 6px 12px;
|
|
background: white;
|
|
border: 1px solid #667eea;
|
|
color: #667eea;
|
|
border-radius: 16px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.suggestion-chip:hover {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.visualization-section {
|
|
padding: 0;
|
|
background: #f9f9f9;
|
|
border-top: 1px solid #e0e0e0;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.visualization-section.fullscreen {
|
|
padding: 10px;
|
|
}
|
|
|
|
.summary-section {
|
|
padding: 20px;
|
|
background: linear-gradient(135deg, #f8f9ff 0%, #f0f7ff 100%);
|
|
border-top: 1px solid #e0e0e0;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.summary-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.summary-content {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #667eea;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
color: #444;
|
|
white-space: pre-line;
|
|
}
|
|
|
|
.viz-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
padding: 15px 20px 5px 20px;
|
|
background: white;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.viz-header.fullscreen {
|
|
padding: 10px 15px 5px 15px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.viz-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.viz-toggle {
|
|
padding: 6px 12px;
|
|
background: #667eea;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.viz-toggle:hover {
|
|
background: #764ba2;
|
|
}
|
|
|
|
#knowledgeGraph {
|
|
width: 100%;
|
|
height: 350px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
background: white;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#knowledgeGraph.fullscreen {
|
|
height: calc(100vh - 120px);
|
|
border: 1px solid #ddd;
|
|
}
|
|
|
|
.node {
|
|
position: absolute;
|
|
padding: 8px 12px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
max-width: 150px;
|
|
word-wrap: break-word;
|
|
text-align: center;
|
|
z-index: 10;
|
|
}
|
|
|
|
.node:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
z-index: 20;
|
|
}
|
|
|
|
.node.selected {
|
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.link {
|
|
position: absolute;
|
|
background: #667eea;
|
|
height: 2px;
|
|
transform-origin: left center;
|
|
z-index: 1;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.link::after {
|
|
content: '→';
|
|
position: absolute;
|
|
right: -8px;
|
|
top: -6px;
|
|
color: #667eea;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.link-label {
|
|
position: absolute;
|
|
font-size: 10px;
|
|
color: #666;
|
|
background: rgba(255,255,255,0.95);
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
white-space: nowrap;
|
|
z-index: 5;
|
|
pointer-events: auto;
|
|
cursor: pointer;
|
|
border: 1px solid #ddd;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
max-width: 200px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.link-label:hover {
|
|
background: #667eea;
|
|
color: white;
|
|
transform: scale(1.05);
|
|
z-index: 15;
|
|
white-space: normal;
|
|
max-width: 300px;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.node-tooltip {
|
|
position: absolute;
|
|
background: rgba(0,0,0,0.9);
|
|
color: white;
|
|
padding: 10px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
max-width: 250px;
|
|
z-index: 30;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.node-tooltip.visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
.tag {
|
|
display: inline-block;
|
|
background: rgba(255,255,255,0.2);
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
font-size: 10px;
|
|
margin: 2px;
|
|
}
|
|
|
|
.empty-viz {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: #999;
|
|
font-style: italic;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🌟 SkyTalk - AI Interview Chat</h1>
|
|
<div class="status">
|
|
Status: <span class="status-badge" id="status">Not Started</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="start-section" id="startSection">
|
|
<form class="start-form" id="startForm">
|
|
<input
|
|
type="text"
|
|
id="topicInput"
|
|
placeholder="Enter a topic to explore (e.g., 'AI ethics in healthcare')"
|
|
required
|
|
>
|
|
<button type="submit" id="startButton">Start Interview</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="suggestions hidden" id="suggestions">
|
|
<div class="suggestions-title">Suggested topics:</div>
|
|
<div class="suggestion-chips">
|
|
<div class="suggestion-chip" onclick="setTopic('AI ethics in healthcare')">AI Ethics in Healthcare</div>
|
|
<div class="suggestion-chip" onclick="setTopic('Future of remote work')">Future of Remote Work</div>
|
|
<div class="suggestion-chip" onclick="setTopic('Climate change solutions')">Climate Change Solutions</div>
|
|
<div class="suggestion-chip" onclick="setTopic('Space exploration')">Space Exploration</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-container" id="chatContainer">
|
|
<!-- Messages will appear here -->
|
|
</div>
|
|
|
|
<div class="input-section hidden" id="inputSection">
|
|
<form class="input-form" id="messageForm">
|
|
<input
|
|
type="text"
|
|
id="messageInput"
|
|
placeholder="Type your response..."
|
|
required
|
|
>
|
|
<button type="submit" id="sendButton">Send</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="summary-section hidden" id="summarySection">
|
|
<div class="summary-title">
|
|
📝 Session Summary
|
|
</div>
|
|
<div class="summary-content" id="summaryContent">
|
|
Loading summary...
|
|
</div>
|
|
</div>
|
|
|
|
<div class="visualization-section hidden" id="visualizationSection">
|
|
<div class="viz-header">
|
|
<div class="viz-title">📊 Knowledge Graph</div>
|
|
<button class="viz-toggle" onclick="toggleVisualization()">Hide Graph</button>
|
|
</div>
|
|
<div id="knowledgeGraph">
|
|
<div class="empty-viz">Knowledge graph will appear here after synthesis...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_URL = window.location.origin;
|
|
let currentSessionId = null;
|
|
let sessionStatus = 'not_started';
|
|
let processingInterval = null;
|
|
let sessionData = null;
|
|
let graphVisible = false;
|
|
let viewOnlyMode = false;
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// Check if we're viewing a specific session
|
|
const pathMatch = window.location.pathname.match(/\/session\/([a-f0-9-]+)/);
|
|
if (pathMatch) {
|
|
currentSessionId = pathMatch[1];
|
|
viewOnlyMode = true;
|
|
await initViewOnlyMode();
|
|
} else {
|
|
// Normal mode - show start interface
|
|
document.getElementById('suggestions').classList.remove('hidden');
|
|
|
|
// Add event listeners
|
|
document.getElementById('startForm').addEventListener('submit', startSession);
|
|
const messageForm = document.getElementById('messageForm');
|
|
if (messageForm) {
|
|
messageForm.addEventListener('submit', sendMessage);
|
|
}
|
|
}
|
|
});
|
|
|
|
function setTopic(topic) {
|
|
document.getElementById('topicInput').value = topic;
|
|
document.getElementById('topicInput').focus();
|
|
}
|
|
|
|
async function updateStatus(status) {
|
|
sessionStatus = status;
|
|
const statusEl = document.getElementById('status');
|
|
statusEl.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
|
|
|
// Update UI based on status
|
|
if (status === 'active') {
|
|
document.getElementById('inputSection').classList.remove('hidden');
|
|
document.getElementById('messageInput').focus();
|
|
} else if (status === 'processing') {
|
|
document.getElementById('inputSection').classList.add('hidden');
|
|
showProcessingIndicator();
|
|
startStatusPolling();
|
|
} else if (status === 'completed') {
|
|
document.getElementById('inputSection').classList.add('hidden');
|
|
hideProcessingIndicator();
|
|
showCompletionMessage();
|
|
await loadSessionData();
|
|
showVisualization();
|
|
}
|
|
}
|
|
|
|
function addMessage(role, content) {
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = `message ${role}`;
|
|
|
|
const avatarText = role === 'user' ? 'U' : 'AI';
|
|
const roleText = role === 'user' ? 'You' : 'AI Interviewer';
|
|
|
|
messageDiv.innerHTML = `
|
|
<div class="message-avatar">${avatarText}</div>
|
|
<div class="message-content">
|
|
<div class="message-role">${roleText}</div>
|
|
<div class="message-text">${content}</div>
|
|
</div>
|
|
`;
|
|
|
|
chatContainer.appendChild(messageDiv);
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
}
|
|
|
|
function showError(message) {
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'error';
|
|
errorDiv.textContent = `Error: ${message}`;
|
|
chatContainer.appendChild(errorDiv);
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
}
|
|
|
|
function showProcessingIndicator() {
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
const processingDiv = document.createElement('div');
|
|
processingDiv.id = 'processingIndicator';
|
|
processingDiv.className = 'processing-indicator';
|
|
processingDiv.innerHTML = `
|
|
<h3>🔄 Session Ended - Processing Your Conversation</h3>
|
|
<div class="spinner"></div>
|
|
<p>The AI is synthesizing your discussion into structured notes...</p>
|
|
<p style="font-size: 14px; margin-top: 10px; opacity: 0.7;">This may take 10-30 seconds</p>
|
|
`;
|
|
chatContainer.appendChild(processingDiv);
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
}
|
|
|
|
function hideProcessingIndicator() {
|
|
const indicator = document.getElementById('processingIndicator');
|
|
if (indicator) {
|
|
indicator.remove();
|
|
}
|
|
}
|
|
|
|
function showCompletionMessage() {
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
const completionDiv = document.createElement('div');
|
|
completionDiv.className = 'processing-indicator';
|
|
completionDiv.innerHTML = `
|
|
<h3>✅ Synthesis Complete!</h3>
|
|
<p>Your conversation has been processed and stored as structured knowledge.</p>
|
|
<p style="margin-top: 20px;">
|
|
<button onclick="location.reload()" style="padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 8px; cursor: pointer;">
|
|
Start New Interview
|
|
</button>
|
|
</p>
|
|
`;
|
|
chatContainer.appendChild(completionDiv);
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
}
|
|
|
|
async function startSession(event) {
|
|
event.preventDefault();
|
|
|
|
const topic = document.getElementById('topicInput').value.trim();
|
|
if (!topic) return;
|
|
|
|
const startButton = document.getElementById('startButton');
|
|
startButton.disabled = true;
|
|
startButton.textContent = 'Starting...';
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/sessions/start`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ topic }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
currentSessionId = data.session_id;
|
|
|
|
// Hide start section and suggestions
|
|
document.getElementById('startSection').style.display = 'none';
|
|
document.getElementById('suggestions').classList.add('hidden');
|
|
|
|
// Add initial messages
|
|
addMessage('user', `I want to explore the topic: ${topic}`);
|
|
addMessage('assistant', data.message);
|
|
|
|
await updateStatus(data.status);
|
|
|
|
} catch (error) {
|
|
showError(`Failed to start session: ${error.message}`);
|
|
startButton.disabled = false;
|
|
startButton.textContent = 'Start Interview';
|
|
}
|
|
}
|
|
|
|
async function sendMessage(event) {
|
|
event.preventDefault();
|
|
|
|
const messageInput = document.getElementById('messageInput');
|
|
const message = messageInput.value.trim();
|
|
if (!message || !currentSessionId) return;
|
|
|
|
const sendButton = document.getElementById('sendButton');
|
|
sendButton.disabled = true;
|
|
messageInput.disabled = true;
|
|
|
|
// Add user message immediately
|
|
addMessage('user', message);
|
|
messageInput.value = '';
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/sessions/sendMessage`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
session_id: currentSessionId,
|
|
message: message,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Add AI response
|
|
addMessage('assistant', data.message);
|
|
|
|
// Update status
|
|
await updateStatus(data.status);
|
|
|
|
// Re-enable input if still active
|
|
if (data.status === 'active') {
|
|
sendButton.disabled = false;
|
|
messageInput.disabled = false;
|
|
messageInput.focus();
|
|
}
|
|
|
|
} catch (error) {
|
|
showError(`Failed to send message: ${error.message}`);
|
|
sendButton.disabled = false;
|
|
messageInput.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function checkSessionStatus() {
|
|
if (!currentSessionId) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/sessions/getStatus?session_id=${currentSessionId}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'completed') {
|
|
clearInterval(processingInterval);
|
|
await updateStatus('completed');
|
|
} else if (data.status === 'failed') {
|
|
clearInterval(processingInterval);
|
|
showError('Session processing failed');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to check status:', error);
|
|
}
|
|
}
|
|
|
|
function startStatusPolling() {
|
|
// Check status every 3 seconds
|
|
processingInterval = setInterval(checkSessionStatus, 3000);
|
|
}
|
|
|
|
async function loadSessionData() {
|
|
if (!currentSessionId) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/sessions/getData?session_id=${currentSessionId}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
sessionData = await response.json();
|
|
console.log('Session data loaded:', sessionData);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load session data:', error);
|
|
}
|
|
}
|
|
|
|
function showVisualization() {
|
|
if (!sessionData || !sessionData.notes || sessionData.notes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// For view-only mode, optimize for fullscreen
|
|
if (viewOnlyMode) {
|
|
document.getElementById('visualizationSection').classList.add('fullscreen');
|
|
document.querySelector('.viz-header').classList.add('fullscreen');
|
|
document.getElementById('knowledgeGraph').classList.add('fullscreen');
|
|
|
|
// Add summary to header instead of separate section
|
|
if (sessionData.summary) {
|
|
const summaryText = sessionData.summary.split('\n\n')[0] + ' • ' +
|
|
sessionData.notes.length + ' concepts • ' +
|
|
getTotalLinks() + ' connections';
|
|
document.querySelector('.viz-title').innerHTML =
|
|
`📊 ${summaryText}`;
|
|
}
|
|
} else {
|
|
// Show summary section for normal mode
|
|
if (sessionData.summary) {
|
|
document.getElementById('summarySection').classList.remove('hidden');
|
|
document.getElementById('summaryContent').textContent = sessionData.summary;
|
|
}
|
|
}
|
|
|
|
document.getElementById('visualizationSection').classList.remove('hidden');
|
|
graphVisible = true;
|
|
|
|
// Delay rendering to ensure layout is complete
|
|
setTimeout(renderKnowledgeGraph, 100);
|
|
}
|
|
|
|
function toggleVisualization() {
|
|
const vizSection = document.getElementById('visualizationSection');
|
|
const summarySection = document.getElementById('summarySection');
|
|
const toggleBtn = document.querySelector('.viz-toggle');
|
|
|
|
if (graphVisible) {
|
|
vizSection.classList.add('hidden');
|
|
summarySection.classList.add('hidden');
|
|
toggleBtn.textContent = 'Show Graph';
|
|
graphVisible = false;
|
|
} else {
|
|
vizSection.classList.remove('hidden');
|
|
if (sessionData && sessionData.summary) {
|
|
summarySection.classList.remove('hidden');
|
|
}
|
|
toggleBtn.textContent = 'Hide Graph';
|
|
graphVisible = true;
|
|
if (sessionData) renderKnowledgeGraph();
|
|
}
|
|
}
|
|
|
|
function renderKnowledgeGraph() {
|
|
if (!sessionData || !sessionData.notes) return;
|
|
|
|
const container = document.getElementById('knowledgeGraph');
|
|
container.innerHTML = '';
|
|
|
|
const notes = sessionData.notes;
|
|
const containerWidth = container.offsetWidth;
|
|
const containerHeight = container.offsetHeight;
|
|
|
|
// Position nodes in a circle or grid
|
|
const positions = calculateNodePositions(notes.length, containerWidth, containerHeight);
|
|
|
|
// Create nodes
|
|
notes.forEach((note, index) => {
|
|
const nodeEl = createNodeElement(note, positions[index]);
|
|
container.appendChild(nodeEl);
|
|
});
|
|
|
|
// Create links
|
|
notes.forEach(note => {
|
|
note.links.forEach(link => {
|
|
const linkEl = createLinkElement(note, link, notes, container);
|
|
if (linkEl) container.appendChild(linkEl);
|
|
});
|
|
});
|
|
|
|
// Create tooltip
|
|
const tooltip = document.createElement('div');
|
|
tooltip.className = 'node-tooltip';
|
|
container.appendChild(tooltip);
|
|
}
|
|
|
|
function calculateNodePositions(nodeCount, width, height) {
|
|
const positions = [];
|
|
const padding = Math.min(width * 0.1, height * 0.1, 100); // Responsive padding
|
|
const usableWidth = width - padding * 2;
|
|
const usableHeight = height - padding * 2;
|
|
|
|
if (nodeCount <= 4) {
|
|
// Optimized grid layout for small numbers
|
|
const cols = nodeCount === 1 ? 1 : nodeCount === 2 ? 2 : nodeCount === 3 ? 3 : 2;
|
|
const rows = Math.ceil(nodeCount / cols);
|
|
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
const col = i % cols;
|
|
const row = Math.floor(i / cols);
|
|
|
|
// Center the layout if there are fewer items than columns
|
|
const actualCols = nodeCount < cols ? nodeCount : cols;
|
|
const xOffset = (cols - actualCols) * (usableWidth / (cols - 1 || 1)) / 2;
|
|
|
|
positions.push({
|
|
x: padding + xOffset + (col * usableWidth / (cols - 1 || 1)),
|
|
y: padding + (row * usableHeight / (rows - 1 || 1))
|
|
});
|
|
}
|
|
} else {
|
|
// Enhanced circular layout for larger numbers
|
|
const centerX = width / 2;
|
|
const centerY = height / 2;
|
|
const radius = Math.min(usableWidth, usableHeight) / 2.5; // Slightly smaller radius for better spacing
|
|
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
const angle = (i * 2 * Math.PI) / nodeCount - Math.PI / 2; // Start from top
|
|
positions.push({
|
|
x: centerX + radius * Math.cos(angle) - 75, // Offset for node width
|
|
y: centerY + radius * Math.sin(angle) - 20 // Offset for node height
|
|
});
|
|
}
|
|
}
|
|
|
|
return positions;
|
|
}
|
|
|
|
function createNodeElement(note, position) {
|
|
const nodeEl = document.createElement('div');
|
|
nodeEl.className = 'node';
|
|
nodeEl.style.left = `${position.x}px`;
|
|
nodeEl.style.top = `${position.y}px`;
|
|
nodeEl.textContent = note.title;
|
|
nodeEl.dataset.noteId = note.id;
|
|
|
|
// Add click handler
|
|
nodeEl.addEventListener('click', () => selectNode(nodeEl));
|
|
|
|
// Add hover handlers for tooltip
|
|
nodeEl.addEventListener('mouseenter', (e) => showTooltip(e, note));
|
|
nodeEl.addEventListener('mouseleave', hideTooltip);
|
|
|
|
return nodeEl;
|
|
}
|
|
|
|
function createLinkElement(sourceNote, link, allNotes, container) {
|
|
const targetNote = allNotes.find(n => n.id === link.target_note_id);
|
|
if (!targetNote) {
|
|
console.warn('Target note not found:', link.target_note_id);
|
|
return null;
|
|
}
|
|
|
|
const sourceEl = container.querySelector(`[data-note-id="${sourceNote.id}"]`);
|
|
const targetEl = container.querySelector(`[data-note-id="${targetNote.id}"]`);
|
|
|
|
if (!sourceEl || !targetEl) return null;
|
|
|
|
const sourceRect = {
|
|
x: parseInt(sourceEl.style.left) + sourceEl.offsetWidth / 2,
|
|
y: parseInt(sourceEl.style.top) + sourceEl.offsetHeight / 2
|
|
};
|
|
|
|
const targetRect = {
|
|
x: parseInt(targetEl.style.left) + targetEl.offsetWidth / 2,
|
|
y: parseInt(targetEl.style.top) + targetEl.offsetHeight / 2
|
|
};
|
|
|
|
const linkEl = document.createElement('div');
|
|
linkEl.className = 'link';
|
|
|
|
const dx = targetRect.x - sourceRect.x;
|
|
const dy = targetRect.y - sourceRect.y;
|
|
const length = Math.sqrt(dx * dx + dy * dy);
|
|
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
|
|
linkEl.style.left = `${sourceRect.x}px`;
|
|
linkEl.style.top = `${sourceRect.y}px`;
|
|
linkEl.style.width = `${length}px`;
|
|
linkEl.style.transform = `rotate(${angle}deg)`;
|
|
|
|
// Add relationship label
|
|
const labelEl = document.createElement('div');
|
|
labelEl.className = 'link-label';
|
|
labelEl.textContent = link.relationship.substring(0, 50) + (link.relationship.length > 50 ? '...' : '');
|
|
labelEl.title = link.relationship; // Full text on hover
|
|
labelEl.style.left = `${sourceRect.x + dx/2 - 100}px`;
|
|
labelEl.style.top = `${sourceRect.y + dy/2 - 15}px`;
|
|
|
|
// Add click handler to show full relationship
|
|
labelEl.addEventListener('click', () => {
|
|
showConnectionDetails(sourceNote, targetNote, link);
|
|
});
|
|
|
|
container.appendChild(labelEl);
|
|
|
|
return linkEl;
|
|
}
|
|
|
|
function selectNode(nodeEl) {
|
|
// Remove previous selection
|
|
document.querySelectorAll('.node').forEach(n => n.classList.remove('selected'));
|
|
|
|
// Select current node
|
|
nodeEl.classList.add('selected');
|
|
}
|
|
|
|
function showTooltip(event, note) {
|
|
const tooltip = document.querySelector('.node-tooltip');
|
|
const tagsHtml = note.tags.map(tag => `<span class="tag">${tag}</span>`).join('');
|
|
|
|
tooltip.innerHTML = `
|
|
<strong>${note.title}</strong><br>
|
|
<div style="margin: 8px 0; font-size: 11px; line-height: 1.4;">
|
|
${note.content.substring(0, 150)}${note.content.length > 150 ? '...' : ''}
|
|
</div>
|
|
<div>${tagsHtml}</div>
|
|
`;
|
|
|
|
tooltip.style.left = `${event.pageX + 10}px`;
|
|
tooltip.style.top = `${event.pageY - 10}px`;
|
|
tooltip.classList.add('visible');
|
|
}
|
|
|
|
function hideTooltip() {
|
|
const tooltip = document.querySelector('.node-tooltip');
|
|
if (tooltip) {
|
|
tooltip.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
function showConnectionDetails(sourceNote, targetNote, link) {
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
|
|
// Remove any existing connection details
|
|
const existing = document.querySelector('.connection-details');
|
|
if (existing) existing.remove();
|
|
|
|
const detailsDiv = document.createElement('div');
|
|
detailsDiv.className = 'connection-details';
|
|
detailsDiv.style.cssText = `
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
max-width: 500px;
|
|
z-index: 100;
|
|
border: 2px solid #667eea;
|
|
`;
|
|
|
|
detailsDiv.innerHTML = `
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
|
<h3 style="color: #667eea; margin: 0;">🔗 Connection Details</h3>
|
|
<button onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; font-size: 20px; cursor: pointer; color: #999;">×</button>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 15px;">
|
|
<strong style="color: #764ba2;">From:</strong> ${sourceNote.title}
|
|
</div>
|
|
|
|
<div style="margin-bottom: 15px;">
|
|
<strong style="color: #764ba2;">To:</strong> ${targetNote.title}
|
|
</div>
|
|
|
|
<div style="margin-bottom: 15px;">
|
|
<strong style="color: #764ba2;">Relationship:</strong>
|
|
<div style="background: #f8f9ff; padding: 10px; border-radius: 6px; margin-top: 5px; font-style: italic; line-height: 1.5;">
|
|
${link.relationship}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: center; margin-top: 20px;">
|
|
<button onclick="this.parentElement.parentElement.remove()" style="padding: 8px 16px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer;">Close</button>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(detailsDiv);
|
|
|
|
// Add overlay
|
|
const overlay = document.createElement('div');
|
|
overlay.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.5);
|
|
z-index: 99;
|
|
`;
|
|
overlay.addEventListener('click', () => {
|
|
detailsDiv.remove();
|
|
overlay.remove();
|
|
});
|
|
|
|
document.body.appendChild(overlay);
|
|
}
|
|
|
|
function getTotalLinks() {
|
|
if (!sessionData || !sessionData.notes) return 0;
|
|
return sessionData.notes.reduce((total, note) => total + note.links.length, 0);
|
|
}
|
|
|
|
async function initViewOnlyMode() {
|
|
// Hide start section and suggestions
|
|
document.getElementById('startSection').style.display = 'none';
|
|
document.getElementById('suggestions').classList.add('hidden');
|
|
document.getElementById('inputSection').classList.add('hidden');
|
|
|
|
// Enable fullscreen mode for visualization
|
|
document.querySelector('.container').classList.add('fullscreen');
|
|
|
|
// Update header for view-only mode
|
|
const statusEl = document.getElementById('status');
|
|
statusEl.textContent = 'Loading Session...';
|
|
|
|
try {
|
|
// Load session status first
|
|
const statusResponse = await fetch(`${API_URL}/sessions/getStatus?session_id=${currentSessionId}`);
|
|
if (!statusResponse.ok) {
|
|
throw new Error(`Session not found: ${statusResponse.status}`);
|
|
}
|
|
|
|
const statusData = await statusResponse.json();
|
|
sessionStatus = statusData.status;
|
|
|
|
// Update status display
|
|
statusEl.textContent = `${statusData.status.charAt(0).toUpperCase() + statusData.status.slice(1)} (${statusData.notes_count} notes)`;
|
|
|
|
// Load session data for visualization
|
|
await loadSessionData();
|
|
|
|
if (sessionData && sessionData.notes && sessionData.notes.length > 0) {
|
|
showVisualization();
|
|
// Add info message
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
const infoDiv = document.createElement('div');
|
|
infoDiv.className = 'processing-indicator';
|
|
infoDiv.innerHTML = `
|
|
<h3>📊 Session Visualization</h3>
|
|
<p>Viewing knowledge graph for session created on ${new Date(sessionData.created_at).toLocaleDateString()}</p>
|
|
<p style="margin-top: 10px; font-size: 14px; opacity: 0.8;">
|
|
This session contains ${sessionData.notes.length} synthesized notes.
|
|
</p>
|
|
`;
|
|
chatContainer.appendChild(infoDiv);
|
|
} else {
|
|
// Show message for sessions without visualization data
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = 'processing-indicator';
|
|
messageDiv.innerHTML = `
|
|
<h3>⚠️ No Visualization Available</h3>
|
|
<p>This session doesn't have any synthesized notes yet.</p>
|
|
<p style="margin-top: 10px; font-size: 14px; opacity: 0.8;">
|
|
Session Status: ${statusData.status}
|
|
</p>
|
|
`;
|
|
chatContainer.appendChild(messageDiv);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading session:', error);
|
|
const statusEl = document.getElementById('status');
|
|
statusEl.textContent = 'Error Loading Session';
|
|
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'error';
|
|
errorDiv.textContent = `Failed to load session: ${error.message}`;
|
|
chatContainer.appendChild(errorDiv);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |