|
|
|
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)"], |
|
} |
|
}; |
|
|
|
|
|
let currentNode = 'q1'; |
|
let navigationHistory = []; |
|
|
|
|
|
let scale = 1; |
|
let translateX = 0; |
|
let translateY = 0; |
|
let isDragging = false; |
|
let lastMousePos = { x: 0, y: 0 }; |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
updateGuidedView(); |
|
initializeMermaid(); |
|
|
|
|
|
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'); |
|
} |
|
} |
|
|
|
|
|
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 { |
|
|
|
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() { |
|
|
|
|
|
} |
|
|
|
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; |
|
} |
|
|
|
|
|
function initializeMermaid() { |
|
const svg = generateFlowchartSVG(); |
|
document.getElementById('mermaid-container').innerHTML = svg; |
|
setupFlowchartInteraction(); |
|
} |
|
|
|
function generateFlowchartSVG() { |
|
|
|
const config = { |
|
startX: 200, |
|
startY: 60, |
|
questionSpacing: 220, |
|
outcomeY: 1450, |
|
outcomeSpacing: 350, |
|
articleOffset: 70, |
|
rightOutcomeX: 600, |
|
diamondSize: 100 |
|
}; |
|
|
|
let svgContent = `<svg viewBox="0 0 1600 1600" style="width: 100%; height: auto;">`; |
|
|
|
|
|
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> |
|
`; |
|
|
|
|
|
const startY = config.startY; |
|
svgContent += generateStartNode(config.startX, startY); |
|
|
|
|
|
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); |
|
|
|
|
|
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)"/>`; |
|
|
|
|
|
if (index < questions.length - 1) { |
|
|
|
const label = (index === 1 || index === 3) ? "Yes" : "No"; |
|
svgContent += `<text x="${config.startX + 20}" y="${y + config.diamondSize + 20}" text-anchor="start" font-size="12" font-weight="bold" fill="#333">${label}</text>`; |
|
} |
|
}); |
|
|
|
|
|
svgContent += generateOutcome('outcome_research', config.rightOutcomeX, startY + 60 + 2 * config.questionSpacing); |
|
svgContent += generateArticles('outcome_research', config.rightOutcomeX + config.articleOffset, startY + 60 + 2 * config.questionSpacing - 40); |
|
|
|
|
|
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>`; |
|
|
|
|
|
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>`; |
|
|
|
|
|
const bottomOutcomes = ['outcome_open_source', 'outcome_gpai_provider_with_obligations', 'outcome_systemic_risk']; |
|
bottomOutcomes.forEach((outcomeId, index) => { |
|
const x = config.startX + index * config.outcomeSpacing; |
|
|
|
|
|
svgContent += generateOutcome(outcomeId, x, config.outcomeY); |
|
|
|
|
|
svgContent += generateArticles(outcomeId, x + config.articleOffset, config.outcomeY - 40); |
|
|
|
|
|
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; |
|
|
|
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 ''; |
|
|
|
|
|
const articleCount = outcome.articles.length; |
|
const width = 180; |
|
const height = Math.max(100, articleCount * 18 + 60); |
|
|
|
|
|
const adjustedY = y; |
|
|
|
let content = `<rect x="${x}" y="${adjustedY}" width="${width}" height="${height}" fill="#E8F5E8" stroke="#4CAF50" stroke-width="3" rx="10"/>`; |
|
|
|
|
|
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>`; |
|
}); |
|
|
|
|
|
let articleStartY = adjustedY + 20 + titleLines.length * 15 + 15; |
|
outcome.articles.forEach((article, index) => { |
|
const articleY = articleStartY + index * 18; |
|
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': |
|
|
|
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': |
|
|
|
const q4Y = questionY + 4 * config.questionSpacing; |
|
const q5Y_yes = questionY + 5 * config.questionSpacing; |
|
const midPointX = x - 50; |
|
|
|
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': |
|
|
|
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; |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
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; |
|
}); |
|
|
|
|
|
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(); |
|
}); |
|
|
|
|
|
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(); |
|
}); |
|
} |