Files
app/gui.html
2025-08-17 04:00:21 +00:00

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;">&times;</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>