Class_generator/server.js
StillHammer 05142bdfbc Implement comprehensive AI text report/export system
- Add AIReportSystem.js for detailed AI response capture and report generation
- Add AIReportInterface.js UI component for report access and export
- Integrate AI reporting into LLMValidator and SmartPreviewOrchestrator
- Add missing modules to Application.js configuration (unifiedDRS, smartPreviewOrchestrator)
- Create missing content/chapters/sbs.json for book metadata
- Enhance Application.js with debug logging for module loading
- Add multi-format export capabilities (text, HTML, JSON)
- Implement automatic learning insights extraction from AI feedback
- Add session management and performance tracking for AI reports

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 21:24:13 +08:00

807 lines
29 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Development Server - Simple HTTP server for local development
* Handles static files, CORS, and development features
*/
import { createServer } from 'http';
import { readFile, writeFile, stat, readdir, mkdir } from 'fs/promises';
import { join, extname } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { existsSync } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PORT = process.env.PORT || 8080;
const HOST = process.env.HOST || 'localhost';
// MIME types for different file extensions
const MIME_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.mp3': 'audio/mpeg',
'.wav': 'audio/wav',
'.mp4': 'video/mp4'
};
const server = createServer(async (req, res) => {
try {
// Parse URL and remove query parameters
const urlPath = new URL(req.url, `http://${req.headers.host}`).pathname;
// Default to index.html for root requests
const filePath = urlPath === '/' ? 'index.html' : urlPath.slice(1);
const fullPath = join(__dirname, filePath);
console.log(`${new Date().toISOString()} - ${req.method} ${urlPath}`);
// Legacy API endpoint to get all available books (deprecated - use ContentLoader)
if (urlPath === '/api/books') {
console.warn('⚠️ /api/books is deprecated. Use ContentLoader.loadBooks() instead');
return await handleBooksAPI(res);
}
// API endpoint for LLM configuration (IAEngine)
if (urlPath === '/api/llm-config') {
return await handleLLMConfigAPI(req, res);
}
// Progress API endpoints (DRS and Flashcards only)
if (urlPath === '/api/progress/save') {
return await handleProgressSave(req, res);
}
// Data merge endpoint for combining local and external sources
if (urlPath === '/api/progress/merge') {
return await handleProgressMerge(req, res);
}
// Sync status endpoint
if (urlPath === '/api/progress/sync-status') {
return await handleSyncStatus(req, res);
}
// DRS progress load: /api/progress/load/drs/bookId/chapterId
const drsLoadMatch = urlPath.match(/^\/api\/progress\/load\/drs\/([^\/]+)\/([^\/]+)$/);
if (drsLoadMatch) {
const [, bookId, chapterId] = drsLoadMatch;
return await handleProgressLoad(req, res, 'drs', bookId, chapterId);
}
// Flashcards progress load: /api/progress/load/flashcards
if (urlPath === '/api/progress/load/flashcards') {
return await handleProgressLoad(req, res, 'flashcards');
}
// Set CORS headers for all requests
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Disable caching completely for development
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.setHeader('Surrogate-Control', 'no-store');
// Handle preflight requests
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
// Check if file exists
try {
const stats = await stat(fullPath);
if (stats.isDirectory()) {
// Try to serve index.html from directory
const indexPath = join(fullPath, 'index.html');
try {
await stat(indexPath);
return serveFile(indexPath, res);
} catch {
return send404(res, `Directory listing not allowed for ${urlPath}`);
}
}
return serveFile(fullPath, res);
} catch (error) {
if (error.code === 'ENOENT') {
return send404(res, `File not found: ${urlPath}`);
}
throw error;
}
} catch (error) {
console.error('Server error:', error);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
}
});
async function handleBooksAPI(res) {
try {
const booksDir = join(__dirname, 'content', 'books');
const files = await readdir(booksDir);
const jsonFiles = files.filter(file => file.endsWith('.json'));
const books = [];
for (const file of jsonFiles) {
try {
const filePath = join(booksDir, file);
const content = await readFile(filePath, 'utf8');
const data = JSON.parse(content);
books.push({
id: data.id,
name: data.name,
description: data.description,
difficulty: data.difficulty,
language: data.language,
chapters: data.chapters || []
});
} catch (error) {
console.error(`Error reading ${file}:`, error);
}
}
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end(JSON.stringify(books, null, 2));
console.log(` ✅ Served API books list (${books.length} books)`);
} catch (error) {
console.error('Error in books API:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to load books' }));
}
}
async function handleChaptersAPI(res, bookId) {
try {
const booksDir = join(__dirname, 'content', 'books');
const bookPath = join(booksDir, `${bookId}.json`);
const content = await readFile(bookPath, 'utf8');
const bookData = JSON.parse(content);
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end(JSON.stringify(bookData.chapters || [], null, 2));
console.log(` ✅ Served API chapters for book ${bookId} (${bookData.chapters?.length || 0} chapters)`);
} catch (error) {
console.error(`Error in chapters API for ${bookId}:`, error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to load chapters' }));
}
}
async function handleChapterContentAPI(res, chapterId) {
try {
const chaptersDir = join(__dirname, 'content', 'chapters');
const chapterPath = join(chaptersDir, `${chapterId}.json`);
const content = await readFile(chapterPath, 'utf8');
const chapterData = JSON.parse(content);
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end(JSON.stringify(chapterData, null, 2));
console.log(` ✅ Served API content for chapter ${chapterId}`);
} catch (error) {
console.error(`Error in chapter content API for ${chapterId}:`, error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to load chapter content' }));
}
}
async function serveFile(filePath, res) {
try {
const ext = extname(filePath).toLowerCase();
const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
// Set content type
res.setHeader('Content-Type', mimeType);
// Set cache headers for static assets
if (['.css', '.png', '.jpg', '.gif', '.svg', '.ico', '.woff', '.woff2'].includes(ext)) {
res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour
} else {
res.setHeader('Cache-Control', 'no-cache'); // No cache for HTML and JS files (development)
}
// Add security headers
if (ext === '.js') {
res.setHeader('X-Content-Type-Options', 'nosniff');
}
// Read and serve file
const content = await readFile(filePath);
res.writeHead(200);
res.end(content);
console.log(` ✅ Served ${filePath} (${content.length} bytes, ${mimeType})`);
} catch (error) {
console.error(`Error serving file ${filePath}:`, error);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error reading file');
}
}
function send404(res, message = 'Not Found') {
const html404 = `
<!DOCTYPE html>
<html>
<head>
<title>404 - Not Found</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
}
h1 { font-size: 3rem; margin-bottom: 1rem; }
p { font-size: 1.2rem; opacity: 0.8; }
a { color: white; text-decoration: underline; }
</style>
</head>
<body>
<h1>404</h1>
<p>${message}</p>
<p><a href="/">← Back to Class Generator</a></p>
</body>
</html>
`;
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end(html404);
console.log(` ❌ 404: ${message}`);
}
// Progress storage functions for DRS and Flashcards
async function handleProgressSave(req, res) {
try {
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
// Read request body
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', async () => {
try {
const { system, bookId, chapterId, progressData } = JSON.parse(body);
// Validate system (only DRS and Flashcards allowed)
if (!['drs', 'flashcards'].includes(system)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid system. Only "drs" and "flashcards" allowed' }));
return;
}
// Create saves directory if it doesn't exist
const savesDir = join(__dirname, 'saves');
if (!existsSync(savesDir)) {
await mkdir(savesDir, { recursive: true });
}
// Create filename based on system and identifiers
const filename = system === 'drs'
? `${system}-progress-${bookId}-${chapterId}.json`
: `${system}-progress.json`;
const filePath = join(savesDir, filename);
// Add metadata
const saveData = {
...progressData,
system,
bookId: system === 'drs' ? bookId : undefined,
chapterId: system === 'drs' ? chapterId : undefined,
savedAt: new Date().toISOString(),
version: '1.0'
};
await writeFile(filePath, JSON.stringify(saveData, null, 2));
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end(JSON.stringify({
success: true,
filename,
savedAt: saveData.savedAt
}));
console.log(` ✅ Saved ${system} progress: ${filename}`);
} catch (parseError) {
console.error('Error parsing progress save request:', parseError);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON data' }));
}
});
} catch (error) {
console.error('Error in progress save API:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to save progress' }));
}
}
async function handleProgressLoad(req, res, system, bookId, chapterId) {
try {
if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
// Validate system
if (!['drs', 'flashcards'].includes(system)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid system' }));
return;
}
const filename = system === 'drs'
? `${system}-progress-${bookId}-${chapterId}.json`
: `${system}-progress.json`;
const filePath = join(__dirname, 'saves', filename);
try {
const content = await readFile(filePath, 'utf8');
const progressData = JSON.parse(content);
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end(JSON.stringify(progressData));
console.log(` ✅ Loaded ${system} progress: ${filename}`);
} catch (fileError) {
// File doesn't exist - return empty progress
const emptyProgress = system === 'drs' ? {
masteredVocabulary: [],
masteredPhrases: [],
masteredGrammar: [],
completed: false,
masteryCount: 0,
system,
bookId,
chapterId
} : {
system: 'flashcards',
progress: {},
stats: {}
};
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end(JSON.stringify(emptyProgress));
console.log(` No saved progress found for ${system}, returning empty`);
}
} catch (error) {
console.error('Error in progress load API:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to load progress' }));
}
}
// API handler for LLM configuration
async function handleLLMConfigAPI(req, res) {
try {
// Only allow GET requests
if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
// Load environment variables
const { config } = await import('dotenv');
config();
// Extract only the LLM API keys
const llmConfig = {
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
MISTRAL_API_KEY: process.env.MISTRAL_API_KEY,
GEMINI_API_KEY: process.env.GEMINI_API_KEY
};
// Filter out undefined keys
const validKeys = Object.fromEntries(
Object.entries(llmConfig).filter(([key, value]) => value && value.length > 0)
);
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end(JSON.stringify(validKeys));
console.log(` ✅ Served LLM config with ${Object.keys(validKeys).length} API keys`);
} catch (error) {
console.error('Error in LLM config API:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to load LLM configuration' }));
}
}
// Data merge handler for combining local and external progress
async function handleProgressMerge(req, res) {
try {
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
// Read request body
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', async () => {
try {
const { system, bookId, chapterId, localData, externalData, mergeStrategy = 'timestamp' } = JSON.parse(body);
// Validate system
if (!['drs', 'flashcards'].includes(system)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid system. Only "drs" and "flashcards" allowed' }));
return;
}
// Perform data merge
const mergedData = await mergeProgressData(localData, externalData, mergeStrategy);
// Add merge metadata
mergedData.mergeInfo = {
strategy: mergeStrategy,
mergedAt: new Date().toISOString(),
localItems: countProgressItems(localData),
externalItems: countProgressItems(externalData),
totalItems: countProgressItems(mergedData),
conflicts: mergedData.conflicts || []
};
// Save merged data
const savesDir = join(__dirname, 'saves');
if (!existsSync(savesDir)) {
await mkdir(savesDir, { recursive: true });
}
const filename = system === 'drs'
? `${system}-progress-${bookId}-${chapterId}.json`
: `${system}-progress.json`;
const filePath = join(savesDir, filename);
await writeFile(filePath, JSON.stringify(mergedData, null, 2));
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end(JSON.stringify({
success: true,
mergedData,
mergeInfo: mergedData.mergeInfo
}));
console.log(` ✅ Merged ${system} progress: ${mergedData.mergeInfo.totalItems} total items`);
} catch (parseError) {
console.error('Error parsing progress merge request:', parseError);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON data' }));
}
});
} catch (error) {
console.error('Error in progress merge API:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to merge progress' }));
}
}
// Sync status handler
async function handleSyncStatus(req, res) {
try {
if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
const savesDir = join(__dirname, 'saves');
const syncStatus = {
savesDirectory: savesDir,
lastSync: null,
savedFiles: [],
totalFiles: 0
};
try {
if (existsSync(savesDir)) {
const files = await readdir(savesDir);
const jsonFiles = files.filter(file => file.endsWith('.json'));
syncStatus.totalFiles = jsonFiles.length;
for (const file of jsonFiles) {
try {
const filePath = join(savesDir, file);
const stats = await stat(filePath);
const content = await readFile(filePath, 'utf8');
const data = JSON.parse(content);
syncStatus.savedFiles.push({
filename: file,
lastModified: stats.mtime.toISOString(),
savedAt: data.savedAt || stats.mtime.toISOString(),
system: data.system || 'unknown',
bookId: data.bookId,
chapterId: data.chapterId,
hasTimestamps: hasTimestampData(data),
itemCount: countProgressItems(data)
});
// Update last sync time to most recent file
if (!syncStatus.lastSync || stats.mtime > new Date(syncStatus.lastSync)) {
syncStatus.lastSync = stats.mtime.toISOString();
}
} catch (fileError) {
console.warn(`Error reading file ${file}:`, fileError);
}
}
}
} catch (dirError) {
console.warn('Saves directory not accessible:', dirError);
}
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.end(JSON.stringify(syncStatus));
console.log(` ✅ Sync status: ${syncStatus.totalFiles} files`);
} catch (error) {
console.error('Error in sync status API:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to get sync status' }));
}
}
// Data merge utilities
async function mergeProgressData(localData, externalData, strategy = 'timestamp') {
const merged = {
masteredVocabulary: [],
masteredPhrases: [],
masteredGrammar: [],
completed: false,
masteryCount: 0,
conflicts: []
};
// Copy metadata from most recent source
const localTimestamp = localData?.savedAt || localData?.lastModified || '1970-01-01T00:00:00.000Z';
const externalTimestamp = externalData?.savedAt || externalData?.lastModified || '1970-01-01T00:00:00.000Z';
const useExternal = new Date(externalTimestamp) > new Date(localTimestamp);
const primarySource = useExternal ? externalData : localData;
const secondarySource = useExternal ? localData : externalData;
// Copy basic properties
merged.system = primarySource.system || localData.system || 'drs';
merged.bookId = primarySource.bookId || localData.bookId;
merged.chapterId = primarySource.chapterId || localData.chapterId;
merged.completed = primarySource.completed || secondarySource.completed || false;
merged.savedAt = new Date().toISOString();
// Merge each category
const categories = ['masteredVocabulary', 'masteredPhrases', 'masteredGrammar'];
for (const category of categories) {
const localItems = localData[category] || [];
const externalItems = externalData[category] || [];
const mergeResult = mergeItemArrays(localItems, externalItems, strategy);
merged[category] = mergeResult.items;
merged.conflicts.push(...mergeResult.conflicts.map(c => ({ ...c, category })));
}
// Calculate mastery count (max of both sources plus any new merged items)
merged.masteryCount = Math.max(
localData.masteryCount || 0,
externalData.masteryCount || 0,
merged.masteredVocabulary.length + merged.masteredPhrases.length + merged.masteredGrammar.length
);
return merged;
}
function mergeItemArrays(localItems, externalItems, strategy) {
const result = {
items: [],
conflicts: []
};
const itemMap = new Map();
// Add all items to map, handling conflicts
const addToMap = (items, source) => {
items.forEach(item => {
const itemKey = typeof item === 'string' ? item : item.item;
const itemData = typeof item === 'string' ? { item, masteredAt: '1970-01-01T00:00:00.000Z', source } : { ...item, source };
if (itemMap.has(itemKey)) {
const existing = itemMap.get(itemKey);
const conflict = {
item: itemKey,
local: source === 'local' ? itemData : existing,
external: source === 'external' ? itemData : existing,
resolution: 'pending'
};
// Resolve conflict based on strategy
let resolvedItem;
switch (strategy) {
case 'timestamp':
const existingTime = new Date(existing.masteredAt || existing.lastReviewAt || '1970-01-01');
const newTime = new Date(itemData.masteredAt || itemData.lastReviewAt || '1970-01-01');
if (newTime > existingTime) {
resolvedItem = { ...itemData, attempts: (existing.attempts || 1) + (itemData.attempts || 1) };
conflict.resolution = `newer_timestamp_from_${source}`;
} else {
resolvedItem = { ...existing, attempts: (existing.attempts || 1) + (itemData.attempts || 1) };
conflict.resolution = 'kept_existing_newer_timestamp';
}
break;
case 'attempts':
resolvedItem = {
...existing,
attempts: (existing.attempts || 1) + (itemData.attempts || 1),
lastReviewAt: new Date().toISOString()
};
conflict.resolution = 'merged_attempts';
break;
case 'prefer_local':
resolvedItem = source === 'local' ? itemData : existing;
conflict.resolution = 'preferred_local';
break;
case 'prefer_external':
resolvedItem = source === 'external' ? itemData : existing;
conflict.resolution = 'preferred_external';
break;
default:
resolvedItem = existing;
conflict.resolution = 'kept_existing_default';
}
itemMap.set(itemKey, resolvedItem);
result.conflicts.push(conflict);
} else {
itemMap.set(itemKey, itemData);
}
});
};
// Process both arrays
addToMap(localItems, 'local');
addToMap(externalItems, 'external');
// Convert map back to array, removing source metadata
result.items = Array.from(itemMap.values()).map(item => {
const { source, ...cleanItem } = item;
return cleanItem;
});
return result;
}
function countProgressItems(data) {
if (!data) return 0;
const vocab = data.masteredVocabulary?.length || 0;
const phrases = data.masteredPhrases?.length || 0;
const grammar = data.masteredGrammar?.length || 0;
return vocab + phrases + grammar;
}
function hasTimestampData(data) {
if (!data) return false;
const checkArray = (arr) => {
return arr && arr.length > 0 && arr.some(item =>
typeof item === 'object' && (item.masteredAt || item.lastReviewAt)
);
};
return checkArray(data.masteredVocabulary) ||
checkArray(data.masteredPhrases) ||
checkArray(data.masteredGrammar);
}
// Start server
server.listen(PORT, HOST, () => {
console.log('\n🚀 Class Generator Development Server');
console.log('=====================================');
console.log(`📍 Local: http://${HOST}:${PORT}/`);
console.log(`🌐 Network: http://localhost:${PORT}/`);
console.log('📁 Serving files from:', __dirname);
console.log('\n✨ Features:');
console.log(' • ES6 modules support');
console.log(' • CORS enabled');
console.log(' • Static file serving');
console.log(' • Development-friendly caching');
console.log('\n🔥 Ready for development!');
console.log('Press Ctrl+C to stop\n');
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n👋 Shutting down server...');
server.close(() => {
console.log('✅ Server stopped');
process.exit(0);
});
});
process.on('SIGTERM', () => {
console.log('\n👋 Received SIGTERM, shutting down...');
server.close(() => {
console.log('✅ Server stopped');
process.exit(0);
});
});