fix: docked editor panel height + quiz image parser + assets dir config

- fix(layout): editor-area docked mode sekarang height:70vh (sebelumnya
  max-height:85vh tanpa height, menyebabkan tab Arduino/velxio menciut
  ke 150px karena iframe tidak punya intrinsic height)
- fix(layout): tambah overflow:hidden pada velxio/flowchart/quiz panel
  untuk mencegah nested scrollable flex collapse
- feat(quiz): parser image dari 'image:' directive dan markdown ![alt](path)
  di question flashcard; konversi bare filename ke /assets/ path
- feat(assets): ASSETS_DIR derived dari CONTENT_DIR untuk support
  custom content directory (bukan hardcoded 'assets')
- fix(quiz): QuizTab container gunakan flex:1+min-height:0 (bukan height:100%)
- chore: hapus placeholder examples/assets/put_your_image.here
This commit is contained in:
a2nr 2026-07-21 20:50:01 +07:00
parent 156480b26f
commit 8e89fde3c1
9 changed files with 45 additions and 10 deletions

View File

@ -7,3 +7,9 @@ import os
CONTENT_DIR = os.environ.get('CONTENT_DIR', 'content')
TOKENS_FILE = os.environ.get('TOKENS_FILE', 'tokens.csv')
# Assets directory: derived from parent of CONTENT_DIR
# E.g. CONTENT_DIR='content' → ASSETS_DIR='assets'
# CONTENT_DIR='examples/content' → ASSETS_DIR='examples/assets'
# CONTENT_DIR='/app/content' → ASSETS_DIR='/app/assets'
ASSETS_DIR = os.path.join(os.path.dirname(CONTENT_DIR.rstrip(os.sep)), 'assets')

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -26,6 +26,15 @@ Menandakan bahwa program telah selesai berjalan dengan sukses tanpa ada error.
### Pilih gambar yang melambangkan bahasa C:
- [x] ![C Logo](https://upload.wikimedia.org/wikipedia/commons/1/18/C_Programming_Language.svg)
- [] ![Wrong Logo](https://upload.wikimedia.org/wikipedia/commons/c/c3/Python-logo-notext.svg)
### Apa warna langit pada siang hari?
image: demo_quiz.png
- [x] Biru
- [] Hijau
- [] Merah
- [] Kuning
> Penjelasan: Langit terlihat biru karena hamburan Rayleigh — cahaya matahari dihamburkan oleh atmosfer bumi.
---END_QUIZ_FLASHCARD---
---EXERCISE---

View File

@ -297,7 +297,7 @@
{/if}
{#if mgr.data?.active_tabs?.includes('quiz')}
<div class="tab-panel" class:tab-hidden={mgr.activeTab !== 'quiz'}>
<div class="tab-panel quiz-panel" class:tab-hidden={mgr.activeTab !== 'quiz'}>
<QuizTab
quizData={mgr.data.quiz_data ?? []}
bind:isQuizMode={mgr.isQuizMode}

View File

@ -290,8 +290,9 @@
<style>
.quiz-container {
flex: 1;
min-height: 0;
padding: 1.5rem;
height: 100%;
display: flex;
flex-direction: column;
background: var(--color-bg);

View File

@ -80,7 +80,7 @@
display: flex;
flex-direction: column;
overflow: hidden;
max-height: 85vh;
height: 70vh;
}
.editor-area .editor-body {
flex: 1;
@ -291,15 +291,18 @@
}
.velxio-panel,
.flowchart-panel {
.flowchart-panel,
.quiz-panel {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
position: relative;
overflow: hidden;
}
.velxio-panel.tab-hidden,
.flowchart-panel.tab-hidden {
.flowchart-panel.tab-hidden,
.quiz-panel.tab-hidden {
display: none;
}
.velxio-iframe {
@ -468,4 +471,4 @@
color: var(--color-text-muted);
margin-bottom: 1.5rem;
line-height: 1.4;
}
}

View File

@ -8,7 +8,7 @@ from flask import Blueprint, request, jsonify, send_from_directory
from werkzeug.utils import secure_filename
from compiler import compiler_factory
from config import CONTENT_DIR
from config import CONTENT_DIR, ASSETS_DIR
from services.lesson_service import (
get_ordered_lessons_with_learning_objectives,
render_markdown_content,
@ -225,5 +225,5 @@ def get_key_text(filename):
@lessons_bp.route('/assets/<path:path>')
def send_assets(path):
"""Serve asset files (images, etc.)."""
return send_from_directory('assets', path)
"""Serve asset files (images, etc.) from ASSETS_DIR."""
return send_from_directory(ASSETS_DIR, path)

View File

@ -481,14 +481,30 @@ def _parse_flashcards(text):
if not question:
continue
# Initialize image_url - will be set from markdown image or 'image:' directive
image_url = ""
# Extract image from markdown syntax ![...](path) in question
md_image_match = re.search(r'!\[.*?\]\(([^)]+)\)', question)
if md_image_match:
md_image_path = md_image_match.group(1).strip()
# Jika path lokal (/assets/ atau bare filename) dan belum ada image dari 'image:' directive
if md_image_path.startswith('/assets/') or (not md_image_path.startswith(('http://', 'https://')) and not image_url):
image_url = md_image_path
# Check for MCQ options: - [ ] or - [x]
option_pattern = re.compile(r'^\s*-\s*\[([ xX]?)\]\s*(.*)$', re.MULTILINE)
options = option_pattern.findall(body)
# Check for image: URL
image_match = re.search(r'^\s*image:\s*(.*)$', body, re.MULTILINE)
image_url = image_match.group(1).strip() if image_match else ""
if image_match:
image_url = image_match.group(1).strip() # Override jika ada 'image:' directive
if image_url.startswith('/assets/'):
# Path sudah lengkap, tidak perlu konversi
pass
elif not image_url.startswith(('http://', 'https://', '/')):
image_url = f'/assets/{image_url}'
body = re.sub(r'^\s*image:\s*.*$', '', body, flags=re.MULTILINE).strip()
# Check for explanation (blockquote starting with >)