yjernite's picture
yjernite HF Staff
Upload script.js
fad6ac6 verified
// Define flowchart data structure
const flowchartData = {
q1: {
text: "Did you develop the GPAI model for the sole purpose of scientific research and development?",
type: "question",
context: "You develop a GPAI model",
choices: [
{ text: "Yes", next: "outcome_research" },
{ text: "No", next: "q2" }
]
},
q2: {
text: "Have you made the GPAI model available on the EU market including via a commercial activity or via API or open repository?",
type: "question",
choices: [
{ text: "Yes", next: "q3" },
{ text: "No", next: "outcome_research" }
]
},
q3: {
text: "Does the GPAI model you've published qualify as posing a potential systemic risk?",
type: "question",
choices: [
{ text: "Yes", next: "outcome_systemic_risk" },
{ text: "No", next: "q4" }
]
},
q4: {
text: "Have you published the GPAI model under a free and open-source license along with documentation about model architecture and usage?",
type: "question",
choices: [
{ text: "Yes", next: "q5" },
{ text: "No", next: "outcome_gpai_provider_with_obligations" }
]
},
q5: {
text: "Are you monetising the GPAI model by making its availability contingent on payment, procuring other products/services, viewing ads, or receiving/processing personal data?",
type: "question",
choices: [
{ text: "Yes", next: "outcome_gpai_provider_with_obligations" },
{ text: "No", next: "outcome_open_source" }
]
},
outcome_research: {
text: "You're not a GPAI provider. GPAI provisions do not apply.",
type: "outcome"
},
outcome_systemic_risk: {
text: "You're a GPAISR provider. Open source exemptions from GPAI provisions do not apply.",
type: "outcome",
articles: ["Article 53(1)(a)", "Article 53(1)(b)", "Article 53(1)(c)", "Article 53(1)(d)", "Article 54", "Article 55"],
additional: "Additional obligations for GPAI with Systemic Risk: Article 55"
},
outcome_gpai_provider_with_obligations: {
text: "You're a GPAI provider. Open source exemptions from GPAI provisions do not apply.",
type: "outcome",
class: "gpai",
articles: ["Article 53(1)(a)", "Article 53(1)(b)", "Article 53(1)(c)", "Article 53(1)(d)", "Article 54"],
},
outcome_open_source: {
text: "You're an open-source GPAI provider. Open source exemptions apply.",
type: "outcome",
class: "gpai",
articles: ["Article 53(1)(a)", "Article 53(1)(b)"],
}
};
// State variables
let currentNode = 'q1';
let navigationHistory = [];
// Zoom and pan variables for flowchart
let scale = 1;
let translateX = 0;
let translateY = 0;
let isDragging = false;
let lastMousePos = { x: 0, y: 0 };
// Initialize the app
document.addEventListener('DOMContentLoaded', function() {
updateGuidedView();
initializeMermaid();
// Set up mode switching
document.getElementById('guided-btn').addEventListener('click', () => switchMode('guided'));
document.getElementById('flowchart-btn').addEventListener('click', () => switchMode('flowchart'));
});
function switchMode(mode) {
const guidedMode = document.getElementById('guided-mode');
const flowchartMode = document.getElementById('flowchart-mode');
const guidedBtn = document.getElementById('guided-btn');
const flowchartBtn = document.getElementById('flowchart-btn');
if (mode === 'guided') {
guidedMode.classList.remove('hidden');
flowchartMode.classList.add('hidden');
guidedBtn.classList.add('active');
flowchartBtn.classList.remove('active');
} else {
guidedMode.classList.add('hidden');
flowchartMode.classList.remove('hidden');
guidedBtn.classList.remove('active');
flowchartBtn.classList.add('active');
}
}
// Guided mode functions
function updateGuidedView() {
updateContext();
updateHistory();
updateCurrentQuestion();
updateArticles();
}
function updateContext() {
const contextElement = document.querySelector('.context-text');
const currentData = flowchartData[currentNode];
contextElement.textContent = currentData.context || "You develop a GPAI model";
}
function updateHistory() {
const historyContainer = document.querySelector('.history-section');
const historyHTML = navigationHistory.map((item, index) => `
<div class="history-item" onclick="goToHistoryStep(${index})">
<div class="history-question">${truncateText(item.question, 50)}</div>
<div class="history-answer">→ ${item.answer}</div>
<div class="tooltip">${item.question}</div>
</div>
`).join('');
historyContainer.innerHTML = `
<h3>Decision History</h3>
${historyHTML}
`;
}
function updateCurrentQuestion() {
const container = document.getElementById('question-container');
const currentData = flowchartData[currentNode];
if (currentData.type === 'question') {
container.innerHTML = `
<div class="question">${currentData.text}</div>
<div class="choices">
${currentData.choices.map(choice =>
`<button class="choice-btn" onclick="nextQuestion('${choice.next}', '${choice.text}')">${choice.text}</button>`
).join('')}
</div>
`;
} else {
// Outcome
let articlesHTML = '';
if (currentData.articles) {
const articlesList = currentData.articles.map(article =>
`<li>${article}</li>`
).join('');
articlesHTML = `
<div class="articles-section" style="margin-top: 30px;">
<h3>Applicable Articles</h3>
<ul class="articles-list">
${articlesList}
</ul>
</div>
`;
}
container.innerHTML = `
<div class="outcome ${currentData.class || ''}">${currentData.text}</div>
${articlesHTML}
<button class="restart-btn" onclick="restart()">Start Over</button>
`;
}
}
function updateArticles() {
// This function is no longer needed since articles are now displayed in updateCurrentQuestion
// But we'll keep it empty to avoid breaking the updateGuidedView call
}
function nextQuestion(nodeId, selectedChoice = null) {
if (selectedChoice) {
navigationHistory.push({
node: currentNode,
question: flowchartData[currentNode].text,
answer: selectedChoice,
nextNode: nodeId
});
}
currentNode = nodeId;
updateGuidedView();
}
function goToHistoryStep(stepIndex) {
navigationHistory = navigationHistory.slice(0, stepIndex);
currentNode = stepIndex === 0 ? 'q1' : navigationHistory[stepIndex - 1].nextNode;
updateGuidedView();
}
function restart() {
currentNode = 'q1';
navigationHistory = [];
updateGuidedView();
}
function truncateText(text, maxLength) {
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
}
// Create dynamic SVG flowchart from flowchartData
function initializeMermaid() {
const svg = generateFlowchartSVG();
document.getElementById('mermaid-container').innerHTML = svg;
setupFlowchartInteraction();
}
function generateFlowchartSVG() {
// Layout configuration
const config = {
startX: 200,
startY: 60,
questionSpacing: 220,
outcomeY: 1450, // Moved further down from 1350
outcomeSpacing: 350,
articleOffset: 70, // Reduced from 140 to bring articles closer
rightOutcomeX: 600,
diamondSize: 100
};
let svgContent = `<svg viewBox="0 0 1600 1600" style="width: 100%; height: auto;">`; // Increased height
// Add arrow marker definition
svgContent += `
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#333"/>
</marker>
</defs>
`;
// Generate start node
const startY = config.startY;
svgContent += generateStartNode(config.startX, startY);
// Generate questions vertically
const questions = ['q1', 'q2', 'q3', 'q4', 'q5'];
questions.forEach((qId, index) => {
const y = startY + 60 + (index + 1) * config.questionSpacing;
svgContent += generateQuestion(qId, config.startX, y);
// Add connecting line from previous element
const prevY = index === 0 ? startY + 30 : startY + 60 + index * config.questionSpacing + config.diamondSize;
svgContent += `<line x1="${config.startX}" y1="${prevY}" x2="${config.startX}" y2="${y - config.diamondSize}" stroke="#333" stroke-width="2" marker-end="url(#arrowhead)"/>`;
// Add correct label for vertical flow (except last question)
if (index < questions.length - 1) {
// Q2→Q3 and Q4→Q5 should be "Yes", Q1→Q2 and Q3→Q4 should be "No"
const label = (index === 1 || index === 3) ? "Yes" : "No"; // Q2(index=1)→Q3 and Q4(index=3)→Q5
svgContent += `<text x="${config.startX + 20}" y="${y + config.diamondSize + 20}" text-anchor="start" font-size="12" font-weight="bold" fill="#333">${label}</text>`;
}
});
// Generate outcome_research (right of q2, not q1)
svgContent += generateOutcome('outcome_research', config.rightOutcomeX, startY + 60 + 2 * config.questionSpacing); // Changed to Q2 position
svgContent += generateArticles('outcome_research', config.rightOutcomeX + config.articleOffset, startY + 60 + 2 * config.questionSpacing - 40);
// Arrow from Q1 Yes to outcome_research (curved to reach Q2 level)
const q1Y = startY + 60 + config.questionSpacing;
const q2Y = startY + 60 + 2 * config.questionSpacing;
svgContent += `<line x1="${config.startX + config.diamondSize}" y1="${q1Y}" x2="${config.rightOutcomeX - 150}" y2="${q1Y}" stroke="#333" stroke-width="2"/>`;
svgContent += `<line x1="${config.rightOutcomeX - 150}" y1="${q1Y}" x2="${config.rightOutcomeX - 150}" y2="${q2Y}" stroke="#333" stroke-width="2"/>`;
svgContent += `<line x1="${config.rightOutcomeX - 150}" y1="${q2Y}" x2="${config.rightOutcomeX - 60}" y2="${q2Y}" stroke="#333" stroke-width="2" marker-end="url(#arrowhead)"/>`;
svgContent += `<text x="${config.startX + 140}" y="${q1Y - 5}" text-anchor="middle" font-size="12" font-weight="bold" fill="#333">Yes</text>`;
// Arrow from Q2 No to outcome_research (straight right)
svgContent += `<line x1="${config.startX + config.diamondSize}" y1="${q2Y}" x2="${config.rightOutcomeX - 60}" y2="${q2Y}" stroke="#333" stroke-width="2" marker-end="url(#arrowhead)"/>`;
svgContent += `<text x="${config.startX + 140}" y="${q2Y - 5}" text-anchor="middle" font-size="12" font-weight="bold" fill="#333">No</text>`;
// Generate bottom row outcomes
const bottomOutcomes = ['outcome_open_source', 'outcome_gpai_provider_with_obligations', 'outcome_systemic_risk'];
bottomOutcomes.forEach((outcomeId, index) => {
const x = config.startX + index * config.outcomeSpacing;
// Generate outcome circle
svgContent += generateOutcome(outcomeId, x, config.outcomeY);
// Generate articles to the right
svgContent += generateArticles(outcomeId, x + config.articleOffset, config.outcomeY - 40);
// Add connecting arrows based on outcome type
svgContent += generateOutcomeArrow(outcomeId, x, config);
});
svgContent += `</svg>`;
return svgContent;
}
function generateStartNode(x, y) {
return `
<ellipse cx="${x}" cy="${y}" rx="100" ry="30" fill="#FFA726" stroke="#FF8F00" stroke-width="3"/>
<text x="${x}" y="${y + 5}" text-anchor="middle" font-size="14" font-weight="bold" fill="#333">You develop a GPAI model</text>
`;
}
function generateQuestion(questionId, x, y) {
const question = flowchartData[questionId];
if (!question) return '';
const lines = wrapText(question.text, 25);
const lineHeight = 15;
const totalHeight = lines.length * lineHeight;
const startY = y - totalHeight / 2 + lineHeight / 2;
const size = 100; // Larger diamond size
let content = `<polygon points="${x},${y-size} ${x-size},${y} ${x},${y+size} ${x+size},${y}" fill="#A5D6A7" stroke="#4CAF50" stroke-width="3"/>`;
lines.forEach((line, index) => {
content += `<text x="${x}" y="${startY + index * lineHeight}" text-anchor="middle" font-size="11" font-weight="bold" fill="#333">${line}</text>`;
});
return content;
}
function generateOutcome(outcomeId, x, y) {
const outcome = flowchartData[outcomeId];
if (!outcome) return '';
const lines = wrapText(outcome.text, 18);
const lineHeight = 15;
const totalHeight = lines.length * lineHeight;
const startY = y - totalHeight / 2 + lineHeight / 2;
let content = `<circle cx="${x}" cy="${y}" r="60" fill="#333"/>`;
lines.forEach((line, index) => {
content += `<text x="${x}" y="${startY + index * lineHeight}" text-anchor="middle" font-size="10" fill="white" font-weight="bold">${line}</text>`;
});
return content;
}
function generateArticles(outcomeId, x, y) {
const outcome = flowchartData[outcomeId];
if (!outcome || !outcome.articles) return '';
// Dynamic sizing based on content
const articleCount = outcome.articles.length;
const width = 180;
const height = Math.max(100, articleCount * 18 + 60); // Better height calculation
// Top-aligned with the outcome circle (circle is at y, radius 60, so top is y-60)
const adjustedY = y; // Align to top of circle instead of center
let content = `<rect x="${x}" y="${adjustedY}" width="${width}" height="${height}" fill="#E8F5E8" stroke="#4CAF50" stroke-width="3" rx="10"/>`;
// Title
const titleLines = wrapText(`Applicable obligations for ${getOutcomeTitle(outcomeId)}`, 22);
let titleY = adjustedY + 20;
titleLines.forEach((line, index) => {
content += `<text x="${x + width/2}" y="${titleY + index * 15}" text-anchor="middle" font-size="11" font-weight="bold" fill="#333">${line}</text>`;
});
// Articles with better spacing
let articleStartY = adjustedY + 20 + titleLines.length * 15 + 15;
outcome.articles.forEach((article, index) => {
const articleY = articleStartY + index * 18; // Increased spacing between articles
content += `<text x="${x + 15}" y="${articleY}" text-anchor="start" font-size="10" fill="#667eea">${article}</text>`;
});
return content;
}
function generateOutcomeArrow(outcomeId, x, config) {
const questionY = config.startY + 60;
let content = '';
switch(outcomeId) {
case 'outcome_open_source':
// From Q5 No (straight down)
const q5Y = questionY + 5 * config.questionSpacing;
content = `
<line x1="${config.startX}" y1="${q5Y + config.diamondSize}" x2="${config.startX}" y2="${config.outcomeY - 60}" stroke="#333" stroke-width="2" marker-end="url(#arrowhead)"/>
<text x="${config.startX - 30}" y="${q5Y + config.diamondSize + 40}" text-anchor="middle" font-size="12" font-weight="bold" fill="#333">No</text>
`;
break;
case 'outcome_gpai_provider_with_obligations':
// From Q4 No and Q5 Yes - combine to same circle
const q4Y = questionY + 4 * config.questionSpacing;
const q5Y_yes = questionY + 5 * config.questionSpacing;
const midPointX = x - 50; // Meeting point for both arrows
content = `
<!-- Q4 No path -->
<line x1="${config.startX + config.diamondSize}" y1="${q4Y}" x2="${midPointX}" y2="${q4Y}" stroke="#333" stroke-width="2"/>
<line x1="${midPointX}" y1="${q4Y}" x2="${midPointX}" y2="${config.outcomeY - 100}" stroke="#333" stroke-width="2"/>
<text x="${config.startX + 150}" y="${q4Y - 5}" text-anchor="middle" font-size="12" font-weight="bold" fill="#333">No</text>
<!-- Q5 Yes path -->
<line x1="${config.startX + config.diamondSize}" y1="${q5Y_yes}" x2="${midPointX}" y2="${q5Y_yes}" stroke="#333" stroke-width="2"/>
<line x1="${midPointX}" y1="${q5Y_yes}" x2="${midPointX}" y2="${config.outcomeY - 100}" stroke="#333" stroke-width="2"/>
<text x="${config.startX + 150}" y="${q5Y_yes - 5}" text-anchor="middle" font-size="12" font-weight="bold" fill="#333">Yes</text>
<!-- Combined path to circle -->
<line x1="${midPointX}" y1="${config.outcomeY - 100}" x2="${x}" y2="${config.outcomeY - 60}" stroke="#333" stroke-width="2" marker-end="url(#arrowhead)"/>
`;
break;
case 'outcome_systemic_risk':
// From Q3 Yes (horizontal then down)
const q3Y = questionY + 3 * config.questionSpacing;
content = `
<line x1="${config.startX + config.diamondSize}" y1="${q3Y}" x2="${x}" y2="${q3Y}" stroke="#333" stroke-width="2"/>
<line x1="${x}" y1="${q3Y}" x2="${x}" y2="${config.outcomeY - 60}" stroke="#333" stroke-width="2" marker-end="url(#arrowhead)"/>
<text x="${config.startX + 150}" y="${q3Y - 5}" text-anchor="middle" font-size="12" font-weight="bold" fill="#333">Yes</text>
`;
break;
}
return content;
}
function getOutcomeTitle(outcomeId) {
switch(outcomeId) {
case 'outcome_research': return 'research providers';
case 'outcome_open_source': return 'open-source GPAI providers';
case 'outcome_gpai_provider_with_obligations': return 'GPAI providers';
case 'outcome_systemic_risk': return 'GPAISR providers';
default: return 'providers';
}
}
function wrapText(text, maxLength) {
const words = text.split(' ');
const lines = [];
let currentLine = '';
words.forEach(word => {
if ((currentLine + word).length <= maxLength) {
currentLine += (currentLine ? ' ' : '') + word;
} else {
if (currentLine) lines.push(currentLine);
currentLine = word;
}
});
if (currentLine) lines.push(currentLine);
return lines;
}
// Flowchart interaction functions (zoom, pan)
function updateTransform() {
const mermaidContainer = document.getElementById('mermaid-container');
if (mermaidContainer) {
mermaidContainer.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
}
}
function zoomIn() {
scale = Math.min(scale * 1.2, 3);
updateTransform();
}
function zoomOut() {
scale = Math.max(scale / 1.2, 0.3);
updateTransform();
}
function resetView() {
scale = 1;
translateX = 0;
translateY = 0;
updateTransform();
}
function setupFlowchartInteraction() {
const container = document.getElementById('flowchart-container');
if (!container) return;
// Mouse events for dragging
container.addEventListener('mousedown', function(e) {
isDragging = true;
lastMousePos = { x: e.clientX, y: e.clientY };
e.preventDefault();
});
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;
const deltaX = e.clientX - lastMousePos.x;
const deltaY = e.clientY - lastMousePos.y;
translateX += deltaX;
translateY += deltaY;
updateTransform();
lastMousePos = { x: e.clientX, y: e.clientY };
});
document.addEventListener('mouseup', function() {
isDragging = false;
});
// Wheel zoom
container.addEventListener('wheel', function(e) {
e.preventDefault();
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const zoom = e.deltaY > 0 ? 0.9 : 1.1;
const newScale = Math.min(Math.max(scale * zoom, 0.3), 3);
const factor = newScale / scale;
translateX = x - (x - translateX) * factor;
translateY = y - (y - translateY) * factor;
scale = newScale;
updateTransform();
});
// Touch events for mobile
let touchStartDistance = 0;
let touchStartScale = 1;
container.addEventListener('touchstart', function(e) {
if (e.touches.length === 1) {
isDragging = true;
lastMousePos = { x: e.touches[0].clientX, y: e.touches[0].clientY };
} else if (e.touches.length === 2) {
isDragging = false;
const touch1 = e.touches[0];
const touch2 = e.touches[1];
touchStartDistance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
touchStartScale = scale;
}
e.preventDefault();
});
container.addEventListener('touchmove', function(e) {
if (e.touches.length === 1 && isDragging) {
const deltaX = e.touches[0].clientX - lastMousePos.x;
const deltaY = e.touches[0].clientY - lastMousePos.y;
translateX += deltaX;
translateY += deltaY;
updateTransform();
lastMousePos = { x: e.touches[0].clientX, y: e.touches[0].clientY };
} else if (e.touches.length === 2) {
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
scale = Math.min(Math.max(touchStartScale * (currentDistance / touchStartDistance), 0.3), 3);
updateTransform();
}
e.preventDefault();
});
container.addEventListener('touchend', function(e) {
isDragging = false;
e.preventDefault();
});
}