Spaces:
Paused
Paused
AnhviNguyen commited on
Commit ·
02f8344
1
Parent(s): 987d4ac
shadowing
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- backend/app/db/models.py +36 -0
- backend/app/main.py +4 -0
- backend/app/repositories/shadowing_repository.py +119 -0
- backend/app/routers/shadowing.py +152 -0
- backend/app/routers/vocabulary.py +27 -0
- backend/app/schemas.py +58 -0
- backend/app/services/shadowing_pronunciation_service.py +109 -0
- backend/app/services/shadowing_service.py +174 -0
- backend/app/services/shadowing_whisper_service.py +112 -0
- backend/app/services/translate_service.py +44 -0
- backend/app/services/vocab_lookup_service.py +326 -0
- backend/app/services/youtube_transcript_service.py +95 -0
- backend/app/utils/segment_utils.py +132 -0
- backend/requirements.txt +2 -0
- fronted/dist/assets/AudioPlayer-COEgM5Q5.js +0 -1
- fronted/dist/assets/AudioPlayer-RR1aaAJE.js +1 -0
- fronted/dist/assets/AudioPlayer-yjMJBD8R.css +0 -1
- fronted/dist/assets/Dashboard-BmWC62sI.js +0 -1
- fronted/dist/assets/Dashboard-C9eprd66.css +0 -1
- fronted/dist/assets/Dashboard-JNo6Dw71.js +1 -0
- fronted/dist/assets/History-B_R9pOYA.js +1 -0
- fronted/dist/assets/History-CCzhCIbk.js +0 -1
- fronted/dist/assets/History-PkzicY-T.css +0 -1
- fronted/dist/assets/Leaderboard-BRWxu-5I.css +0 -1
- fronted/dist/assets/Leaderboard-Cklwb0qQ.js +0 -1
- fronted/dist/assets/Leaderboard-DqCSL79X.js +1 -0
- fronted/dist/assets/Listening-B_vWRFIo.js +0 -1
- fronted/dist/assets/Listening-CsI8kE6C.js +1 -0
- fronted/dist/assets/Login-C03J8r1H.css +0 -1
- fronted/dist/assets/Login-CipjpBTk.js +1 -0
- fronted/dist/assets/Login-CrChhjRP.js +0 -1
- fronted/dist/assets/MockTestMode-DE-_sNCc.js +1 -0
- fronted/dist/assets/MockTestMode-DYXoE6qd.js +0 -1
- fronted/dist/assets/ModePickerModal-CUJKgPsz.js +1 -0
- fronted/dist/assets/Paginator-C1T4CVWL.js +1 -0
- fronted/dist/assets/Paginator-VCw60xVa.css +0 -1
- fronted/dist/assets/Paginator-lTh9FHNB.js +0 -1
- fronted/dist/assets/Profile-CXJvjz7d.js +1 -0
- fronted/dist/assets/Profile-C_lVTmCB.css +0 -1
- fronted/dist/assets/Profile-D5X-3cOn.js +0 -1
- fronted/dist/assets/{QuizResult-DS1mc336.js → QuizResult-CHzIRTvk.js} +1 -1
- fronted/dist/assets/QuizRunner-DCdBBWGG.js +1 -0
- fronted/dist/assets/QuizRunner-RdUrirGe.js +0 -1
- fronted/dist/assets/QuizRunner-iXYCFrWo.css +0 -1
- fronted/dist/assets/Reading-CN1Pbi4C.js +1 -0
- fronted/dist/assets/Reading-Cc5Jk6VP.js +0 -1
- fronted/dist/assets/Register-ChpOvjos.js +1 -0
- fronted/dist/assets/Register-DDXKtuDu.js +0 -1
- fronted/dist/assets/Register-EQfUP2x7.css +0 -1
- fronted/dist/assets/{Result-Czy97pS1.js → Result-mQyw3_em.js} +1 -1
backend/app/db/models.py
CHANGED
|
@@ -47,6 +47,9 @@ class User(Base):
|
|
| 47 |
study_plan_tasks: Mapped[list["StudyPlanTask"]] = relationship(
|
| 48 |
"StudyPlanTask", back_populates="user", cascade="all, delete-orphan"
|
| 49 |
)
|
|
|
|
|
|
|
|
|
|
| 50 |
vocab_topics: Mapped[list["VocabTopic"]] = relationship(
|
| 51 |
"VocabTopic", back_populates="user", cascade="all, delete-orphan"
|
| 52 |
)
|
|
@@ -209,6 +212,39 @@ class ReadingAnnotation(Base):
|
|
| 209 |
user: Mapped["User"] = relationship("User", back_populates="reading_annotations")
|
| 210 |
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
class StudyPlanTask(Base):
|
| 213 |
"""A single to-do task inside a user's AI-generated study plan."""
|
| 214 |
__tablename__ = "study_plan_tasks"
|
|
|
|
| 47 |
study_plan_tasks: Mapped[list["StudyPlanTask"]] = relationship(
|
| 48 |
"StudyPlanTask", back_populates="user", cascade="all, delete-orphan"
|
| 49 |
)
|
| 50 |
+
shadowing_history: Mapped[list["ShadowingUserHistory"]] = relationship(
|
| 51 |
+
"ShadowingUserHistory", back_populates="user", cascade="all, delete-orphan"
|
| 52 |
+
)
|
| 53 |
vocab_topics: Mapped[list["VocabTopic"]] = relationship(
|
| 54 |
"VocabTopic", back_populates="user", cascade="all, delete-orphan"
|
| 55 |
)
|
|
|
|
| 212 |
user: Mapped["User"] = relationship("User", back_populates="reading_annotations")
|
| 213 |
|
| 214 |
|
| 215 |
+
class ShadowingVideo(Base):
|
| 216 |
+
"""Cached YouTube transcript for shadowing / dictation practice."""
|
| 217 |
+
__tablename__ = "shadowing_videos"
|
| 218 |
+
|
| 219 |
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
| 220 |
+
video_id: Mapped[str] = mapped_column(String(20), unique=True, nullable=False, index=True)
|
| 221 |
+
title: Mapped[str] = mapped_column(String(500), default="")
|
| 222 |
+
level: Mapped[str] = mapped_column(String(50), default="Intermediate")
|
| 223 |
+
language: Mapped[str] = mapped_column(String(10), default="en")
|
| 224 |
+
source_url: Mapped[str] = mapped_column(Text, default="")
|
| 225 |
+
transcript_source: Mapped[str] = mapped_column(String(20), default="youtube") # youtube | whisper
|
| 226 |
+
segments: Mapped[Any] = mapped_column(JSON, default=list)
|
| 227 |
+
created_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
| 228 |
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class ShadowingUserHistory(Base):
|
| 232 |
+
"""Per-user shadowing watch history (last opened video)."""
|
| 233 |
+
__tablename__ = "shadowing_user_history"
|
| 234 |
+
__table_args__ = (UniqueConstraint("user_id", "video_id", name="uq_shadowing_user_video"),)
|
| 235 |
+
|
| 236 |
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
| 237 |
+
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
| 238 |
+
video_id: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
| 239 |
+
display_title: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
| 240 |
+
display_level: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
| 241 |
+
last_viewed_at: Mapped[datetime] = mapped_column(
|
| 242 |
+
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
user: Mapped["User"] = relationship("User", back_populates="shadowing_history")
|
| 246 |
+
|
| 247 |
+
|
| 248 |
class StudyPlanTask(Base):
|
| 249 |
"""A single to-do task inside a user's AI-generated study plan."""
|
| 250 |
__tablename__ = "study_plan_tasks"
|
backend/app/main.py
CHANGED
|
@@ -22,6 +22,7 @@ from app.routers import auth, history, practice, profile, progress, users
|
|
| 22 |
from app.routers import mock_tests, writing, speaking as speaking_router
|
| 23 |
from app.routers.vocabulary import router as vocabulary_router, annotations_router
|
| 24 |
from app.routers.leaderboard import router as leaderboard_router
|
|
|
|
| 25 |
|
| 26 |
# ── Logging ──────────────────────────────────────────────────────────────────
|
| 27 |
logging.basicConfig(
|
|
@@ -58,6 +59,8 @@ async def lifespan(app: FastAPI):
|
|
| 58 |
"ALTER TABLE vocab_words ADD COLUMN srs_repetitions INTEGER DEFAULT 0",
|
| 59 |
"ALTER TABLE vocab_words ADD COLUMN srs_next_review_at TIMESTAMP",
|
| 60 |
"ALTER TABLE vocab_words ADD COLUMN srs_last_review_at TIMESTAMP",
|
|
|
|
|
|
|
| 61 |
]
|
| 62 |
for _stmt in _col_migrations:
|
| 63 |
try:
|
|
@@ -114,6 +117,7 @@ app.include_router(speaking_router.router)
|
|
| 114 |
app.include_router(vocabulary_router)
|
| 115 |
app.include_router(annotations_router)
|
| 116 |
app.include_router(leaderboard_router)
|
|
|
|
| 117 |
|
| 118 |
|
| 119 |
# ── Health check ─────────────────────────────────────────────────────────────
|
|
|
|
| 22 |
from app.routers import mock_tests, writing, speaking as speaking_router
|
| 23 |
from app.routers.vocabulary import router as vocabulary_router, annotations_router
|
| 24 |
from app.routers.leaderboard import router as leaderboard_router
|
| 25 |
+
from app.routers.shadowing import router as shadowing_router
|
| 26 |
|
| 27 |
# ── Logging ──────────────────────────────────────────────────────────────────
|
| 28 |
logging.basicConfig(
|
|
|
|
| 59 |
"ALTER TABLE vocab_words ADD COLUMN srs_repetitions INTEGER DEFAULT 0",
|
| 60 |
"ALTER TABLE vocab_words ADD COLUMN srs_next_review_at TIMESTAMP",
|
| 61 |
"ALTER TABLE vocab_words ADD COLUMN srs_last_review_at TIMESTAMP",
|
| 62 |
+
"ALTER TABLE shadowing_user_history ADD COLUMN display_title VARCHAR(500)",
|
| 63 |
+
"ALTER TABLE shadowing_user_history ADD COLUMN display_level VARCHAR(50)",
|
| 64 |
]
|
| 65 |
for _stmt in _col_migrations:
|
| 66 |
try:
|
|
|
|
| 117 |
app.include_router(vocabulary_router)
|
| 118 |
app.include_router(annotations_router)
|
| 119 |
app.include_router(leaderboard_router)
|
| 120 |
+
app.include_router(shadowing_router)
|
| 121 |
|
| 122 |
|
| 123 |
# ── Health check ─────────────────────────────────────────────────────────────
|
backend/app/repositories/shadowing_repository.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Data access for shadowing videos and user watch history."""
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
|
| 5 |
+
from sqlalchemy import delete, select
|
| 6 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 7 |
+
|
| 8 |
+
from app.db.models import ShadowingUserHistory, ShadowingVideo
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class ShadowingRepository:
|
| 12 |
+
def __init__(self, db: AsyncSession):
|
| 13 |
+
self._db = db
|
| 14 |
+
|
| 15 |
+
async def get_by_video_id(self, video_id: str) -> ShadowingVideo | None:
|
| 16 |
+
r = await self._db.execute(
|
| 17 |
+
select(ShadowingVideo).where(ShadowingVideo.video_id == video_id)
|
| 18 |
+
)
|
| 19 |
+
return r.scalar_one_or_none()
|
| 20 |
+
|
| 21 |
+
async def upsert(
|
| 22 |
+
self,
|
| 23 |
+
*,
|
| 24 |
+
video_id: str,
|
| 25 |
+
title: str,
|
| 26 |
+
level: str,
|
| 27 |
+
language: str,
|
| 28 |
+
source_url: str,
|
| 29 |
+
transcript_source: str,
|
| 30 |
+
segments: list,
|
| 31 |
+
created_by: int | None,
|
| 32 |
+
) -> ShadowingVideo:
|
| 33 |
+
row = await self.get_by_video_id(video_id)
|
| 34 |
+
if row:
|
| 35 |
+
row.title = title
|
| 36 |
+
row.level = level
|
| 37 |
+
row.language = language
|
| 38 |
+
row.source_url = source_url
|
| 39 |
+
row.transcript_source = transcript_source
|
| 40 |
+
row.segments = segments
|
| 41 |
+
return row
|
| 42 |
+
|
| 43 |
+
row = ShadowingVideo(
|
| 44 |
+
video_id=video_id,
|
| 45 |
+
title=title,
|
| 46 |
+
level=level,
|
| 47 |
+
language=language,
|
| 48 |
+
source_url=source_url,
|
| 49 |
+
transcript_source=transcript_source,
|
| 50 |
+
segments=segments,
|
| 51 |
+
created_by=created_by,
|
| 52 |
+
)
|
| 53 |
+
self._db.add(row)
|
| 54 |
+
await self._db.flush()
|
| 55 |
+
await self._db.refresh(row)
|
| 56 |
+
return row
|
| 57 |
+
|
| 58 |
+
async def record_view(self, user_id: int, video_id: str) -> ShadowingUserHistory:
|
| 59 |
+
r = await self._db.execute(
|
| 60 |
+
select(ShadowingUserHistory).where(
|
| 61 |
+
ShadowingUserHistory.user_id == user_id,
|
| 62 |
+
ShadowingUserHistory.video_id == video_id,
|
| 63 |
+
)
|
| 64 |
+
)
|
| 65 |
+
row = r.scalar_one_or_none()
|
| 66 |
+
now = datetime.now(timezone.utc)
|
| 67 |
+
if row:
|
| 68 |
+
row.last_viewed_at = now
|
| 69 |
+
return row
|
| 70 |
+
row = ShadowingUserHistory(user_id=user_id, video_id=video_id, last_viewed_at=now)
|
| 71 |
+
self._db.add(row)
|
| 72 |
+
await self._db.flush()
|
| 73 |
+
await self._db.refresh(row)
|
| 74 |
+
return row
|
| 75 |
+
|
| 76 |
+
async def list_history(self, user_id: int, *, limit: int = 30) -> list[tuple[ShadowingUserHistory, ShadowingVideo | None]]:
|
| 77 |
+
r = await self._db.execute(
|
| 78 |
+
select(ShadowingUserHistory, ShadowingVideo)
|
| 79 |
+
.outerjoin(ShadowingVideo, ShadowingVideo.video_id == ShadowingUserHistory.video_id)
|
| 80 |
+
.where(ShadowingUserHistory.user_id == user_id)
|
| 81 |
+
.order_by(ShadowingUserHistory.last_viewed_at.desc())
|
| 82 |
+
.limit(limit)
|
| 83 |
+
)
|
| 84 |
+
return list(r.all())
|
| 85 |
+
|
| 86 |
+
async def get_history_entry(self, user_id: int, video_id: str) -> ShadowingUserHistory | None:
|
| 87 |
+
r = await self._db.execute(
|
| 88 |
+
select(ShadowingUserHistory).where(
|
| 89 |
+
ShadowingUserHistory.user_id == user_id,
|
| 90 |
+
ShadowingUserHistory.video_id == video_id,
|
| 91 |
+
)
|
| 92 |
+
)
|
| 93 |
+
return r.scalar_one_or_none()
|
| 94 |
+
|
| 95 |
+
async def update_history_display(
|
| 96 |
+
self,
|
| 97 |
+
user_id: int,
|
| 98 |
+
video_id: str,
|
| 99 |
+
*,
|
| 100 |
+
display_title: str | None = None,
|
| 101 |
+
display_level: str | None = None,
|
| 102 |
+
) -> ShadowingUserHistory | None:
|
| 103 |
+
row = await self.get_history_entry(user_id, video_id)
|
| 104 |
+
if not row:
|
| 105 |
+
return None
|
| 106 |
+
if display_title is not None:
|
| 107 |
+
row.display_title = display_title.strip() or None
|
| 108 |
+
if display_level is not None:
|
| 109 |
+
row.display_level = display_level.strip() or None
|
| 110 |
+
return row
|
| 111 |
+
|
| 112 |
+
async def delete_history(self, user_id: int, video_id: str) -> bool:
|
| 113 |
+
r = await self._db.execute(
|
| 114 |
+
delete(ShadowingUserHistory).where(
|
| 115 |
+
ShadowingUserHistory.user_id == user_id,
|
| 116 |
+
ShadowingUserHistory.video_id == video_id,
|
| 117 |
+
)
|
| 118 |
+
)
|
| 119 |
+
return (r.rowcount or 0) > 0
|
backend/app/routers/shadowing.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shadowing API — YouTube transcript pipeline for dictation & shadowing.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
| 8 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 9 |
+
|
| 10 |
+
from app.core.dependencies import get_current_user
|
| 11 |
+
from app.db.database import get_db
|
| 12 |
+
from app.db.models import User
|
| 13 |
+
from app.repositories.shadowing_repository import ShadowingRepository
|
| 14 |
+
from app.schemas import (
|
| 15 |
+
ShadowingHistoryItemOut,
|
| 16 |
+
ShadowingHistoryListOut,
|
| 17 |
+
ShadowingHistoryUpdateRequest,
|
| 18 |
+
ShadowingProcessVideoRequest,
|
| 19 |
+
ShadowingTranslateRequest,
|
| 20 |
+
ShadowingTranslateResponse,
|
| 21 |
+
ShadowingVideoDataOut,
|
| 22 |
+
)
|
| 23 |
+
from app.services.shadowing_service import ShadowingService
|
| 24 |
+
from app.services.shadowing_pronunciation_service import check_pronunciation_from_bytes
|
| 25 |
+
|
| 26 |
+
logger = logging.getLogger(__name__)
|
| 27 |
+
router = APIRouter(prefix="/shadowing", tags=["Shadowing"])
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _svc(db: AsyncSession) -> ShadowingService:
|
| 31 |
+
return ShadowingService(ShadowingRepository(db))
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@router.post("/video/process", response_model=ShadowingVideoDataOut)
|
| 35 |
+
async def process_video(
|
| 36 |
+
body: ShadowingProcessVideoRequest,
|
| 37 |
+
db: AsyncSession = Depends(get_db),
|
| 38 |
+
user: User = Depends(get_current_user),
|
| 39 |
+
):
|
| 40 |
+
"""Extract transcript (captions or Whisper), store in DB, return VideoData."""
|
| 41 |
+
svc = _svc(db)
|
| 42 |
+
try:
|
| 43 |
+
data = await svc.process_url(
|
| 44 |
+
body.url,
|
| 45 |
+
level=body.level,
|
| 46 |
+
translate=body.translate,
|
| 47 |
+
user_id=user.id,
|
| 48 |
+
)
|
| 49 |
+
except ValueError as e:
|
| 50 |
+
raise HTTPException(status_code=400, detail=str(e)) from e
|
| 51 |
+
return data
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@router.get("/video/{video_id}", response_model=ShadowingVideoDataOut)
|
| 55 |
+
async def get_video(
|
| 56 |
+
video_id: str,
|
| 57 |
+
db: AsyncSession = Depends(get_db),
|
| 58 |
+
user: User = Depends(get_current_user),
|
| 59 |
+
):
|
| 60 |
+
svc = _svc(db)
|
| 61 |
+
data = await svc.get_video(video_id)
|
| 62 |
+
if not data:
|
| 63 |
+
raise HTTPException(status_code=404, detail="Video not found. Process it first via POST /shadowing/video/process")
|
| 64 |
+
await svc.record_view(user.id, video_id)
|
| 65 |
+
return data
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@router.get("/history", response_model=ShadowingHistoryListOut)
|
| 69 |
+
async def list_history(
|
| 70 |
+
limit: int = 30,
|
| 71 |
+
db: AsyncSession = Depends(get_db),
|
| 72 |
+
user: User = Depends(get_current_user),
|
| 73 |
+
):
|
| 74 |
+
svc = _svc(db)
|
| 75 |
+
items = await svc.list_history(user.id, limit=min(max(limit, 1), 50))
|
| 76 |
+
return ShadowingHistoryListOut(items=items)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@router.post("/history/{video_id}/touch")
|
| 80 |
+
async def touch_history(
|
| 81 |
+
video_id: str,
|
| 82 |
+
db: AsyncSession = Depends(get_db),
|
| 83 |
+
user: User = Depends(get_current_user),
|
| 84 |
+
):
|
| 85 |
+
svc = _svc(db)
|
| 86 |
+
await svc.record_view(user.id, video_id)
|
| 87 |
+
return {"ok": True}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@router.patch("/history/{video_id}", response_model=ShadowingHistoryItemOut)
|
| 91 |
+
async def update_history_item(
|
| 92 |
+
video_id: str,
|
| 93 |
+
body: ShadowingHistoryUpdateRequest,
|
| 94 |
+
db: AsyncSession = Depends(get_db),
|
| 95 |
+
user: User = Depends(get_current_user),
|
| 96 |
+
):
|
| 97 |
+
if body.title is None and body.level is None:
|
| 98 |
+
raise HTTPException(status_code=400, detail="Provide title and/or level to update")
|
| 99 |
+
svc = _svc(db)
|
| 100 |
+
item = await svc.update_history_item(
|
| 101 |
+
user.id, video_id, title=body.title, level=body.level
|
| 102 |
+
)
|
| 103 |
+
if not item:
|
| 104 |
+
raise HTTPException(status_code=404, detail="History entry not found")
|
| 105 |
+
return item
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@router.delete("/history/{video_id}")
|
| 109 |
+
async def delete_history_item(
|
| 110 |
+
video_id: str,
|
| 111 |
+
db: AsyncSession = Depends(get_db),
|
| 112 |
+
user: User = Depends(get_current_user),
|
| 113 |
+
):
|
| 114 |
+
svc = _svc(db)
|
| 115 |
+
ok = await svc.delete_history_item(user.id, video_id)
|
| 116 |
+
if not ok:
|
| 117 |
+
raise HTTPException(status_code=404, detail="History entry not found")
|
| 118 |
+
return {"ok": True}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@router.post("/translate", response_model=ShadowingTranslateResponse)
|
| 122 |
+
async def translate_segment(
|
| 123 |
+
body: ShadowingTranslateRequest,
|
| 124 |
+
db: AsyncSession = Depends(get_db),
|
| 125 |
+
_user: User = Depends(get_current_user),
|
| 126 |
+
):
|
| 127 |
+
svc = _svc(db)
|
| 128 |
+
translation = await svc.translate_one(body.text, body.from_lang, body.to_lang)
|
| 129 |
+
return ShadowingTranslateResponse(translation=translation)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@router.post("/pronunciation/check")
|
| 133 |
+
async def check_pronunciation(
|
| 134 |
+
file: UploadFile = File(...),
|
| 135 |
+
target_text: str = Form(...),
|
| 136 |
+
_user: User = Depends(get_current_user),
|
| 137 |
+
):
|
| 138 |
+
"""Whisper transcript + pron_scorer model vs target sentence."""
|
| 139 |
+
if not (target_text or "").strip():
|
| 140 |
+
raise HTTPException(status_code=400, detail="target_text is required")
|
| 141 |
+
try:
|
| 142 |
+
data = await check_pronunciation_from_bytes(
|
| 143 |
+
await file.read(),
|
| 144 |
+
file.filename or "audio.webm",
|
| 145 |
+
target_text.strip(),
|
| 146 |
+
)
|
| 147 |
+
return data
|
| 148 |
+
except FileNotFoundError as e:
|
| 149 |
+
raise HTTPException(status_code=503, detail=str(e)) from e
|
| 150 |
+
except Exception as e:
|
| 151 |
+
logger.exception("pronunciation check failed")
|
| 152 |
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
backend/app/routers/vocabulary.py
CHANGED
|
@@ -6,9 +6,12 @@ All business logic lives in VocabService; all DB access in VocabRepository.
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
from fastapi import APIRouter, Depends, Query, status
|
|
|
|
| 9 |
from sqlalchemy import select
|
| 10 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 11 |
|
|
|
|
|
|
|
| 12 |
from app.core.dependencies import get_current_user
|
| 13 |
from app.db.database import get_db
|
| 14 |
from app.db.models import ReadingAnnotation, User
|
|
@@ -205,6 +208,30 @@ async def delete_word(
|
|
| 205 |
|
| 206 |
# ═══ Search & Stats ═══════════════════════════════════════════════════════════
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
@router.get("/words/search", response_model=list[VocabWordResponse])
|
| 209 |
async def search_words(
|
| 210 |
q: str = Query(min_length=1, description="Search query (word or meaning)"),
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
from fastapi import APIRouter, Depends, Query, status
|
| 9 |
+
from fastapi.responses import StreamingResponse
|
| 10 |
from sqlalchemy import select
|
| 11 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 12 |
|
| 13 |
+
from app.services.vocab_lookup_service import stream_word_lookup
|
| 14 |
+
|
| 15 |
from app.core.dependencies import get_current_user
|
| 16 |
from app.db.database import get_db
|
| 17 |
from app.db.models import ReadingAnnotation, User
|
|
|
|
| 208 |
|
| 209 |
# ═══ Search & Stats ═══════════════════════════════════════════════════════════
|
| 210 |
|
| 211 |
+
@router.get("/lookup/stream")
|
| 212 |
+
async def lookup_word_stream(
|
| 213 |
+
word: str = Query(..., min_length=1, max_length=80, description="English word to look up"),
|
| 214 |
+
_user: User = Depends(get_current_user),
|
| 215 |
+
):
|
| 216 |
+
"""
|
| 217 |
+
SSE stream: partial field patches while OpenRouter generates, then final result.
|
| 218 |
+
Events: data: {"patch": {...}} | {"done": true, "result": {...}} | {"error": "..."}
|
| 219 |
+
"""
|
| 220 |
+
async def event_generator():
|
| 221 |
+
async for chunk in stream_word_lookup(word.strip()):
|
| 222 |
+
yield chunk
|
| 223 |
+
|
| 224 |
+
return StreamingResponse(
|
| 225 |
+
event_generator(),
|
| 226 |
+
media_type="text/event-stream",
|
| 227 |
+
headers={
|
| 228 |
+
"Cache-Control": "no-cache",
|
| 229 |
+
"Connection": "keep-alive",
|
| 230 |
+
"X-Accel-Buffering": "no",
|
| 231 |
+
},
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
@router.get("/words/search", response_model=list[VocabWordResponse])
|
| 236 |
async def search_words(
|
| 237 |
q: str = Query(min_length=1, description="Search query (word or meaning)"),
|
backend/app/schemas.py
CHANGED
|
@@ -419,3 +419,61 @@ class AnnotationResponse(BaseModel):
|
|
| 419 |
note: Optional[str]
|
| 420 |
updated_at: datetime
|
| 421 |
model_config = {"from_attributes": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
note: Optional[str]
|
| 420 |
updated_at: datetime
|
| 421 |
model_config = {"from_attributes": True}
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
# ═══ Shadowing ═════════════════════════════════
|
| 425 |
+
class ShadowingSegmentOut(BaseModel):
|
| 426 |
+
id: int
|
| 427 |
+
text: str
|
| 428 |
+
start: float
|
| 429 |
+
duration: float
|
| 430 |
+
translation: Optional[str] = None
|
| 431 |
+
language: Optional[str] = None
|
| 432 |
+
flagged: bool = False
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
class ShadowingVideoDataOut(BaseModel):
|
| 436 |
+
video_id: str
|
| 437 |
+
title: str
|
| 438 |
+
level: str
|
| 439 |
+
language: str
|
| 440 |
+
segments: list[ShadowingSegmentOut]
|
| 441 |
+
transcript_source: Optional[str] = None
|
| 442 |
+
source_url: Optional[str] = None
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
class ShadowingProcessVideoRequest(BaseModel):
|
| 446 |
+
url: str = Field(..., min_length=8)
|
| 447 |
+
level: str = Field(default="Intermediate", max_length=50)
|
| 448 |
+
translate: bool = Field(default=True)
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
class ShadowingTranslateRequest(BaseModel):
|
| 452 |
+
text: str = Field(..., min_length=1, max_length=5000)
|
| 453 |
+
from_lang: str = Field(default="en", max_length=10)
|
| 454 |
+
to_lang: str = Field(default="vi", max_length=10)
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
class ShadowingTranslateResponse(BaseModel):
|
| 458 |
+
translation: str
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
class ShadowingHistoryItemOut(BaseModel):
|
| 462 |
+
video_id: str
|
| 463 |
+
title: str
|
| 464 |
+
level: str
|
| 465 |
+
language: str
|
| 466 |
+
segment_count: int = 0
|
| 467 |
+
transcript_source: Optional[str] = None
|
| 468 |
+
source_url: Optional[str] = None
|
| 469 |
+
thumbnail_url: Optional[str] = None
|
| 470 |
+
last_viewed_at: datetime
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
class ShadowingHistoryListOut(BaseModel):
|
| 474 |
+
items: list[ShadowingHistoryItemOut]
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
class ShadowingHistoryUpdateRequest(BaseModel):
|
| 478 |
+
title: Optional[str] = Field(None, max_length=500)
|
| 479 |
+
level: Optional[str] = Field(None, max_length=50)
|
backend/app/services/shadowing_pronunciation_service.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shadowing pronunciation check — Whisper + pron_scorer_best.pt."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
import logging
|
| 7 |
+
import re
|
| 8 |
+
import tempfile
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _normalize_token(w: str) -> str:
|
| 15 |
+
return re.sub(r"[^\w']", "", w.lower())
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _align_words(spoken: str, target: str) -> dict:
|
| 19 |
+
target_tokens = [_normalize_token(w) for w in target.split() if _normalize_token(w)]
|
| 20 |
+
spoken_tokens = [_normalize_token(w) for w in spoken.split() if _normalize_token(w)]
|
| 21 |
+
target_words = target.split()
|
| 22 |
+
|
| 23 |
+
word_results = []
|
| 24 |
+
correct_count = 0
|
| 25 |
+
max_len = max(len(target_tokens), len(spoken_tokens))
|
| 26 |
+
for i in range(max_len):
|
| 27 |
+
t = target_tokens[i] if i < len(target_tokens) else None
|
| 28 |
+
s = spoken_tokens[i] if i < len(spoken_tokens) else None
|
| 29 |
+
word = target_words[i] if i < len(target_words) else (spoken.split()[i] if i < len(spoken.split()) else "")
|
| 30 |
+
ok = bool(t and s and t == s)
|
| 31 |
+
if ok:
|
| 32 |
+
correct_count += 1
|
| 33 |
+
word_results.append({
|
| 34 |
+
"word": word or t or s or "",
|
| 35 |
+
"ok": ok,
|
| 36 |
+
"spoken": spoken.split()[i] if i < len(spoken.split()) else None,
|
| 37 |
+
})
|
| 38 |
+
|
| 39 |
+
score = round((correct_count / len(target_tokens)) * 100) if target_tokens else 0
|
| 40 |
+
wrong = [w["word"] for w in word_results if not w["ok"]]
|
| 41 |
+
return {
|
| 42 |
+
"word_results": word_results,
|
| 43 |
+
"score": score,
|
| 44 |
+
"wrong": wrong,
|
| 45 |
+
"correct_count": correct_count,
|
| 46 |
+
"total_words": len(target_tokens),
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
async def check_pronunciation_from_bytes(
|
| 51 |
+
audio_bytes: bytes,
|
| 52 |
+
filename: str,
|
| 53 |
+
target_text: str,
|
| 54 |
+
) -> dict:
|
| 55 |
+
"""Run Whisper + pronunciation model; align transcript to target sentence."""
|
| 56 |
+
from app.routers.speaking import (
|
| 57 |
+
_convert_to_wav,
|
| 58 |
+
_load_audio_16k,
|
| 59 |
+
_run_pronunciation,
|
| 60 |
+
_run_whisper,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
suffix = Path(filename or "audio.webm").suffix or ".webm"
|
| 64 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
| 65 |
+
tmp.write(audio_bytes)
|
| 66 |
+
tmp_path = tmp.name
|
| 67 |
+
|
| 68 |
+
wav_path = tmp_path
|
| 69 |
+
try:
|
| 70 |
+
if suffix.lower() != ".wav":
|
| 71 |
+
try:
|
| 72 |
+
wav_path = _convert_to_wav(tmp_path)
|
| 73 |
+
except Exception as exc:
|
| 74 |
+
logger.warning("wav convert failed: %s", exc)
|
| 75 |
+
|
| 76 |
+
audio_16k = await asyncio.to_thread(_load_audio_16k, wav_path)
|
| 77 |
+
pron_result, whisper_result = await asyncio.gather(
|
| 78 |
+
asyncio.to_thread(_run_pronunciation, audio_16k),
|
| 79 |
+
asyncio.to_thread(_run_whisper, wav_path),
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
transcript = (whisper_result or {}).get("transcript", "").strip()
|
| 83 |
+
alignment = _align_words(transcript, target_text)
|
| 84 |
+
|
| 85 |
+
pron = pron_result or {}
|
| 86 |
+
model_total = float(pron.get("total", 0) or 0)
|
| 87 |
+
# Blend text alignment (0-100) with model score (0-10 → 0-100)
|
| 88 |
+
combined = round(alignment["score"] * 0.55 + min(100, model_total * 10) * 0.45)
|
| 89 |
+
|
| 90 |
+
return {
|
| 91 |
+
"score": combined,
|
| 92 |
+
"text_score": alignment["score"],
|
| 93 |
+
"transcript": transcript,
|
| 94 |
+
"target_text": target_text,
|
| 95 |
+
"word_results": alignment["word_results"],
|
| 96 |
+
"wrong_words": alignment["wrong"],
|
| 97 |
+
"pronunciation": {
|
| 98 |
+
"accuracy": float(pron.get("accuracy", 0)),
|
| 99 |
+
"fluency": float(pron.get("fluency", 0)),
|
| 100 |
+
"prosodic": float(pron.get("prosodic", 0)),
|
| 101 |
+
"total": model_total,
|
| 102 |
+
},
|
| 103 |
+
}
|
| 104 |
+
finally:
|
| 105 |
+
for p in {tmp_path, wav_path}:
|
| 106 |
+
try:
|
| 107 |
+
Path(p).unlink(missing_ok=True)
|
| 108 |
+
except OSError:
|
| 109 |
+
pass
|
backend/app/services/shadowing_service.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shadowing pipeline: YouTube captions → normalize → optional translate → DB cache.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import httpx
|
| 12 |
+
|
| 13 |
+
from app.repositories.shadowing_repository import ShadowingRepository
|
| 14 |
+
from app.services.shadowing_whisper_service import AudioTranscriptionError, transcribe_youtube_audio
|
| 15 |
+
from app.services.translate_service import translate_text
|
| 16 |
+
from app.services.youtube_transcript_service import TranscriptNotFoundError, fetch_youtube_transcript
|
| 17 |
+
from app.utils.segment_utils import extract_youtube_video_id, normalize_segments
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ShadowingService:
|
| 23 |
+
def __init__(self, repo: ShadowingRepository):
|
| 24 |
+
self._repo = repo
|
| 25 |
+
|
| 26 |
+
async def get_video(self, video_id: str) -> dict[str, Any] | None:
|
| 27 |
+
row = await self._repo.get_by_video_id(video_id)
|
| 28 |
+
if not row:
|
| 29 |
+
return None
|
| 30 |
+
return self._to_video_data(row)
|
| 31 |
+
|
| 32 |
+
async def process_url(
|
| 33 |
+
self,
|
| 34 |
+
url: str,
|
| 35 |
+
*,
|
| 36 |
+
level: str = "Intermediate",
|
| 37 |
+
translate: bool = True,
|
| 38 |
+
user_id: int | None = None,
|
| 39 |
+
force_refresh: bool = False,
|
| 40 |
+
) -> dict[str, Any]:
|
| 41 |
+
video_id = extract_youtube_video_id(url)
|
| 42 |
+
if not video_id:
|
| 43 |
+
raise ValueError("Invalid YouTube URL")
|
| 44 |
+
|
| 45 |
+
if not force_refresh:
|
| 46 |
+
cached = await self.get_video(video_id)
|
| 47 |
+
if cached:
|
| 48 |
+
return cached
|
| 49 |
+
|
| 50 |
+
title = await self._fetch_video_title(video_id, url)
|
| 51 |
+
raw: list[dict[str, Any]]
|
| 52 |
+
language: str
|
| 53 |
+
source: str
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
raw, language = await asyncio.to_thread(fetch_youtube_transcript, video_id)
|
| 57 |
+
source = "youtube"
|
| 58 |
+
except TranscriptNotFoundError:
|
| 59 |
+
logger.info("No YouTube captions for %s — Whisper fallback", video_id)
|
| 60 |
+
try:
|
| 61 |
+
raw, language = await asyncio.to_thread(transcribe_youtube_audio, video_id)
|
| 62 |
+
source = "whisper"
|
| 63 |
+
except AudioTranscriptionError as e:
|
| 64 |
+
raise ValueError(str(e)) from e
|
| 65 |
+
|
| 66 |
+
segments = normalize_segments(raw, language=language)
|
| 67 |
+
|
| 68 |
+
if translate and segments:
|
| 69 |
+
segments = await self._translate_segments(segments, language)
|
| 70 |
+
|
| 71 |
+
row = await self._repo.upsert(
|
| 72 |
+
video_id=video_id,
|
| 73 |
+
title=title,
|
| 74 |
+
level=level or "Intermediate",
|
| 75 |
+
language=language,
|
| 76 |
+
source_url=url.strip(),
|
| 77 |
+
transcript_source=source,
|
| 78 |
+
segments=segments,
|
| 79 |
+
created_by=user_id,
|
| 80 |
+
)
|
| 81 |
+
if user_id:
|
| 82 |
+
await self._repo.record_view(user_id, video_id)
|
| 83 |
+
return self._to_video_data(row)
|
| 84 |
+
|
| 85 |
+
async def record_view(self, user_id: int, video_id: str) -> None:
|
| 86 |
+
row = await self._repo.get_by_video_id(video_id)
|
| 87 |
+
if not row:
|
| 88 |
+
return
|
| 89 |
+
await self._repo.record_view(user_id, video_id)
|
| 90 |
+
|
| 91 |
+
async def list_history(self, user_id: int, *, limit: int = 30) -> list[dict[str, Any]]:
|
| 92 |
+
rows = await self._repo.list_history(user_id, limit=limit)
|
| 93 |
+
items: list[dict[str, Any]] = []
|
| 94 |
+
for hist, video in rows:
|
| 95 |
+
items.append(self._history_item_dict(hist, video))
|
| 96 |
+
return items
|
| 97 |
+
|
| 98 |
+
async def update_history_item(
|
| 99 |
+
self,
|
| 100 |
+
user_id: int,
|
| 101 |
+
video_id: str,
|
| 102 |
+
*,
|
| 103 |
+
title: str | None = None,
|
| 104 |
+
level: str | None = None,
|
| 105 |
+
) -> dict[str, Any] | None:
|
| 106 |
+
row = await self._repo.update_history_display(
|
| 107 |
+
user_id, video_id, display_title=title, display_level=level
|
| 108 |
+
)
|
| 109 |
+
if not row:
|
| 110 |
+
return None
|
| 111 |
+
video = await self._repo.get_by_video_id(video_id)
|
| 112 |
+
return self._history_item_dict(row, video)
|
| 113 |
+
|
| 114 |
+
async def delete_history_item(self, user_id: int, video_id: str) -> bool:
|
| 115 |
+
return await self._repo.delete_history(user_id, video_id)
|
| 116 |
+
|
| 117 |
+
@staticmethod
|
| 118 |
+
def _thumbnail_url(video_id: str) -> str:
|
| 119 |
+
return f"https://i.ytimg.com/vi/{video_id}/mqdefault.jpg"
|
| 120 |
+
|
| 121 |
+
def _history_item_dict(self, hist, video) -> dict[str, Any]:
|
| 122 |
+
segs = (video.segments if video else None) or []
|
| 123 |
+
base_title = (video.title if video else None) or hist.video_id
|
| 124 |
+
base_level = (video.level if video else None) or "Intermediate"
|
| 125 |
+
return {
|
| 126 |
+
"video_id": hist.video_id,
|
| 127 |
+
"title": hist.display_title or base_title,
|
| 128 |
+
"level": hist.display_level or base_level,
|
| 129 |
+
"language": (video.language if video else None) or "en",
|
| 130 |
+
"transcript_source": video.transcript_source if video else None,
|
| 131 |
+
"source_url": video.source_url if video else None,
|
| 132 |
+
"segment_count": len(segs),
|
| 133 |
+
"thumbnail_url": self._thumbnail_url(hist.video_id),
|
| 134 |
+
"last_viewed_at": hist.last_viewed_at,
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
async def translate_one(self, text: str, from_lang: str = "en", to_lang: str = "vi") -> str:
|
| 138 |
+
return await translate_text(text, from_lang, to_lang)
|
| 139 |
+
|
| 140 |
+
async def _translate_segments(
|
| 141 |
+
self,
|
| 142 |
+
segments: list[dict[str, Any]],
|
| 143 |
+
from_lang: str,
|
| 144 |
+
) -> list[dict[str, Any]]:
|
| 145 |
+
out: list[dict[str, Any]] = []
|
| 146 |
+
for seg in segments:
|
| 147 |
+
tr = await translate_text(seg["text"], from_lang=from_lang, to_lang="vi")
|
| 148 |
+
out.append({**seg, "translation": tr})
|
| 149 |
+
return out
|
| 150 |
+
|
| 151 |
+
@staticmethod
|
| 152 |
+
async def _fetch_video_title(video_id: str, fallback_url: str) -> str:
|
| 153 |
+
oembed = f"https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v={video_id}&format=json"
|
| 154 |
+
try:
|
| 155 |
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
| 156 |
+
r = await client.get(oembed)
|
| 157 |
+
if r.status_code == 200:
|
| 158 |
+
return r.json().get("title") or video_id
|
| 159 |
+
except Exception:
|
| 160 |
+
pass
|
| 161 |
+
return fallback_url
|
| 162 |
+
|
| 163 |
+
@staticmethod
|
| 164 |
+
def _to_video_data(row) -> dict[str, Any]:
|
| 165 |
+
segs = row.segments or []
|
| 166 |
+
return {
|
| 167 |
+
"video_id": row.video_id,
|
| 168 |
+
"title": row.title or row.video_id,
|
| 169 |
+
"level": row.level or "Intermediate",
|
| 170 |
+
"language": row.language or "en",
|
| 171 |
+
"transcript_source": row.transcript_source,
|
| 172 |
+
"source_url": row.source_url,
|
| 173 |
+
"segments": segs,
|
| 174 |
+
}
|
backend/app/services/shadowing_whisper_service.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fallback: download audio with yt-dlp and transcribe with Whisper (sentence-level segments).
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
import os
|
| 9 |
+
import re
|
| 10 |
+
import subprocess
|
| 11 |
+
import tempfile
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class AudioTranscriptionError(Exception):
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _download_audio(video_id: str, out_dir: Path) -> str:
|
| 23 |
+
url = f"https://www.youtube.com/watch?v={video_id}"
|
| 24 |
+
out_template = str(out_dir / "%(id)s.%(ext)s")
|
| 25 |
+
import shutil
|
| 26 |
+
ytdlp = shutil.which("yt-dlp") or "yt-dlp"
|
| 27 |
+
cmd = [
|
| 28 |
+
ytdlp,
|
| 29 |
+
"-f", "bestaudio[ext=m4a]/bestaudio/best",
|
| 30 |
+
"--extract-audio",
|
| 31 |
+
"--audio-format", "wav",
|
| 32 |
+
"--audio-quality", "0",
|
| 33 |
+
"-o", out_template,
|
| 34 |
+
"--no-playlist",
|
| 35 |
+
"--quiet",
|
| 36 |
+
url,
|
| 37 |
+
]
|
| 38 |
+
try:
|
| 39 |
+
subprocess.run(cmd, check=True, capture_output=True, timeout=300)
|
| 40 |
+
except subprocess.CalledProcessError as e:
|
| 41 |
+
raise AudioTranscriptionError(f"yt-dlp failed: {e.stderr.decode(errors='ignore')[:500]}") from e
|
| 42 |
+
except FileNotFoundError:
|
| 43 |
+
cmd[0] = "python"
|
| 44 |
+
cmd.insert(1, "-m")
|
| 45 |
+
cmd.insert(2, "yt_dlp")
|
| 46 |
+
try:
|
| 47 |
+
subprocess.run(cmd, check=True, capture_output=True, timeout=300)
|
| 48 |
+
except Exception as e2:
|
| 49 |
+
raise AudioTranscriptionError("yt-dlp not installed (pip install yt-dlp)") from e2
|
| 50 |
+
|
| 51 |
+
for ext in (".wav", ".m4a", ".webm", ".mp3", ".opus"):
|
| 52 |
+
p = out_dir / f"{video_id}{ext}"
|
| 53 |
+
if p.exists():
|
| 54 |
+
return str(p)
|
| 55 |
+
for f in out_dir.iterdir():
|
| 56 |
+
if f.suffix in (".wav", ".m4a", ".webm", ".mp3", ".opus"):
|
| 57 |
+
return str(f)
|
| 58 |
+
raise AudioTranscriptionError("Downloaded audio file not found")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _whisper_segments(wav_path: str) -> tuple[list[dict[str, Any]], str]:
|
| 62 |
+
from ml.model_registry import get_whisper_model
|
| 63 |
+
|
| 64 |
+
model = get_whisper_model()
|
| 65 |
+
result = model.transcribe(wav_path, verbose=False)
|
| 66 |
+
language = (result.get("language") or "en").split("-")[0].lower()
|
| 67 |
+
|
| 68 |
+
raw: list[dict[str, Any]] = []
|
| 69 |
+
for seg in result.get("segments", []):
|
| 70 |
+
text = (seg.get("text") or "").strip()
|
| 71 |
+
if not text:
|
| 72 |
+
continue
|
| 73 |
+
raw.append({
|
| 74 |
+
"text": text,
|
| 75 |
+
"start": float(seg.get("start", 0)),
|
| 76 |
+
"duration": max(0.1, float(seg.get("end", 0)) - float(seg.get("start", 0))),
|
| 77 |
+
})
|
| 78 |
+
|
| 79 |
+
if not raw:
|
| 80 |
+
full = (result.get("text") or "").strip()
|
| 81 |
+
if full:
|
| 82 |
+
sentences = re.split(r"(?<=[.!?])\s+", full)
|
| 83 |
+
t = 0.0
|
| 84 |
+
for s in sentences:
|
| 85 |
+
s = s.strip()
|
| 86 |
+
if not s:
|
| 87 |
+
continue
|
| 88 |
+
dur = max(2.0, len(s.split()) * 0.35)
|
| 89 |
+
raw.append({"text": s, "start": t, "duration": dur})
|
| 90 |
+
t += dur
|
| 91 |
+
|
| 92 |
+
return raw, language
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def transcribe_youtube_audio(video_id: str) -> tuple[list[dict[str, Any]], str]:
|
| 96 |
+
"""Download + Whisper. Returns raw caption-style entries."""
|
| 97 |
+
with tempfile.TemporaryDirectory(prefix="shadowing_") as tmp:
|
| 98 |
+
tmp_path = Path(tmp)
|
| 99 |
+
audio_path = _download_audio(video_id, tmp_path)
|
| 100 |
+
if not audio_path.endswith(".wav"):
|
| 101 |
+
wav_out = str(tmp_path / "converted.wav")
|
| 102 |
+
try:
|
| 103 |
+
subprocess.run(
|
| 104 |
+
["ffmpeg", "-y", "-i", audio_path, "-ar", "16000", "-ac", "1", wav_out],
|
| 105 |
+
check=True,
|
| 106 |
+
capture_output=True,
|
| 107 |
+
timeout=120,
|
| 108 |
+
)
|
| 109 |
+
audio_path = wav_out
|
| 110 |
+
except Exception:
|
| 111 |
+
pass
|
| 112 |
+
return _whisper_segments(audio_path)
|
backend/app/services/translate_service.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Lightweight translation via Google Translate public endpoint (no API key).
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
import urllib.parse
|
| 9 |
+
|
| 10 |
+
import httpx
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
async def translate_text(
|
| 16 |
+
text: str,
|
| 17 |
+
from_lang: str = "en",
|
| 18 |
+
to_lang: str = "vi",
|
| 19 |
+
) -> str:
|
| 20 |
+
text = (text or "").strip()
|
| 21 |
+
if not text:
|
| 22 |
+
return ""
|
| 23 |
+
|
| 24 |
+
params = {
|
| 25 |
+
"client": "gtx",
|
| 26 |
+
"sl": from_lang or "auto",
|
| 27 |
+
"tl": to_lang or "vi",
|
| 28 |
+
"dt": "t",
|
| 29 |
+
"q": text,
|
| 30 |
+
}
|
| 31 |
+
url = "https://translate.googleapis.com/translate_a/single?" + urllib.parse.urlencode(params)
|
| 32 |
+
|
| 33 |
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 34 |
+
r = await client.get(url)
|
| 35 |
+
r.raise_for_status()
|
| 36 |
+
data = r.json()
|
| 37 |
+
|
| 38 |
+
if not data or not isinstance(data[0], list):
|
| 39 |
+
return ""
|
| 40 |
+
|
| 41 |
+
parts = [chunk[0] for chunk in data[0] if chunk and chunk[0]]
|
| 42 |
+
result = "".join(parts).strip()
|
| 43 |
+
logger.debug("Translated %d chars %s→%s", len(text), from_lang, to_lang)
|
| 44 |
+
return result
|
backend/app/services/vocab_lookup_service.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Vocabulary lookup via OpenRouter (streaming NDJSON) with dictionary fallback.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
import re
|
| 10 |
+
from collections.abc import AsyncIterator
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
import httpx
|
| 14 |
+
|
| 15 |
+
from app.core.config import settings
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
_OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
|
| 20 |
+
_FAST_MODEL = (settings.OPENROUTER_FAST_MODEL or "google/gemini-2.0-flash-001").strip()
|
| 21 |
+
_FALLBACK_MODEL = "google/gemini-2.0-flash-001"
|
| 22 |
+
_TIMEOUT = 25.0
|
| 23 |
+
|
| 24 |
+
_SYSTEM = """You are a concise English–Vietnamese dictionary for IELTS learners.
|
| 25 |
+
Output ONLY newline-delimited JSON (NDJSON), one object per line, no markdown.
|
| 26 |
+
Emit lines in this order (skip only if truly unknown):
|
| 27 |
+
{"field":"phonetic","value":"IPA without slashes"}
|
| 28 |
+
{"field":"word_type","value":"noun"}
|
| 29 |
+
{"field":"meaning_en","value":"Clear English definition(s), semicolon-separated if multiple"}
|
| 30 |
+
{"field":"meaning_vi","value":"Natural Vietnamese translation"}
|
| 31 |
+
{"field":"example","value":"One short example sentence in English"}
|
| 32 |
+
{"field":"example_vi","value":"Vietnamese translation of the example"}
|
| 33 |
+
{"field":"all_meanings","value":[{"type":"noun","defs":["def1"],"example":"..."}]}
|
| 34 |
+
The all_meanings value must be a JSON array. Keep responses accurate and concise."""
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _openrouter_key() -> str:
|
| 38 |
+
return (settings.OPENROUTER_API_KEY or "").strip()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _model_candidates() -> list[str]:
|
| 42 |
+
out: list[str] = []
|
| 43 |
+
for m in (_FAST_MODEL, _FALLBACK_MODEL, "anthropic/claude-3-haiku"):
|
| 44 |
+
if m and m not in out:
|
| 45 |
+
out.append(m)
|
| 46 |
+
return out
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _headers() -> dict[str, str]:
|
| 50 |
+
return {
|
| 51 |
+
"Authorization": f"Bearer {_openrouter_key()}",
|
| 52 |
+
"Content-Type": "application/json",
|
| 53 |
+
"HTTP-Referer": "http://localhost:5173",
|
| 54 |
+
"X-Title": "LinguaIELTS Vocab Lookup",
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _empty_result(word: str) -> dict[str, Any]:
|
| 59 |
+
return {
|
| 60 |
+
"word": word,
|
| 61 |
+
"phonetic": "",
|
| 62 |
+
"word_type": "",
|
| 63 |
+
"meaning_en": "",
|
| 64 |
+
"meaning_vi": "",
|
| 65 |
+
"example": "",
|
| 66 |
+
"example_vi": "",
|
| 67 |
+
"audio": "",
|
| 68 |
+
"allMeanings": [],
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _apply_field(result: dict[str, Any], field: str, value: Any) -> None:
|
| 73 |
+
if field == "all_meanings":
|
| 74 |
+
if isinstance(value, list):
|
| 75 |
+
result["allMeanings"] = value
|
| 76 |
+
elif isinstance(value, str):
|
| 77 |
+
try:
|
| 78 |
+
result["allMeanings"] = json.loads(value)
|
| 79 |
+
except json.JSONDecodeError:
|
| 80 |
+
pass
|
| 81 |
+
return
|
| 82 |
+
key_map = {
|
| 83 |
+
"phonetic": "phonetic",
|
| 84 |
+
"word_type": "word_type",
|
| 85 |
+
"meaning_en": "meaning_en",
|
| 86 |
+
"meaning_vi": "meaning_vi",
|
| 87 |
+
"example": "example",
|
| 88 |
+
"example_vi": "example_vi",
|
| 89 |
+
}
|
| 90 |
+
if field in key_map and value is not None:
|
| 91 |
+
result[key_map[field]] = str(value).strip() if not isinstance(value, (list, dict)) else value
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _parse_ndjson_line(line: str) -> tuple[str, Any] | None:
|
| 95 |
+
line = line.strip()
|
| 96 |
+
if not line or line.startswith("```"):
|
| 97 |
+
return None
|
| 98 |
+
try:
|
| 99 |
+
obj = json.loads(line)
|
| 100 |
+
except json.JSONDecodeError:
|
| 101 |
+
return None
|
| 102 |
+
field = obj.get("field")
|
| 103 |
+
if not field:
|
| 104 |
+
return None
|
| 105 |
+
return field, obj.get("value")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
async def _fallback_lookup(word: str) -> dict[str, Any]:
|
| 109 |
+
"""Free Dictionary + MyMemory when OpenRouter unavailable."""
|
| 110 |
+
result = _empty_result(word)
|
| 111 |
+
async with httpx.AsyncClient(timeout=8.0) as client:
|
| 112 |
+
try:
|
| 113 |
+
r = await client.get(
|
| 114 |
+
f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
|
| 115 |
+
)
|
| 116 |
+
if r.is_success:
|
| 117 |
+
entry = r.json()[0]
|
| 118 |
+
result["word"] = entry.get("word") or word
|
| 119 |
+
result["phonetic"] = _extract_phonetic(entry)
|
| 120 |
+
result["word_type"] = (entry.get("meanings") or [{}])[0].get("partOfSpeech", "")
|
| 121 |
+
result["meaning_en"] = _build_en_gloss(entry)
|
| 122 |
+
result["example"] = _first_example(entry)
|
| 123 |
+
result["allMeanings"] = _build_all_meanings(entry)
|
| 124 |
+
for p in entry.get("phonetics") or []:
|
| 125 |
+
if p.get("audio"):
|
| 126 |
+
result["audio"] = p["audio"]
|
| 127 |
+
break
|
| 128 |
+
except Exception as exc:
|
| 129 |
+
logger.debug("Dictionary API fallback failed: %s", exc)
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
r2 = await client.get(
|
| 133 |
+
"https://api.mymemory.translated.net/get",
|
| 134 |
+
params={"q": word, "langpair": "en|vi", "de": "a@b.com"},
|
| 135 |
+
)
|
| 136 |
+
if r2.is_success:
|
| 137 |
+
tr = r2.json().get("responseData", {}).get("translatedText", "")
|
| 138 |
+
if tr and tr.lower() != word.lower() and "MYMEMORY WARNING" not in tr.upper():
|
| 139 |
+
result["meaning_vi"] = tr
|
| 140 |
+
except Exception as exc:
|
| 141 |
+
logger.debug("MyMemory fallback failed: %s", exc)
|
| 142 |
+
|
| 143 |
+
if result["example"]:
|
| 144 |
+
try:
|
| 145 |
+
r3 = await client.get(
|
| 146 |
+
"https://api.mymemory.translated.net/get",
|
| 147 |
+
params={
|
| 148 |
+
"q": result["example"][:200],
|
| 149 |
+
"langpair": "en|vi",
|
| 150 |
+
"de": "a@b.com",
|
| 151 |
+
},
|
| 152 |
+
)
|
| 153 |
+
if r3.is_success:
|
| 154 |
+
tr = r3.json().get("responseData", {}).get("translatedText", "")
|
| 155 |
+
if tr:
|
| 156 |
+
result["example_vi"] = tr
|
| 157 |
+
except Exception:
|
| 158 |
+
pass
|
| 159 |
+
|
| 160 |
+
return result
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _extract_phonetic(entry: dict) -> str:
|
| 164 |
+
if entry.get("phonetic"):
|
| 165 |
+
return str(entry["phonetic"]).strip("/ ")
|
| 166 |
+
for p in entry.get("phonetics") or []:
|
| 167 |
+
if p.get("text"):
|
| 168 |
+
return str(p["text"]).strip("/ ")
|
| 169 |
+
return ""
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _build_en_gloss(entry: dict) -> str:
|
| 173 |
+
parts: list[str] = []
|
| 174 |
+
for m in (entry.get("meanings") or [])[:3]:
|
| 175 |
+
pos = m.get("partOfSpeech", "")
|
| 176 |
+
prefix = f"({pos}) " if pos else ""
|
| 177 |
+
for d in (m.get("definitions") or [])[:2]:
|
| 178 |
+
if d.get("definition"):
|
| 179 |
+
parts.append(f"{prefix}{d['definition']}")
|
| 180 |
+
return "; ".join(parts)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _first_example(entry: dict) -> str:
|
| 184 |
+
for m in entry.get("meanings") or []:
|
| 185 |
+
for d in m.get("definitions") or []:
|
| 186 |
+
if d.get("example"):
|
| 187 |
+
return d["example"]
|
| 188 |
+
return ""
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _build_all_meanings(entry: dict) -> list[dict]:
|
| 192 |
+
out = []
|
| 193 |
+
for m in (entry.get("meanings") or [])[:4]:
|
| 194 |
+
defs = [d["definition"] for d in (m.get("definitions") or [])[:3] if d.get("definition")]
|
| 195 |
+
if defs:
|
| 196 |
+
ex = next((d.get("example") for d in m.get("definitions") or [] if d.get("example")), "")
|
| 197 |
+
out.append({"type": m.get("partOfSpeech", ""), "defs": defs, "example": ex or ""})
|
| 198 |
+
return out
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _sse(data: dict) -> str:
|
| 202 |
+
return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
async def stream_word_lookup(word: str) -> AsyncIterator[str]:
|
| 206 |
+
"""Yield SSE events: patch (partial field), done (final), error."""
|
| 207 |
+
clean = re.sub(r"[^a-zA-Z'-]", "", word).lower()
|
| 208 |
+
if not clean:
|
| 209 |
+
yield _sse({"error": "invalid_word"})
|
| 210 |
+
return
|
| 211 |
+
|
| 212 |
+
result = _empty_result(clean)
|
| 213 |
+
|
| 214 |
+
if not _openrouter_key():
|
| 215 |
+
final = await _fallback_lookup(clean)
|
| 216 |
+
yield _sse({"done": True, "result": final})
|
| 217 |
+
return
|
| 218 |
+
|
| 219 |
+
line_buf = ""
|
| 220 |
+
content_buf = ""
|
| 221 |
+
last_exc: Exception | None = None
|
| 222 |
+
|
| 223 |
+
for model_id in _model_candidates():
|
| 224 |
+
payload = {
|
| 225 |
+
"model": model_id,
|
| 226 |
+
"messages": [
|
| 227 |
+
{"role": "system", "content": _SYSTEM},
|
| 228 |
+
{"role": "user", "content": f'Define the English word "{clean}" for an IELTS student.'},
|
| 229 |
+
],
|
| 230 |
+
"temperature": 0.2,
|
| 231 |
+
"max_tokens": 700,
|
| 232 |
+
"stream": True,
|
| 233 |
+
"provider": {"allow_fallbacks": True},
|
| 234 |
+
}
|
| 235 |
+
try:
|
| 236 |
+
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
|
| 237 |
+
async with client.stream(
|
| 238 |
+
"POST",
|
| 239 |
+
_OPENROUTER_URL,
|
| 240 |
+
json=payload,
|
| 241 |
+
headers=_headers(),
|
| 242 |
+
) as resp:
|
| 243 |
+
if resp.status_code == 404 and model_id != _FALLBACK_MODEL:
|
| 244 |
+
logger.warning("OpenRouter model %s unavailable, next fallback", model_id)
|
| 245 |
+
continue
|
| 246 |
+
resp.raise_for_status()
|
| 247 |
+
async for line in resp.aiter_lines():
|
| 248 |
+
if not line.startswith("data: "):
|
| 249 |
+
continue
|
| 250 |
+
data = line[6:].strip()
|
| 251 |
+
if data == "[DONE]":
|
| 252 |
+
break
|
| 253 |
+
try:
|
| 254 |
+
chunk = json.loads(data)
|
| 255 |
+
except json.JSONDecodeError:
|
| 256 |
+
continue
|
| 257 |
+
delta = (
|
| 258 |
+
chunk.get("choices", [{}])[0]
|
| 259 |
+
.get("delta", {})
|
| 260 |
+
.get("content")
|
| 261 |
+
)
|
| 262 |
+
if not delta:
|
| 263 |
+
continue
|
| 264 |
+
content_buf += delta
|
| 265 |
+
line_buf += delta
|
| 266 |
+
while "\n" in line_buf:
|
| 267 |
+
raw_line, line_buf = line_buf.split("\n", 1)
|
| 268 |
+
parsed = _parse_ndjson_line(raw_line)
|
| 269 |
+
if not parsed:
|
| 270 |
+
continue
|
| 271 |
+
field, value = parsed
|
| 272 |
+
_apply_field(result, field, value)
|
| 273 |
+
patch = {k: v for k, v in result.items() if v}
|
| 274 |
+
yield _sse({"patch": patch})
|
| 275 |
+
|
| 276 |
+
# trailing line without newline
|
| 277 |
+
if line_buf.strip():
|
| 278 |
+
parsed = _parse_ndjson_line(line_buf)
|
| 279 |
+
if parsed:
|
| 280 |
+
_apply_field(result, parsed[0], parsed[1])
|
| 281 |
+
yield _sse({"patch": {k: v for k, v in result.items() if v}})
|
| 282 |
+
|
| 283 |
+
# parse full buffer as JSON fallback if NDJSON incomplete
|
| 284 |
+
if not result.get("meaning_en") and content_buf.strip():
|
| 285 |
+
repaired = _try_parse_full_json(content_buf)
|
| 286 |
+
if repaired:
|
| 287 |
+
result.update(repaired)
|
| 288 |
+
|
| 289 |
+
if result.get("meaning_en") or result.get("meaning_vi"):
|
| 290 |
+
yield _sse({"done": True, "result": result})
|
| 291 |
+
return
|
| 292 |
+
|
| 293 |
+
last_exc = RuntimeError("empty stream result")
|
| 294 |
+
except httpx.HTTPStatusError as exc:
|
| 295 |
+
last_exc = exc
|
| 296 |
+
if exc.response.status_code == 404:
|
| 297 |
+
continue
|
| 298 |
+
break
|
| 299 |
+
except Exception as exc:
|
| 300 |
+
last_exc = exc
|
| 301 |
+
logger.warning("Vocab lookup stream error (%s): %s", model_id, exc)
|
| 302 |
+
continue
|
| 303 |
+
|
| 304 |
+
logger.warning("OpenRouter vocab lookup failed, using free APIs: %s", last_exc)
|
| 305 |
+
final = await _fallback_lookup(clean)
|
| 306 |
+
yield _sse({"done": True, "result": final, "fallback": True})
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def _try_parse_full_json(text: str) -> dict[str, Any] | None:
|
| 310 |
+
text = text.strip()
|
| 311 |
+
if text.startswith("```"):
|
| 312 |
+
text = re.sub(r"^```(?:json)?\s*", "", text)
|
| 313 |
+
text = re.sub(r"\s*```$", "", text)
|
| 314 |
+
try:
|
| 315 |
+
obj = json.loads(text)
|
| 316 |
+
except json.JSONDecodeError:
|
| 317 |
+
return None
|
| 318 |
+
out = _empty_result(obj.get("word", ""))
|
| 319 |
+
for k in ("phonetic", "word_type", "meaning_en", "meaning_vi", "example", "example_vi"):
|
| 320 |
+
if obj.get(k):
|
| 321 |
+
out[k] = obj[k]
|
| 322 |
+
if obj.get("all_meanings"):
|
| 323 |
+
out["allMeanings"] = obj["all_meanings"]
|
| 324 |
+
elif obj.get("allMeanings"):
|
| 325 |
+
out["allMeanings"] = obj["allMeanings"]
|
| 326 |
+
return out
|
backend/app/services/youtube_transcript_service.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fetch YouTube captions via youtube-transcript-api (v1+ instance API).
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class TranscriptNotFoundError(Exception):
|
| 14 |
+
pass
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _raw_from_fetched(fetched: Any) -> list[dict[str, Any]]:
|
| 18 |
+
if hasattr(fetched, "to_raw_data"):
|
| 19 |
+
return fetched.to_raw_data()
|
| 20 |
+
raw: list[dict[str, Any]] = []
|
| 21 |
+
for item in fetched:
|
| 22 |
+
if isinstance(item, dict):
|
| 23 |
+
raw.append({
|
| 24 |
+
"text": item.get("text", ""),
|
| 25 |
+
"start": float(item.get("start", 0)),
|
| 26 |
+
"duration": float(item.get("duration", 0.1)),
|
| 27 |
+
})
|
| 28 |
+
else:
|
| 29 |
+
raw.append({
|
| 30 |
+
"text": getattr(item, "text", ""),
|
| 31 |
+
"start": float(getattr(item, "start", 0)),
|
| 32 |
+
"duration": float(getattr(item, "duration", 0.1)),
|
| 33 |
+
})
|
| 34 |
+
return raw
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _language_from_fetched(fetched: Any, fallback: str = "en") -> str:
|
| 38 |
+
code = getattr(fetched, "language_code", None) or getattr(fetched, "language", None) or fallback
|
| 39 |
+
return str(code).split("-")[0].lower()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def fetch_youtube_transcript(video_id: str, preferred_langs: list[str] | None = None) -> tuple[list[dict[str, Any]], str]:
|
| 43 |
+
"""
|
| 44 |
+
Returns (raw_entries, language_code).
|
| 45 |
+
Each entry: { text, start, duration }
|
| 46 |
+
"""
|
| 47 |
+
from youtube_transcript_api import YouTubeTranscriptApi
|
| 48 |
+
from youtube_transcript_api._errors import (
|
| 49 |
+
NoTranscriptFound,
|
| 50 |
+
TranscriptsDisabled,
|
| 51 |
+
VideoUnavailable,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
api = YouTubeTranscriptApi()
|
| 55 |
+
langs = preferred_langs or ["en", "en-US", "en-GB", "vi", "vi-VN"]
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
fetched = api.fetch(video_id, languages=langs)
|
| 59 |
+
raw = _raw_from_fetched(fetched)
|
| 60 |
+
language = _language_from_fetched(fetched)
|
| 61 |
+
except (NoTranscriptFound, TranscriptsDisabled, VideoUnavailable) as e:
|
| 62 |
+
raise TranscriptNotFoundError(str(e)) from e
|
| 63 |
+
except Exception:
|
| 64 |
+
try:
|
| 65 |
+
transcript_list = api.list(video_id)
|
| 66 |
+
transcript = None
|
| 67 |
+
for lang in langs:
|
| 68 |
+
try:
|
| 69 |
+
transcript = transcript_list.find_transcript([lang])
|
| 70 |
+
break
|
| 71 |
+
except Exception:
|
| 72 |
+
continue
|
| 73 |
+
if transcript is None:
|
| 74 |
+
try:
|
| 75 |
+
transcript = transcript_list.find_generated_transcript(["en"])
|
| 76 |
+
except Exception:
|
| 77 |
+
transcript = transcript_list.find_manually_created_transcript(["en"])
|
| 78 |
+
if transcript is None:
|
| 79 |
+
raise TranscriptNotFoundError("No captions available")
|
| 80 |
+
fetched = transcript.fetch()
|
| 81 |
+
raw = _raw_from_fetched(fetched)
|
| 82 |
+
language = _language_from_fetched(
|
| 83 |
+
fetched,
|
| 84 |
+
getattr(transcript, "language_code", "en") or "en",
|
| 85 |
+
)
|
| 86 |
+
except TranscriptNotFoundError:
|
| 87 |
+
raise
|
| 88 |
+
except Exception as e:
|
| 89 |
+
raise TranscriptNotFoundError(str(e)) from e
|
| 90 |
+
|
| 91 |
+
if not raw:
|
| 92 |
+
raise TranscriptNotFoundError("Empty transcript")
|
| 93 |
+
|
| 94 |
+
logger.info("YouTube transcript fetched: video=%s lang=%s lines=%d", video_id, language, len(raw))
|
| 95 |
+
return raw, language
|
backend/app/utils/segment_utils.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Segment normalization for shadowing transcripts.
|
| 3 |
+
Sentence-level chunks, max 10s duration, timestamps to 1 decimal.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import re
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
MAX_SEGMENT_DURATION = 10.0
|
| 12 |
+
_YT_ID_PATTERNS = [
|
| 13 |
+
re.compile(r"(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/)([a-zA-Z0-9_-]{11})"),
|
| 14 |
+
re.compile(r"^([a-zA-Z0-9_-]{11})$"),
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def extract_youtube_video_id(url: str) -> str | None:
|
| 19 |
+
url = (url or "").strip()
|
| 20 |
+
for pat in _YT_ID_PATTERNS:
|
| 21 |
+
m = pat.search(url)
|
| 22 |
+
if m:
|
| 23 |
+
return m.group(1)
|
| 24 |
+
return None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _round_ts(value: float) -> float:
|
| 28 |
+
return round(float(value), 1)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _split_long_segment(text: str, start: float, duration: float) -> list[dict[str, Any]]:
|
| 32 |
+
"""Split a segment longer than MAX_SEGMENT_DURATION into smaller chunks."""
|
| 33 |
+
if duration <= MAX_SEGMENT_DURATION:
|
| 34 |
+
return [{"text": text.strip(), "start": _round_ts(start), "duration": _round_ts(duration)}]
|
| 35 |
+
|
| 36 |
+
words = text.split()
|
| 37 |
+
if len(words) <= 1:
|
| 38 |
+
mid = duration / 2
|
| 39 |
+
return [
|
| 40 |
+
{"text": text.strip(), "start": _round_ts(start), "duration": _round_ts(mid)},
|
| 41 |
+
{"text": text.strip(), "start": _round_ts(start + mid), "duration": _round_ts(duration - mid)},
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
parts: list[dict[str, Any]] = []
|
| 45 |
+
n = max(2, int(duration / MAX_SEGMENT_DURATION) + 1)
|
| 46 |
+
chunk_words = max(1, len(words) // n)
|
| 47 |
+
part_dur = duration / n
|
| 48 |
+
for i in range(n):
|
| 49 |
+
w_start = i * chunk_words
|
| 50 |
+
w_end = len(words) if i == n - 1 else (i + 1) * chunk_words
|
| 51 |
+
chunk_text = " ".join(words[w_start:w_end]).strip()
|
| 52 |
+
if not chunk_text:
|
| 53 |
+
continue
|
| 54 |
+
parts.append({
|
| 55 |
+
"text": chunk_text,
|
| 56 |
+
"start": _round_ts(start + i * part_dur),
|
| 57 |
+
"duration": _round_ts(part_dur if i < n - 1 else max(0.1, duration - i * part_dur)),
|
| 58 |
+
})
|
| 59 |
+
return parts
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _merge_raw_to_sentences(raw: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 63 |
+
"""Merge caption fragments into sentence-level segments using end punctuation."""
|
| 64 |
+
if not raw:
|
| 65 |
+
return []
|
| 66 |
+
|
| 67 |
+
sentences: list[dict[str, Any]] = []
|
| 68 |
+
buf_text: list[str] = []
|
| 69 |
+
buf_start: float | None = None
|
| 70 |
+
buf_end: float = 0.0
|
| 71 |
+
|
| 72 |
+
def flush():
|
| 73 |
+
nonlocal buf_text, buf_start, buf_end
|
| 74 |
+
if not buf_text or buf_start is None:
|
| 75 |
+
return
|
| 76 |
+
text = " ".join(buf_text).strip()
|
| 77 |
+
text = re.sub(r"\s+", " ", text)
|
| 78 |
+
if text:
|
| 79 |
+
duration = max(0.1, buf_end - buf_start)
|
| 80 |
+
sentences.append({
|
| 81 |
+
"text": text,
|
| 82 |
+
"start": _round_ts(buf_start),
|
| 83 |
+
"duration": _round_ts(duration),
|
| 84 |
+
})
|
| 85 |
+
buf_text = []
|
| 86 |
+
buf_start = None
|
| 87 |
+
buf_end = 0.0
|
| 88 |
+
|
| 89 |
+
for entry in raw:
|
| 90 |
+
t = (entry.get("text") or "").strip()
|
| 91 |
+
if not t:
|
| 92 |
+
continue
|
| 93 |
+
start = float(entry.get("start", 0))
|
| 94 |
+
dur = float(entry.get("duration", 0.1))
|
| 95 |
+
end = start + dur
|
| 96 |
+
if buf_start is None:
|
| 97 |
+
buf_start = start
|
| 98 |
+
buf_text.append(t)
|
| 99 |
+
buf_end = end
|
| 100 |
+
if re.search(r"[.!?]\s*$", t):
|
| 101 |
+
flush()
|
| 102 |
+
|
| 103 |
+
flush()
|
| 104 |
+
return sentences
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def normalize_segments(
|
| 108 |
+
raw_entries: list[dict[str, Any]],
|
| 109 |
+
language: str = "en",
|
| 110 |
+
) -> list[dict[str, Any]]:
|
| 111 |
+
"""
|
| 112 |
+
Convert raw transcript entries to numbered sentence-level segments.
|
| 113 |
+
Splits segments longer than 10 seconds.
|
| 114 |
+
"""
|
| 115 |
+
merged = _merge_raw_to_sentences(raw_entries)
|
| 116 |
+
flat: list[dict[str, Any]] = []
|
| 117 |
+
for item in merged:
|
| 118 |
+
flat.extend(_split_long_segment(item["text"], item["start"], item["duration"]))
|
| 119 |
+
|
| 120 |
+
segments: list[dict[str, Any]] = []
|
| 121 |
+
for idx, seg in enumerate(flat, start=1):
|
| 122 |
+
text = (seg.get("text") or "").strip()
|
| 123 |
+
if not text:
|
| 124 |
+
continue
|
| 125 |
+
segments.append({
|
| 126 |
+
"id": idx,
|
| 127 |
+
"text": text,
|
| 128 |
+
"start": seg["start"],
|
| 129 |
+
"duration": seg["duration"],
|
| 130 |
+
"language": language,
|
| 131 |
+
})
|
| 132 |
+
return segments
|
backend/requirements.txt
CHANGED
|
@@ -20,3 +20,5 @@ librosa>=0.10.2
|
|
| 20 |
pydub>=0.25.1
|
| 21 |
httpx>=0.27.0
|
| 22 |
numpy>=1.26.0
|
|
|
|
|
|
|
|
|
| 20 |
pydub>=0.25.1
|
| 21 |
httpx>=0.27.0
|
| 22 |
numpy>=1.26.0
|
| 23 |
+
youtube-transcript-api>=0.6.2
|
| 24 |
+
yt-dlp>=2024.4.9
|
fronted/dist/assets/AudioPlayer-COEgM5Q5.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{q as F,o as p,c as f,a as t,t as k,n as L,j as b,z as y,g as P,F as R,r as E,y as O,b as A,f as S,G as X,p as N,_ as I,e as z,O as V}from"./index-BnN_E-ke.js";const Y={class:"flex flex-col items-center gap-3"},J={class:"relative",style:{width:"140px",height:"140px"}},Q={width:"140",height:"140",viewBox:"0 0 140 140",class:"-rotate-90"},Z=["stroke-dashoffset"],ee={class:"absolute inset-0 flex flex-col items-center justify-center"},te={class:"text-3xl font-extrabold leading-none text-[var(--ink)]"},nt={__name:"BandScoreRing",props:{band:{type:Number,default:0}},setup(o){const n=o,d=2*Math.PI*58,i=b(0);F(()=>setTimeout(()=>{i.value=n.band},120));const g=y(()=>d*(1-Math.min(i.value,9)/9)),x=y(()=>n.band>=7.5?"#34d399":n.band>=6?"#f59e0b":"#f43f5e"),m=y(()=>{const r=n.band;return r>=8.5?"Expert":r>=7.5?"Very Good":r>=7?"Good":r>=6.5?"Competent":r>=6?"Modest":r>=5?"Limited":"Developing"});return(r,s)=>(p(),f("div",Y,[t("div",J,[(p(),f("svg",Q,[s[0]||(s[0]=t("circle",{cx:"70",cy:"70",r:"58",fill:"none",stroke:"#e5e7eb","stroke-width":"12"},null,-1)),t("circle",{cx:"70",cy:"70",r:"58",fill:"none",stroke:"#34d399","stroke-width":"12","stroke-linecap":"round","stroke-dasharray":d,"stroke-dashoffset":g.value,style:{transition:"stroke-dashoffset 1.2s ease"}},null,8,Z)])),t("div",ee,[t("span",te,k(o.band.toFixed(1)),1),s[1]||(s[1]=t("span",{class:"mt-1 text-[9px] font-bold uppercase tracking-widest text-[var(--ink3)]"},"Band",-1))])]),t("span",{class:"rounded-full border px-3 py-0.5 text-[11px] font-semibold",style:L({borderColor:x.value+"66",color:x.value,background:x.value+"11"})},k(m.value),5)]))}},ae={class:"flex flex-col items-center gap-2"},re=["width","height","viewBox"],se=["cx","cy","r","stroke-width"],ne=["cx","cy","r","stroke","stroke-width","stroke-dasharray","stroke-dashoffset"],oe={class:"absolute inset-0 flex flex-col items-center justify-center"},le={class:"text-center text-[11px] font-semibold uppercase tracking-wider text-[var(--ink2)]"},ot={__name:"CircularScore",props:{score:{type:Number,default:0},label:{type:String,default:""},size:{type:Number,default:88}},setup(o){const n=o,d=y(()=>Math.max(6,n.size*.09)),i=y(()=>n.size/2),g=y(()=>n.size/2),x=y(()=>(n.size-d.value)/2),m=y(()=>2*Math.PI*x.value),r=b(0);F(()=>setTimeout(()=>{r.value=n.score},80));const s=y(()=>m.value*(1-Math.min(r.value,10)/10)),v=y(()=>n.score>=7.5?"#34d399":n.score>=5?"#f59e0b":"#f43f5e");return(w,_)=>(p(),f("div",ae,[t("div",{class:"relative",style:L({width:o.size+"px",height:o.size+"px"})},[(p(),f("svg",{width:o.size,height:o.size,viewBox:`0 0 ${o.size} ${o.size}`,class:"-rotate-90"},[t("circle",{cx:i.value,cy:g.value,r:x.value,fill:"none",stroke:"#e5e7eb","stroke-width":d.value},null,8,se),t("circle",{cx:i.value,cy:g.value,r:x.value,fill:"none",stroke:v.value,"stroke-width":d.value,"stroke-linecap":"round","stroke-dasharray":m.value,"stroke-dashoffset":s.value,style:{transition:"stroke-dashoffset 1s ease"}},null,8,ne)],8,re)),t("div",oe,[t("span",{class:"font-bold leading-none text-[var(--ink)]",style:L({fontSize:o.size*.22+"px"})},k(o.score.toFixed(1)),5)])],4),t("div",le,k(o.label),1)]))}},ie={class:"card p-5"},ce={class:"mb-3 flex items-center justify-between"},de={key:0,class:"text-sm italic text-[var(--ink3)]"},ue={key:1,class:"flex flex-wrap gap-x-1 gap-y-1 leading-relaxed"},pe=["title"],fe={key:2,class:"text-sm leading-relaxed text-[var(--ink)]"},ve={key:3,class:"mt-3 flex flex-wrap items-center gap-3 border-t border-[var(--border)] pt-3"},lt={__name:"TranscriptHighlight",props:{transcript:{type:String,default:""},wordTimestamps:{type:Array,default:()=>[]}},setup(o){const n=o,d=b(!1);function i(m){const r=m.score??1;return r>=.8?"text-[var(--ink)]":r>=.6?"rounded bg-[#fef3c7] text-[#92400e] font-medium":"rounded bg-[#fee2e2] text-[#991b1b] font-medium"}function g(m){const r=m.score??1;return r>=.8?"":r>=.6?"Pronunciation needs improvement":"Poor pronunciation – review this word"}async function x(){try{await navigator.clipboard.writeText(n.transcript),d.value=!0,setTimeout(()=>{d.value=!1},1500)}catch{}}return(m,r)=>(p(),f("div",ie,[t("div",ce,[r[1]||(r[1]=t("div",{class:"flex items-center gap-2"},[t("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round",class:"text-[var(--ink2)]"},[t("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})]),t("span",{class:"text-xs font-bold uppercase tracking-wider text-[var(--ink2)]"},"Transcript")],-1)),t("button",{class:"flex items-center gap-1 rounded px-2 py-1 text-[11px] text-[#34d399] transition hover:bg-[#34d39911]",onClick:x},[r[0]||(r[0]=t("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("rect",{x:"9",y:"9",width:"13",height:"13",rx:"2"}),t("path",{d:"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"})],-1)),P(" "+k(d.value?"Copied!":"Copy"),1)])]),o.transcript?o.wordTimestamps&&o.wordTimestamps.length?(p(),f("div",ue,[(p(!0),f(R,null,E(o.wordTimestamps,(s,v)=>(p(),f("span",{key:v,class:O(["cursor-default rounded px-0.5 text-sm transition-colors",i(s)]),title:g(s)},k(s.word),11,pe))),128))])):(p(),f("p",fe,k(o.transcript),1)):(p(),f("p",de,"No transcript available.")),o.wordTimestamps&&o.wordTimestamps.length?(p(),f("div",ve,[...r[2]||(r[2]=[A('<span class="text-[10px] font-semibold uppercase text-[var(--ink3)]">Pronunciation:</span><span class="flex items-center gap-1 text-[10px] text-[var(--ink2)]"><span class="inline-block h-2 w-2 rounded-full bg-[#34d399]"></span> Good</span><span class="flex items-center gap-1 text-[10px] text-[var(--ink2)]"><span class="inline-block h-2 w-2 rounded-full bg-[#f59e0b]"></span> Improve</span><span class="flex items-center gap-1 text-[10px] text-[var(--ink2)]"><span class="inline-block h-2 w-2 rounded-full bg-[#f43f5e]"></span> Poor</span>',4)])])):S("",!0)]))}};let H="",q=null;async function U({transcript:o,questionText:n=""}){const d=`${o}::${n}`;return H===d&&q||(H=d,q=X.post("/speaking/analyze-language",{transcript:o,question_text:n},{timeout:9e4}).then(i=>i.data).catch(i=>{throw H="",q=null,i})),q}function W(){H="",q=null}function D(o,n=[]){const d=String(o||"");if(!d)return"";const i=s=>String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),g=[];for(const s of n){const v=String(s.text||"").trim();if(!v)continue;let w=0;for(;w<d.length;){const _=d.indexOf(v,w);if(_===-1)break;g.push({start:_,end:_+v.length,className:s.className,title:s.title||""}),w=_+v.length}}if(!g.length)return i(d);g.sort((s,v)=>s.start-v.start||v.end-s.end);const x=[];for(const s of g){const v=x[x.length-1];if(!v||s.start>=v.end){x.push({...s});continue}s.end>v.end&&(v.end=s.end)}let m="",r=0;for(const s of x){s.start>r&&(m+=i(d.slice(r,s.start)));const v=s.title?` title="${i(s.title)}"`:"";m+=`<mark class="${s.className}"${v}>${i(d.slice(s.start,s.end))}</mark>`,r=s.end}return r<d.length&&(m+=i(d.slice(r))),m}function xe(o={}){const n=b(!1),d=b(null),i=b(0),g=b([]),x=b(!1),m=o.transcript,r=o.questionText,s=o.auto!==!1;function v(c,u=""){return c&&typeof c=="object"&&"value"in c?c.value||u:c||u}function w(c={}){const u=c.grammar_analysis||c.grammar||{};i.value=Number(u.score??c.grammar_score??0),x.value=!!(c.llm_generated??u.llm_generated??!1);const e=u.errors||c.grammar_errors||[];g.value=e.map(a=>({text:a.text||a.original||"",error_type:a.error_type||a.type||"grammar",correction:a.correction||"",explanation:a.explanation||""}))}const _=y(()=>{const c=v(m,""),u=g.value.filter(e=>e.text).map(e=>({text:e.text,className:"hl-grammar-error",title:e.error_type}));return D(c,u)});async function $(c,u=""){var l,h,B,j;const e=c??v(m,""),a=u??v(r,"");if(!e.trim())return d.value="No transcript to analyze.",null;n.value=!0,d.value=null;try{const C=await U({transcript:e,questionText:a});return w(C),C.grammar_analysis||C}catch(C){const G=((h=(l=C.response)==null?void 0:l.data)==null?void 0:h.error)||((j=(B=C.response)==null?void 0:B.data)==null?void 0:j.detail)||C.message||"Grammar analysis failed.";return d.value=G,null}finally{n.value=!1}}function T(c){var u,e;c&&(w({grammar_analysis:c.grammar_analysis,grammar_errors:(u=c.grammar)==null?void 0:u.errors,grammar_score:((e=c.grammar)==null?void 0:e.score)??c.grammar_range_accuracy_score,llm_generated:c.llm_generated}),x.value=!!c.llm_generated)}return s&&m&&typeof m=="object"&&"value"in m&&N(()=>[v(m),v(r)],([c])=>{c&&c.trim().length>8&&!x.value&&!g.value.length&&$(c,v(r))},{immediate:!1}),{loading:n,error:d,score:i,errors:g,highlightedHtml:_,llmGenerated:x,analyze:$,hydrateFromEvaluate:T,clearCache:W}}const me={class:"card p-5"},ge={class:"mb-4 flex items-center justify-between"},he={class:"flex items-center gap-2"},be={key:0,class:"rounded-full bg-[#ecfdf5] px-2 py-0.5 text-[9px] font-bold text-[#047857]"},ye={key:0,class:"py-6 text-center text-sm text-[var(--ink3)]"},ke={key:1,class:"rounded-lg border border-[#fecaca] bg-[#fef2f2] px-3 py-2 text-xs text-[#b91c1c]"},_e={key:0,class:"mb-4 rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3"},we=["innerHTML"],$e={key:1,class:"flex items-center gap-2 text-sm text-[#34d399]"},Te={key:2,class:"space-y-3"},Ce={class:"mb-1 flex flex-wrap items-center gap-2"},ze={class:"rounded-full bg-[#fee2e2] px-2 py-0.5 text-[10px] font-bold uppercase text-[#b91c1c]"},Be={class:"text-[12px] text-[#f43f5e] line-through"},Me={class:"mt-2 flex items-start gap-2 text-sm"},Se={class:"font-semibold text-[#34d399]"},Ne={key:0,class:"mt-1.5 text-[11px] text-[var(--ink3)]"},qe={__name:"GrammarCard",props:{transcript:{type:String,default:""},questionText:{type:String,default:""},score:{type:Number,default:0},errors:{type:Array,default:()=>[]},evaluateResult:{type:Object,default:null}},setup(o){const n=o,d=V(n,"transcript"),i=V(n,"questionText"),{loading:g,error:x,score:m,errors:r,highlightedHtml:s,llmGenerated:v,hydrateFromEvaluate:w,analyze:_}=xe({transcript:d,questionText:i,auto:!1}),$=y(()=>v.value?m.value:n.score),T=y(()=>v.value&&r.value.length?r.value:(n.errors||[]).map(u=>({text:u.text||u.original||"",error_type:u.error_type||u.type||"grammar",correction:u.correction||"",explanation:u.explanation||""}))),c=y(()=>{const u=$.value;return u>=7?"#34d399":u>=5?"#f59e0b":"#f43f5e"});return N(()=>n.evaluateResult,u=>{u&&w(u)},{immediate:!0,deep:!0}),N(()=>[n.transcript,n.evaluateResult],([u,e])=>{e!=null&&e.llm_generated&&(e.grammar_analysis||e.grammar)||u&&u.trim().length>8&&_(u,n.questionText)},{immediate:!0}),(u,e)=>(p(),f("div",me,[t("div",ge,[t("div",he,[e[0]||(e[0]=t("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round",class:"text-[var(--ink2)]"},[t("path",{d:"M12 20h9"}),t("path",{d:"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"})],-1)),e[1]||(e[1]=t("span",{class:"text-xs font-bold uppercase tracking-wider text-[var(--ink2)]"},"Grammar",-1)),z(v)?(p(),f("span",be,"AI")):S("",!0)]),t("span",{class:"rounded-full border px-2.5 py-0.5 text-[11px] font-bold",style:L({borderColor:c.value+"55",color:c.value,background:c.value+"11"})},k($.value.toFixed(1))+"/9 ",5)]),z(g)?(p(),f("div",ye,"Đang phân tích ngữ pháp (LLM)…")):z(x)?(p(),f("div",ke,k(z(x)),1)):(p(),f(R,{key:2},[o.transcript?(p(),f("div",_e,[e[2]||(e[2]=t("div",{class:"mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},"Transcript (lỗi được tô đỏ nhạt)",-1)),t("p",{class:"transcript-html text-[13px] leading-relaxed text-[var(--ink)]",innerHTML:z(s)},null,8,we)])):S("",!0),T.value.length?(p(),f("div",Te,[(p(!0),f(R,null,E(T.value,(a,l)=>(p(),f("div",{key:l,class:"rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3"},[t("div",Ce,[t("span",ze,k(a.error_type||"grammar"),1),t("span",Be,k(a.text||a.original),1)]),t("div",Me,[e[4]||(e[4]=t("svg",{class:"mt-0.5 shrink-0 text-[#34d399]",width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[t("polyline",{points:"20 6 9 17 4 12"})],-1)),t("span",Se,k(a.correction),1)]),a.explanation?(p(),f("p",Ne,k(a.explanation),1)):S("",!0)]))),128))])):(p(),f("div",$e,[...e[3]||(e[3]=[t("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[t("polyline",{points:"20 6 9 17 4 12"})],-1),P(" No grammar errors found in transcript. ",-1)])]))],64))]))}},it=I(qe,[["__scopeId","data-v-901d0371"]]);function Le(o={}){const n=b(!1),d=b(null),i=b(0),g=b([]),x=b([]),m=b([]),r=b(!1),s=o.transcript,v=o.questionText,w=o.auto!==!1;function _(e,a=""){return e&&typeof e=="object"&&"value"in e?e.value||a:e||a}function $(e={}){const a=e.vocabulary_analysis||e.vocabulary||{};i.value=Number(a.score??e.vocabulary_score??0),r.value=!!(e.llm_generated??a.llm_generated??!1),g.value=(a.weak_words||[]).map(h=>({text:h.text||h.word_used||h.word||"",reason:h.reason||""})),x.value=(a.strong_words||[]).map(h=>({text:h.text||h.word||"",reason:h.reason||""}));const l=a.replacements||a.feedback||e.vocabulary_feedback||[];m.value=l.map(h=>({weak:h.weak||h.word_used||h.text||"",better:h.better||h.better_alternative||"",reason:h.reason||""}))}const T=y(()=>{const e=_(s,""),a=[...g.value.map(l=>({text:l.text,className:"hl-vocab-weak",title:l.reason||"weak / repetitive"})),...x.value.map(l=>({text:l.text,className:"hl-vocab-strong",title:l.reason||"strong usage"}))];return D(e,a)});async function c(e,a=""){var B,j,C,G;const l=e??_(s,""),h=a??_(v,"");if(!l.trim())return d.value="No transcript to analyze.",null;n.value=!0,d.value=null;try{const M=await U({transcript:l,questionText:h});return $(M),M.vocabulary_analysis||M}catch(M){const K=((j=(B=M.response)==null?void 0:B.data)==null?void 0:j.error)||((G=(C=M.response)==null?void 0:C.data)==null?void 0:G.detail)||M.message||"Vocabulary analysis failed.";return d.value=K,null}finally{n.value=!1}}function u(e){var a,l;e&&($({vocabulary_analysis:e.vocabulary_analysis,vocabulary_feedback:(a=e.vocabulary)==null?void 0:a.feedback,vocabulary_score:((l=e.vocabulary)==null?void 0:l.score)??e.lexical_resource_score,llm_generated:e.llm_generated}),r.value=!!e.llm_generated)}return w&&s&&typeof s=="object"&&"value"in s&&N(()=>[_(s),_(v)],([e])=>{e&&e.trim().length>8&&!r.value&&!g.value.length&&!x.value.length&&c(e,_(v))},{immediate:!1}),{loading:n,error:d,score:i,weakWords:g,strongWords:x,replacements:m,highlightedHtml:T,llmGenerated:r,analyze:c,hydrateFromEvaluate:u,clearCache:W}}const Re={class:"card p-5"},je={class:"mb-4 flex items-center justify-between"},Ge={class:"flex items-center gap-2"},He={key:0,class:"rounded-full bg-[#ecfdf5] px-2 py-0.5 text-[9px] font-bold text-[#047857]"},Ve={key:0,class:"py-6 text-center text-sm text-[var(--ink3)]"},Ee={key:1,class:"rounded-lg border border-[#fecaca] bg-[#fef2f2] px-3 py-2 text-xs text-[#b91c1c]"},Ae={key:0,class:"mb-4 rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3"},Fe=["innerHTML"],Pe={key:1,class:"space-y-3"},Oe={class:"flex flex-wrap items-center gap-2 text-sm"},Ie={class:"rounded bg-[#fef9c3] px-2 py-0.5 font-medium text-[#854d0e]"},Ue={class:"rounded-full border border-[#34d39955] bg-[#34d39911] px-2 py-0.5 text-[11px] font-semibold text-[#34d399]"},We={key:0,class:"mt-1.5 text-[11px] text-[var(--ink3)]"},De={key:2,class:"text-sm text-[#34d399]"},Ke={key:3,class:"text-sm text-[var(--ink3)]"},Xe={__name:"VocabCard",props:{transcript:{type:String,default:""},questionText:{type:String,default:""},score:{type:Number,default:0},feedback:{type:Array,default:()=>[]},evaluateResult:{type:Object,default:null}},setup(o){const n=o,d=V(n,"transcript"),i=V(n,"questionText"),{loading:g,error:x,score:m,replacements:r,strongWords:s,highlightedHtml:v,llmGenerated:w,hydrateFromEvaluate:_,analyze:$}=Le({transcript:d,questionText:i,auto:!1}),T=y(()=>w.value?m.value:n.score),c=y(()=>w.value&&r.value.length?r.value.map(a=>({weak:a.weak,better:a.better,reason:a.reason})):(n.feedback||[]).map(a=>({weak:a.word_used||a.weak||"",better:a.better_alternative||a.better||"",reason:a.reason||""}))),u=y(()=>s.value||[]),e=y(()=>{const a=T.value;return a>=7?"#34d399":a>=5?"#f59e0b":"#f43f5e"});return N(()=>n.evaluateResult,a=>{a&&_(a)},{immediate:!0,deep:!0}),N(()=>[n.transcript,n.evaluateResult],([a,l])=>{l!=null&&l.llm_generated&&(l.vocabulary_analysis||l.vocabulary)||a&&a.trim().length>8&&$(a,n.questionText)},{immediate:!0}),(a,l)=>(p(),f("div",Re,[t("div",je,[t("div",Ge,[l[0]||(l[0]=t("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round",class:"text-[var(--ink2)]"},[t("path",{d:"M4 19.5A2.5 2.5 0 0 1 6.5 17H20"}),t("path",{d:"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"})],-1)),l[1]||(l[1]=t("span",{class:"text-xs font-bold uppercase tracking-wider text-[var(--ink2)]"},"Vocabulary",-1)),z(w)?(p(),f("span",He,"AI")):S("",!0)]),t("span",{class:"rounded-full border px-2.5 py-0.5 text-[11px] font-bold",style:L({borderColor:e.value+"55",color:e.value,background:e.value+"11"})},k(T.value.toFixed(1))+"/9 ",5)]),l[4]||(l[4]=A('<div class="mb-3 flex flex-wrap gap-3 text-[10px] text-[var(--ink3)]" data-v-6f1180b1><span class="inline-flex items-center gap-1" data-v-6f1180b1><span class="legend legend--weak" data-v-6f1180b1></span> Từ yếu / lặp</span><span class="inline-flex items-center gap-1" data-v-6f1180b1><span class="legend legend--strong" data-v-6f1180b1></span> Từ tốt / collocation</span></div>',1)),z(g)?(p(),f("div",Ve,"Đang phân tích từ vựng (LLM)…")):z(x)?(p(),f("div",Ee,k(z(x)),1)):(p(),f(R,{key:2},[o.transcript?(p(),f("div",Ae,[l[2]||(l[2]=t("div",{class:"mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},"Transcript",-1)),t("p",{class:"transcript-html text-[13px] leading-relaxed text-[var(--ink)]",innerHTML:z(v)},null,8,Fe)])):S("",!0),c.value.length?(p(),f("div",Pe,[(p(!0),f(R,null,E(c.value,(h,B)=>(p(),f("div",{key:B,class:"rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3"},[t("div",Oe,[t("span",Ie,k(h.weak),1),l[3]||(l[3]=t("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"#34d399","stroke-width":"2"},[t("line",{x1:"5",y1:"12",x2:"19",y2:"12"}),t("polyline",{points:"12 5 19 12 12 19"})],-1)),t("span",Ue,k(h.better),1)]),h.reason?(p(),f("p",We,k(h.reason),1)):S("",!0)]))),128))])):u.value.length?(p(),f("div",De," Strong vocabulary detected. See highlighted phrases above. ")):(p(),f("div",Ke,"No vocabulary suggestions from LLM for this transcript."))],64))]))}},ct=I(Xe,[["__scopeId","data-v-6f1180b1"]]),Ye={class:"rounded-xl border border-[var(--border)] bg-[var(--bg)] p-4"},Je=["src"],Qe={class:"flex items-center gap-3"},Ze={key:0,width:"10",height:"10",viewBox:"0 0 24 24",fill:"currentColor"},et={key:1,width:"10",height:"10",viewBox:"0 0 24 24",fill:"currentColor"},tt={class:"text-xs tabular-nums text-[var(--ink3)]"},at={class:"ml-auto flex items-center gap-1"},rt=["onClick"],dt={__name:"AudioPlayer",props:{audioUrl:{type:String,default:""},audioBlob:{type:Object,default:null}},setup(o){const n=o,d=y(()=>n.audioBlob?URL.createObjectURL(n.audioBlob):n.audioUrl),i=b(null),g=b(null),x=b(!1),m=b(0),r=b(0),s=b(1),v=y(()=>r.value>0?m.value/r.value*100:0);function w(){var e;m.value=((e=i.value)==null?void 0:e.currentTime)??0}function _(){var e;r.value=((e=i.value)==null?void 0:e.duration)??0}function $(){i.value&&(x.value?(i.value.pause(),x.value=!1):(i.value.play(),x.value=!0))}function T(e){if(!g.value||!i.value)return;const{left:a,width:l}=g.value.getBoundingClientRect();i.value.currentTime=Math.max(0,Math.min(1,(e.clientX-a)/l))*r.value}function c(e){s.value=e,i.value&&(i.value.playbackRate=e)}function u(e){return isFinite(e)?`${Math.floor(e/60)}:${Math.floor(e%60).toString().padStart(2,"0")}`:"0:00"}return N(s,e=>{i.value&&(i.value.playbackRate=e)}),(e,a)=>(p(),f("div",Ye,[a[3]||(a[3]=A('<div class="mb-3 flex items-center gap-2 text-[var(--ink2)]"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg><span class="text-[11px] font-semibold uppercase tracking-wider">Your Recording</span></div>',1)),t("audio",{ref_key:"audioEl",ref:i,src:d.value,preload:"metadata",class:"hidden",onTimeupdate:w,onLoadedmetadata:_,onEnded:a[0]||(a[0]=l=>x.value=!1)},null,40,Je),t("div",{class:"relative mb-3 h-1.5 cursor-pointer overflow-hidden rounded-full bg-[var(--border)]",onClick:T,ref_key:"barEl",ref:g},[t("div",{class:"absolute inset-y-0 left-0 rounded-full bg-[#34d399] transition-[width]",style:L({width:v.value+"%"})},null,4)],512),t("div",Qe,[t("button",{class:"flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-2 border-[#34d399] text-[#34d399] transition hover:bg-[#34d399] hover:text-white",onClick:$},[x.value?(p(),f("svg",et,[...a[2]||(a[2]=[t("rect",{x:"6",y:"4",width:"4",height:"16"},null,-1),t("rect",{x:"14",y:"4",width:"4",height:"16"},null,-1)])])):(p(),f("svg",Ze,[...a[1]||(a[1]=[t("polygon",{points:"5 3 19 12 5 21 5 3"},null,-1)])]))]),t("span",tt,k(u(m.value))+" / "+k(u(r.value)),1),t("div",at,[(p(),f(R,null,E([.75,1,1.5],l=>t("button",{key:l,class:O(["rounded px-1.5 py-0.5 text-[10px] font-bold transition",s.value===l?"bg-[#34d399] text-white":"text-[var(--ink3)] hover:text-[var(--ink2)]"]),onClick:h=>c(l)},k(l)+"×",11,rt)),64))])])]))}};export{it as G,ct as V,nt as _,dt as a,ot as b,lt as c,W as d};
|
|
|
|
|
|
fronted/dist/assets/AudioPlayer-RR1aaAJE.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{q as F,o as p,c as f,a as t,t as k,n as L,j as b,z as y,g as P,F as R,r as G,y as O,b as V,f as S,M as K,p as N,e as M,L as A}from"./index-CId0hMaT.js";const X={class:"flex flex-col items-center gap-3"},Y={class:"relative",style:{width:"140px",height:"140px"}},J={width:"140",height:"140",viewBox:"0 0 140 140",class:"-rotate-90"},Q=["stroke-dashoffset"],Z={class:"absolute inset-0 flex flex-col items-center justify-center"},ee={class:"text-3xl font-extrabold leading-none text-[var(--ink)]"},rt={__name:"BandScoreRing",props:{band:{type:Number,default:0}},setup(o){const n=o,d=2*Math.PI*58,i=b(0);F(()=>setTimeout(()=>{i.value=n.band},120));const g=y(()=>d*(1-Math.min(i.value,9)/9)),x=y(()=>n.band>=7.5?"#34d399":n.band>=6?"#f59e0b":"#f43f5e"),m=y(()=>{const a=n.band;return a>=8.5?"Expert":a>=7.5?"Very Good":a>=7?"Good":a>=6.5?"Competent":a>=6?"Modest":a>=5?"Limited":"Developing"});return(a,s)=>(p(),f("div",X,[t("div",Y,[(p(),f("svg",J,[s[0]||(s[0]=t("circle",{cx:"70",cy:"70",r:"58",fill:"none",stroke:"#e5e7eb","stroke-width":"12"},null,-1)),t("circle",{cx:"70",cy:"70",r:"58",fill:"none",stroke:"#34d399","stroke-width":"12","stroke-linecap":"round","stroke-dasharray":d,"stroke-dashoffset":g.value,style:{transition:"stroke-dashoffset 1.2s ease"}},null,8,Q)])),t("div",Z,[t("span",ee,k(o.band.toFixed(1)),1),s[1]||(s[1]=t("span",{class:"mt-1 text-[9px] font-bold uppercase tracking-widest text-[var(--ink3)]"},"Band",-1))])]),t("span",{class:"rounded-full border px-3 py-0.5 text-[11px] font-semibold",style:L({borderColor:x.value+"66",color:x.value,background:x.value+"11"})},k(m.value),5)]))}},te={class:"flex flex-col items-center gap-2"},re=["width","height","viewBox"],ae=["cx","cy","r","stroke-width"],se=["cx","cy","r","stroke","stroke-width","stroke-dasharray","stroke-dashoffset"],ne={class:"absolute inset-0 flex flex-col items-center justify-center"},oe={class:"text-center text-[11px] font-semibold uppercase tracking-wider text-[var(--ink2)]"},at={__name:"CircularScore",props:{score:{type:Number,default:0},label:{type:String,default:""},size:{type:Number,default:88}},setup(o){const n=o,d=y(()=>Math.max(6,n.size*.09)),i=y(()=>n.size/2),g=y(()=>n.size/2),x=y(()=>(n.size-d.value)/2),m=y(()=>2*Math.PI*x.value),a=b(0);F(()=>setTimeout(()=>{a.value=n.score},80));const s=y(()=>m.value*(1-Math.min(a.value,10)/10)),v=y(()=>n.score>=7.5?"#34d399":n.score>=5?"#f59e0b":"#f43f5e");return(w,_)=>(p(),f("div",te,[t("div",{class:"relative",style:L({width:o.size+"px",height:o.size+"px"})},[(p(),f("svg",{width:o.size,height:o.size,viewBox:`0 0 ${o.size} ${o.size}`,class:"-rotate-90"},[t("circle",{cx:i.value,cy:g.value,r:x.value,fill:"none",stroke:"#e5e7eb","stroke-width":d.value},null,8,ae),t("circle",{cx:i.value,cy:g.value,r:x.value,fill:"none",stroke:v.value,"stroke-width":d.value,"stroke-linecap":"round","stroke-dasharray":m.value,"stroke-dashoffset":s.value,style:{transition:"stroke-dashoffset 1s ease"}},null,8,se)],8,re)),t("div",ne,[t("span",{class:"font-bold leading-none text-[var(--ink)]",style:L({fontSize:o.size*.22+"px"})},k(o.score.toFixed(1)),5)])],4),t("div",oe,k(o.label),1)]))}},le={class:"card p-5"},ie={class:"mb-3 flex items-center justify-between"},ce={key:0,class:"text-sm italic text-[var(--ink3)]"},de={key:1,class:"flex flex-wrap gap-x-1 gap-y-1 leading-relaxed"},ue=["title"],pe={key:2,class:"text-sm leading-relaxed text-[var(--ink)]"},fe={key:3,class:"mt-3 flex flex-wrap items-center gap-3 border-t border-[var(--border)] pt-3"},st={__name:"TranscriptHighlight",props:{transcript:{type:String,default:""},wordTimestamps:{type:Array,default:()=>[]}},setup(o){const n=o,d=b(!1);function i(m){const a=m.score??1;return a>=.8?"text-[var(--ink)]":a>=.6?"rounded bg-[#fef3c7] text-[#92400e] font-medium":"rounded bg-[#fee2e2] text-[#991b1b] font-medium"}function g(m){const a=m.score??1;return a>=.8?"":a>=.6?"Pronunciation needs improvement":"Poor pronunciation – review this word"}async function x(){try{await navigator.clipboard.writeText(n.transcript),d.value=!0,setTimeout(()=>{d.value=!1},1500)}catch{}}return(m,a)=>(p(),f("div",le,[t("div",ie,[a[1]||(a[1]=t("div",{class:"flex items-center gap-2"},[t("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round",class:"text-[var(--ink2)]"},[t("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})]),t("span",{class:"text-xs font-bold uppercase tracking-wider text-[var(--ink2)]"},"Transcript")],-1)),t("button",{class:"flex items-center gap-1 rounded px-2 py-1 text-[11px] text-[#34d399] transition hover:bg-[#34d39911]",onClick:x},[a[0]||(a[0]=t("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("rect",{x:"9",y:"9",width:"13",height:"13",rx:"2"}),t("path",{d:"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"})],-1)),P(" "+k(d.value?"Copied!":"Copy"),1)])]),o.transcript?o.wordTimestamps&&o.wordTimestamps.length?(p(),f("div",de,[(p(!0),f(R,null,G(o.wordTimestamps,(s,v)=>(p(),f("span",{key:v,class:O(["cursor-default rounded px-0.5 text-sm transition-colors",i(s)]),title:g(s)},k(s.word),11,ue))),128))])):(p(),f("p",pe,k(o.transcript),1)):(p(),f("p",ce,"No transcript available.")),o.wordTimestamps&&o.wordTimestamps.length?(p(),f("div",fe,[...a[2]||(a[2]=[V('<span class="text-[10px] font-semibold uppercase text-[var(--ink3)]">Pronunciation:</span><span class="flex items-center gap-1 text-[10px] text-[var(--ink2)]"><span class="inline-block h-2 w-2 rounded-full bg-[#34d399]"></span> Good</span><span class="flex items-center gap-1 text-[10px] text-[var(--ink2)]"><span class="inline-block h-2 w-2 rounded-full bg-[#f59e0b]"></span> Improve</span><span class="flex items-center gap-1 text-[10px] text-[var(--ink2)]"><span class="inline-block h-2 w-2 rounded-full bg-[#f43f5e]"></span> Poor</span>',4)])])):S("",!0)]))}};let E="",q=null;async function I({transcript:o,questionText:n=""}){const d=`${o}::${n}`;return E===d&&q||(E=d,q=K.post("/speaking/analyze-language",{transcript:o,question_text:n},{timeout:9e4}).then(i=>i.data).catch(i=>{throw E="",q=null,i})),q}function U(){E="",q=null}function W(o,n=[]){const d=String(o||"");if(!d)return"";const i=s=>String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),g=[];for(const s of n){const v=String(s.text||"").trim();if(!v)continue;let w=0;for(;w<d.length;){const _=d.indexOf(v,w);if(_===-1)break;g.push({start:_,end:_+v.length,className:s.className,title:s.title||""}),w=_+v.length}}if(!g.length)return i(d);g.sort((s,v)=>s.start-v.start||v.end-s.end);const x=[];for(const s of g){const v=x[x.length-1];if(!v||s.start>=v.end){x.push({...s});continue}s.end>v.end&&(v.end=s.end)}let m="",a=0;for(const s of x){s.start>a&&(m+=i(d.slice(a,s.start)));const v=s.title?` title="${i(s.title)}"`:"";m+=`<mark class="${s.className}"${v}>${i(d.slice(s.start,s.end))}</mark>`,a=s.end}return a<d.length&&(m+=i(d.slice(a))),m}function ve(o={}){const n=b(!1),d=b(null),i=b(0),g=b([]),x=b(!1),m=o.transcript,a=o.questionText,s=o.auto!==!1;function v(c,u=""){return c&&typeof c=="object"&&"value"in c?c.value||u:c||u}function w(c={}){const u=c.grammar_analysis||c.grammar||{};i.value=Number(u.score??c.grammar_score??0),x.value=!!(c.llm_generated??u.llm_generated??!1);const e=u.errors||c.grammar_errors||[];g.value=e.map(r=>({text:r.text||r.original||"",error_type:r.error_type||r.type||"grammar",correction:r.correction||"",explanation:r.explanation||""}))}const _=y(()=>{const c=v(m,""),u=g.value.filter(e=>e.text).map(e=>({text:e.text,className:"hl-grammar-error",title:e.error_type}));return W(c,u)});async function $(c,u=""){var l,h,z,j;const e=c??v(m,""),r=u??v(a,"");if(!e.trim())return d.value="No transcript to analyze.",null;n.value=!0,d.value=null;try{const C=await I({transcript:e,questionText:r});return w(C),C.grammar_analysis||C}catch(C){const H=((h=(l=C.response)==null?void 0:l.data)==null?void 0:h.error)||((j=(z=C.response)==null?void 0:z.data)==null?void 0:j.detail)||C.message||"Grammar analysis failed.";return d.value=H,null}finally{n.value=!1}}function T(c){var u,e;c&&(w({grammar_analysis:c.grammar_analysis,grammar_errors:(u=c.grammar)==null?void 0:u.errors,grammar_score:((e=c.grammar)==null?void 0:e.score)??c.grammar_range_accuracy_score,llm_generated:c.llm_generated}),x.value=!!c.llm_generated)}return s&&m&&typeof m=="object"&&"value"in m&&N(()=>[v(m),v(a)],([c])=>{c&&c.trim().length>8&&!x.value&&!g.value.length&&$(c,v(a))},{immediate:!1}),{loading:n,error:d,score:i,errors:g,highlightedHtml:_,llmGenerated:x,analyze:$,hydrateFromEvaluate:T,clearCache:U}}const xe={class:"card p-5"},me={class:"mb-4 flex items-center justify-between"},ge={class:"flex items-center gap-2"},he={key:0,class:"rounded-full bg-[#ecfdf5] px-2 py-0.5 text-[9px] font-bold text-[#047857]"},be={key:0,class:"py-6 text-center text-sm text-[var(--ink3)]"},ye={key:1,class:"rounded-lg border border-[#fecaca] bg-[#fef2f2] px-3 py-2 text-xs text-[#b91c1c]"},ke={key:0,class:"mb-4 rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3"},_e=["innerHTML"],we={key:1,class:"flex items-center gap-2 text-sm text-[#34d399]"},$e={key:2,class:"space-y-3"},Te={class:"mb-1 flex flex-wrap items-center gap-2"},Ce={class:"rounded-full bg-[#fee2e2] px-2 py-0.5 text-[10px] font-bold uppercase text-[#b91c1c]"},Me={class:"text-[12px] text-[#f43f5e] line-through"},ze={class:"mt-2 flex items-start gap-2 text-sm"},Be={class:"font-semibold text-[#34d399]"},Se={key:0,class:"mt-1.5 text-[11px] text-[var(--ink3)]"},nt={__name:"GrammarCard",props:{transcript:{type:String,default:""},questionText:{type:String,default:""},score:{type:Number,default:0},errors:{type:Array,default:()=>[]},evaluateResult:{type:Object,default:null}},setup(o){const n=o,d=A(n,"transcript"),i=A(n,"questionText"),{loading:g,error:x,score:m,errors:a,highlightedHtml:s,llmGenerated:v,hydrateFromEvaluate:w,analyze:_}=ve({transcript:d,questionText:i,auto:!1}),$=y(()=>v.value?m.value:n.score),T=y(()=>v.value&&a.value.length?a.value:(n.errors||[]).map(u=>({text:u.text||u.original||"",error_type:u.error_type||u.type||"grammar",correction:u.correction||"",explanation:u.explanation||""}))),c=y(()=>{const u=$.value;return u>=7?"#34d399":u>=5?"#f59e0b":"#f43f5e"});return N(()=>n.evaluateResult,u=>{u&&w(u)},{immediate:!0,deep:!0}),N(()=>[n.transcript,n.evaluateResult],([u,e])=>{e!=null&&e.llm_generated&&(e.grammar_analysis||e.grammar)||u&&u.trim().length>8&&_(u,n.questionText)},{immediate:!0}),(u,e)=>(p(),f("div",xe,[t("div",me,[t("div",ge,[e[0]||(e[0]=t("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round",class:"text-[var(--ink2)]"},[t("path",{d:"M12 20h9"}),t("path",{d:"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"})],-1)),e[1]||(e[1]=t("span",{class:"text-xs font-bold uppercase tracking-wider text-[var(--ink2)]"},"Grammar",-1)),M(v)?(p(),f("span",he,"AI")):S("",!0)]),t("span",{class:"rounded-full border px-2.5 py-0.5 text-[11px] font-bold",style:L({borderColor:c.value+"55",color:c.value,background:c.value+"11"})},k($.value.toFixed(1))+"/9 ",5)]),M(g)?(p(),f("div",be,"Đang phân tích ngữ pháp (LLM)…")):M(x)?(p(),f("div",ye,k(M(x)),1)):(p(),f(R,{key:2},[o.transcript?(p(),f("div",ke,[e[2]||(e[2]=t("div",{class:"mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},"Transcript (lỗi được tô đỏ nhạt)",-1)),t("p",{class:"transcript-html text-[13px] leading-relaxed text-[var(--ink)]",innerHTML:M(s)},null,8,_e)])):S("",!0),T.value.length?(p(),f("div",$e,[(p(!0),f(R,null,G(T.value,(r,l)=>(p(),f("div",{key:l,class:"rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3"},[t("div",Te,[t("span",Ce,k(r.error_type||"grammar"),1),t("span",Me,k(r.text||r.original),1)]),t("div",ze,[e[4]||(e[4]=t("svg",{class:"mt-0.5 shrink-0 text-[#34d399]",width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[t("polyline",{points:"20 6 9 17 4 12"})],-1)),t("span",Be,k(r.correction),1)]),r.explanation?(p(),f("p",Se,k(r.explanation),1)):S("",!0)]))),128))])):(p(),f("div",we,[...e[3]||(e[3]=[t("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[t("polyline",{points:"20 6 9 17 4 12"})],-1),P(" No grammar errors found in transcript. ",-1)])]))],64))]))}};function Ne(o={}){const n=b(!1),d=b(null),i=b(0),g=b([]),x=b([]),m=b([]),a=b(!1),s=o.transcript,v=o.questionText,w=o.auto!==!1;function _(e,r=""){return e&&typeof e=="object"&&"value"in e?e.value||r:e||r}function $(e={}){const r=e.vocabulary_analysis||e.vocabulary||{};i.value=Number(r.score??e.vocabulary_score??0),a.value=!!(e.llm_generated??r.llm_generated??!1),g.value=(r.weak_words||[]).map(h=>({text:h.text||h.word_used||h.word||"",reason:h.reason||""})),x.value=(r.strong_words||[]).map(h=>({text:h.text||h.word||"",reason:h.reason||""}));const l=r.replacements||r.feedback||e.vocabulary_feedback||[];m.value=l.map(h=>({weak:h.weak||h.word_used||h.text||"",better:h.better||h.better_alternative||"",reason:h.reason||""}))}const T=y(()=>{const e=_(s,""),r=[...g.value.map(l=>({text:l.text,className:"hl-vocab-weak",title:l.reason||"weak / repetitive"})),...x.value.map(l=>({text:l.text,className:"hl-vocab-strong",title:l.reason||"strong usage"}))];return W(e,r)});async function c(e,r=""){var z,j,C,H;const l=e??_(s,""),h=r??_(v,"");if(!l.trim())return d.value="No transcript to analyze.",null;n.value=!0,d.value=null;try{const B=await I({transcript:l,questionText:h});return $(B),B.vocabulary_analysis||B}catch(B){const D=((j=(z=B.response)==null?void 0:z.data)==null?void 0:j.error)||((H=(C=B.response)==null?void 0:C.data)==null?void 0:H.detail)||B.message||"Vocabulary analysis failed.";return d.value=D,null}finally{n.value=!1}}function u(e){var r,l;e&&($({vocabulary_analysis:e.vocabulary_analysis,vocabulary_feedback:(r=e.vocabulary)==null?void 0:r.feedback,vocabulary_score:((l=e.vocabulary)==null?void 0:l.score)??e.lexical_resource_score,llm_generated:e.llm_generated}),a.value=!!e.llm_generated)}return w&&s&&typeof s=="object"&&"value"in s&&N(()=>[_(s),_(v)],([e])=>{e&&e.trim().length>8&&!a.value&&!g.value.length&&!x.value.length&&c(e,_(v))},{immediate:!1}),{loading:n,error:d,score:i,weakWords:g,strongWords:x,replacements:m,highlightedHtml:T,llmGenerated:a,analyze:c,hydrateFromEvaluate:u,clearCache:U}}const qe={class:"card p-5"},Le={class:"mb-4 flex items-center justify-between"},Re={class:"flex items-center gap-2"},je={key:0,class:"rounded-full bg-[#ecfdf5] px-2 py-0.5 text-[9px] font-bold text-[#047857]"},He={key:0,class:"py-6 text-center text-sm text-[var(--ink3)]"},Ee={key:1,class:"rounded-lg border border-[#fecaca] bg-[#fef2f2] px-3 py-2 text-xs text-[#b91c1c]"},Ae={key:0,class:"mb-4 rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3"},Ge=["innerHTML"],Ve={key:1,class:"space-y-3"},Fe={class:"flex flex-wrap items-center gap-2 text-sm"},Pe={class:"rounded bg-[#fef9c3] px-2 py-0.5 font-medium text-[#854d0e]"},Oe={class:"rounded-full border border-[#34d39955] bg-[#34d39911] px-2 py-0.5 text-[11px] font-semibold text-[#34d399]"},Ie={key:0,class:"mt-1.5 text-[11px] text-[var(--ink3)]"},Ue={key:2,class:"text-sm text-[#34d399]"},We={key:3,class:"text-sm text-[var(--ink3)]"},ot={__name:"VocabCard",props:{transcript:{type:String,default:""},questionText:{type:String,default:""},score:{type:Number,default:0},feedback:{type:Array,default:()=>[]},evaluateResult:{type:Object,default:null}},setup(o){const n=o,d=A(n,"transcript"),i=A(n,"questionText"),{loading:g,error:x,score:m,replacements:a,strongWords:s,highlightedHtml:v,llmGenerated:w,hydrateFromEvaluate:_,analyze:$}=Ne({transcript:d,questionText:i,auto:!1}),T=y(()=>w.value?m.value:n.score),c=y(()=>w.value&&a.value.length?a.value.map(r=>({weak:r.weak,better:r.better,reason:r.reason})):(n.feedback||[]).map(r=>({weak:r.word_used||r.weak||"",better:r.better_alternative||r.better||"",reason:r.reason||""}))),u=y(()=>s.value||[]),e=y(()=>{const r=T.value;return r>=7?"#34d399":r>=5?"#f59e0b":"#f43f5e"});return N(()=>n.evaluateResult,r=>{r&&_(r)},{immediate:!0,deep:!0}),N(()=>[n.transcript,n.evaluateResult],([r,l])=>{l!=null&&l.llm_generated&&(l.vocabulary_analysis||l.vocabulary)||r&&r.trim().length>8&&$(r,n.questionText)},{immediate:!0}),(r,l)=>(p(),f("div",qe,[t("div",Le,[t("div",Re,[l[0]||(l[0]=t("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round",class:"text-[var(--ink2)]"},[t("path",{d:"M4 19.5A2.5 2.5 0 0 1 6.5 17H20"}),t("path",{d:"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"})],-1)),l[1]||(l[1]=t("span",{class:"text-xs font-bold uppercase tracking-wider text-[var(--ink2)]"},"Vocabulary",-1)),M(w)?(p(),f("span",je,"AI")):S("",!0)]),t("span",{class:"rounded-full border px-2.5 py-0.5 text-[11px] font-bold",style:L({borderColor:e.value+"55",color:e.value,background:e.value+"11"})},k(T.value.toFixed(1))+"/9 ",5)]),l[4]||(l[4]=V('<div class="mb-3 flex flex-wrap gap-3 text-[10px] text-[var(--ink3)]"><span class="inline-flex items-center gap-1"><span class="inline-block h-2.5 w-2.5 rounded-sm bg-yellow-200"></span> Từ yếu / lặp</span><span class="inline-flex items-center gap-1"><span class="inline-block h-2.5 w-2.5 rounded-sm bg-green-200"></span> Từ tốt / collocation</span></div>',1)),M(g)?(p(),f("div",He,"Đang phân tích từ vựng (LLM)…")):M(x)?(p(),f("div",Ee,k(M(x)),1)):(p(),f(R,{key:2},[o.transcript?(p(),f("div",Ae,[l[2]||(l[2]=t("div",{class:"mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},"Transcript",-1)),t("p",{class:"transcript-html text-[13px] leading-relaxed text-[var(--ink)]",innerHTML:M(v)},null,8,Ge)])):S("",!0),c.value.length?(p(),f("div",Ve,[(p(!0),f(R,null,G(c.value,(h,z)=>(p(),f("div",{key:z,class:"rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3"},[t("div",Fe,[t("span",Pe,k(h.weak),1),l[3]||(l[3]=t("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"#34d399","stroke-width":"2"},[t("line",{x1:"5",y1:"12",x2:"19",y2:"12"}),t("polyline",{points:"12 5 19 12 12 19"})],-1)),t("span",Oe,k(h.better),1)]),h.reason?(p(),f("p",Ie,k(h.reason),1)):S("",!0)]))),128))])):u.value.length?(p(),f("div",Ue," Strong vocabulary detected. See highlighted phrases above. ")):(p(),f("div",We,"No vocabulary suggestions from LLM for this transcript."))],64))]))}},De={class:"rounded-xl border border-[var(--border)] bg-[var(--bg)] p-4"},Ke=["src"],Xe={class:"flex items-center gap-3"},Ye={key:0,width:"10",height:"10",viewBox:"0 0 24 24",fill:"currentColor"},Je={key:1,width:"10",height:"10",viewBox:"0 0 24 24",fill:"currentColor"},Qe={class:"text-xs tabular-nums text-[var(--ink3)]"},Ze={class:"ml-auto flex items-center gap-1"},et=["onClick"],lt={__name:"AudioPlayer",props:{audioUrl:{type:String,default:""},audioBlob:{type:Object,default:null}},setup(o){const n=o,d=y(()=>n.audioBlob?URL.createObjectURL(n.audioBlob):n.audioUrl),i=b(null),g=b(null),x=b(!1),m=b(0),a=b(0),s=b(1),v=y(()=>a.value>0?m.value/a.value*100:0);function w(){var e;m.value=((e=i.value)==null?void 0:e.currentTime)??0}function _(){var e;a.value=((e=i.value)==null?void 0:e.duration)??0}function $(){i.value&&(x.value?(i.value.pause(),x.value=!1):(i.value.play(),x.value=!0))}function T(e){if(!g.value||!i.value)return;const{left:r,width:l}=g.value.getBoundingClientRect();i.value.currentTime=Math.max(0,Math.min(1,(e.clientX-r)/l))*a.value}function c(e){s.value=e,i.value&&(i.value.playbackRate=e)}function u(e){return isFinite(e)?`${Math.floor(e/60)}:${Math.floor(e%60).toString().padStart(2,"0")}`:"0:00"}return N(s,e=>{i.value&&(i.value.playbackRate=e)}),(e,r)=>(p(),f("div",De,[r[3]||(r[3]=V('<div class="mb-3 flex items-center gap-2 text-[var(--ink2)]"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg><span class="text-[11px] font-semibold uppercase tracking-wider">Your Recording</span></div>',1)),t("audio",{ref_key:"audioEl",ref:i,src:d.value,preload:"metadata",class:"hidden",onTimeupdate:w,onLoadedmetadata:_,onEnded:r[0]||(r[0]=l=>x.value=!1)},null,40,Ke),t("div",{class:"relative mb-3 h-1.5 cursor-pointer overflow-hidden rounded-full bg-[var(--border)]",onClick:T,ref_key:"barEl",ref:g},[t("div",{class:"absolute inset-y-0 left-0 rounded-full bg-[#34d399] transition-[width]",style:L({width:v.value+"%"})},null,4)],512),t("div",Xe,[t("button",{class:"flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-2 border-[#34d399] text-[#34d399] transition hover:bg-[#34d399] hover:text-white",onClick:$},[x.value?(p(),f("svg",Je,[...r[2]||(r[2]=[t("rect",{x:"6",y:"4",width:"4",height:"16"},null,-1),t("rect",{x:"14",y:"4",width:"4",height:"16"},null,-1)])])):(p(),f("svg",Ye,[...r[1]||(r[1]=[t("polygon",{points:"5 3 19 12 5 21 5 3"},null,-1)])]))]),t("span",Qe,k(u(m.value))+" / "+k(u(a.value)),1),t("div",Ze,[(p(),f(R,null,G([.75,1,1.5],l=>t("button",{key:l,class:O(["rounded px-1.5 py-0.5 text-[10px] font-bold transition",s.value===l?"bg-[#34d399] text-white":"text-[var(--ink3)] hover:text-[var(--ink2)]"]),onClick:h=>c(l)},k(l)+"×",11,et)),64))])])]))}};export{rt as _,lt as a,at as b,st as c,nt as d,ot as e,U as f};
|
fronted/dist/assets/AudioPlayer-yjMJBD8R.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
[data-v-901d0371] .hl-grammar-error{background:#fecaca;color:#991b1b;border-radius:3px;padding:0 2px}.transcript-html[data-v-901d0371] mark{font-weight:600}[data-v-6f1180b1] .hl-vocab-weak{background:#fef08a;color:#854d0e;border-radius:3px;padding:0 2px}[data-v-6f1180b1] .hl-vocab-strong{background:#bbf7d0;color:#166534;border-radius:3px;padding:0 2px}.legend[data-v-6f1180b1]{display:inline-block;width:10px;height:10px;border-radius:2px}.legend--weak[data-v-6f1180b1]{background:#fef08a}.legend--strong[data-v-6f1180b1]{background:#bbf7d0}
|
|
|
|
|
|
fronted/dist/assets/Dashboard-BmWC62sI.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{u as F,p as X,q as U,o as t,c as s,a as e,b as A,F as M,r as C,t as a,n as I,f as D,d as Z,v as G,s as ee,w as te,h as z,i as K,x as R,y as S,j as N,k as Y,z as m,A as se,_ as Q,e as B,g as P,K as ne,B as oe,l as re}from"./index-BnN_E-ke.js";import{u as E,i as ae}from"./ielts-lE7MGpiZ.js";const ie={class:"space-y-4"},le={class:"ct-card overflow-hidden"},de={key:0,class:"flex justify-end"},ce={class:"max-w-[75%] rounded-2xl rounded-tr-sm bg-[#34d399] px-3.5 py-2.5 text-[13px] text-white shadow-sm"},ue={key:1,class:"flex gap-2"},ve={class:"max-w-[75%] rounded-2xl rounded-tl-sm bg-white px-3.5 py-2.5 text-[13px] leading-relaxed text-[var(--ink)] shadow-sm",style:{"white-space":"pre-wrap"}},he={key:0,class:"flex gap-2"},ge={class:"flex items-center gap-1 rounded-2xl rounded-tl-sm bg-white px-3.5 py-3 shadow-sm"},pe={class:"flex flex-wrap gap-1.5 border-t border-[var(--border)] px-4 py-2"},xe=["disabled","onClick"],fe={class:"flex gap-2 border-t border-[var(--border)] bg-white px-3 py-3"},be=["disabled","onKeydown"],ye=["disabled"],ke={class:"ct-card p-5"},me={class:"grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-5"},we=["innerHTML"],_e={class:"text-[12px] font-semibold text-[var(--ink)]"},$e={class:"ct-card p-5"},Me={class:"mb-1 flex items-center justify-between"},Se={class:"ct-badge",style:{background:"var(--green-bg)",color:"var(--green)"}},Ce={class:"mb-4 h-1.5 overflow-hidden rounded-full bg-[var(--bg2)]"},Le={class:"space-y-2"},Be={class:"flex items-center gap-3 min-w-0"},je=["innerHTML"],Te={class:"min-w-0"},De={class:"text-[11px] text-[var(--ink3)]"},ze={class:"ml-3 shrink-0"},He={key:0,class:"flex h-6 w-6 items-center justify-center rounded-full bg-[#34d399]/15"},Pe={key:1,class:"flex h-6 w-6 items-center justify-center rounded-full border border-[var(--border2)] text-[var(--ink3)] transition group-hover:border-[#34d399] group-hover:text-[#34d399]"},Ie={__name:"DashboardHome",setup(_){const n=E(),u=F(),f=N(""),x=N(!1),v=N([]),l=N(null),$=["How to improve speaking?","Give me a plan for today","How do I raise writing score?","What should I focus on first?"];async function j(o){f.value=o,await L()}async function L(){const o=f.value.trim();if(!(!o||x.value)){x.value=!0,v.value.push({role:"user",content:o}),f.value="",await b();try{const y=await ae.askDashboardCoach({userMessage:o,history:v.value.slice(-10)});v.value.push({role:"assistant",content:y.reply||y.error||"No response."})}catch{v.value.push({role:"assistant",content:"Catbot is unavailable right now. Please try again later."})}finally{x.value=!1,await b()}}}async function b(){await se(),l.value&&(l.value.scrollTop=l.value.scrollHeight)}X(()=>v.value.length,b);function c(){return new Date().toISOString().slice(0,10)}function r(o){return o?String(o).slice(0,10)===c():!1}const h=m(()=>n.history.some(o=>o.skill==="speaking"&&r(o.date))),i=m(()=>n.history.some(o=>o.skill==="writing"&&r(o.date))),H=m(()=>n.history.some(o=>o.skill==="reading"&&r(o.date))),V=m(()=>n.history.some(o=>o.skill==="listening"&&r(o.date))),T=m(()=>{var o;return!!((o=u.profile)!=null&&o.target_band)});U(()=>{const o=new Date,W=new Date(o.getFullYear(),o.getMonth(),o.getDate()+1,0,0,0)-o;setTimeout(async()=>{await n.fetchHistory()},W)});const g=[{label:"Reading",path:"/reading",color:"#2563eb",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>'},{label:"Listening",path:"/listening",color:"#7c3aed",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>'},{label:"Writing",path:"/writing",color:"#d97706",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>'},{label:"Speaking",path:"/speaking",color:"#059669",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'},{label:"Từ vựng",path:"/vocabulary",color:"#0891b2",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'}],w={profile:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>',speaking:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',writing:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',reading:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',listening:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>'},p=m(()=>[{label:"Set your target band score",hint:"Go to Profile and set your IELTS goal",route:"/profile",done:T.value,icon:w.profile},{label:"Complete a Speaking session",hint:"Find out your speaking level",route:"/speaking",done:h.value,icon:w.speaking},{label:"Complete a Writing session",hint:"Get AI feedback on your writing",route:"/writing",done:i.value,icon:w.writing},{label:"Complete a Reading test",hint:"Practice Cambridge-style passages",route:"/reading",done:H.value,icon:w.reading},{label:"Complete a Listening test",hint:"Train your listening comprehension",route:"/listening",done:V.value,icon:w.listening}]),d=m(()=>{const o=p.value.filter(y=>y.done).length;return Math.round(o/p.value.length*100)});return(o,y)=>{const W=Y("RouterLink");return t(),s("div",ie,[e("div",le,[y[5]||(y[5]=A('<div class="flex items-center gap-3 border-b border-[var(--border)] bg-[#f0fdf4] px-5 py-4"><div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white shadow-sm"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="8" width="16" height="11" rx="4"></rect><path d="M9 8V5a3 3 0 0 1 6 0v3"></path><circle cx="9.5" cy="13" r="1"></circle><circle cx="14.5" cy="13" r="1"></circle><path d="M9 16c1 .8 2 .8 3 .8s2 0 3-.8"></path></svg></div><div><div class="text-base font-bold text-[var(--ink)]">Catbot</div><div class="flex items-center gap-1.5 text-[11px] text-[var(--ink3)]"><span class="inline-block h-1.5 w-1.5 rounded-full bg-[#34d399]"></span> IELTS Coach · always online </div></div></div>',1)),e("div",{ref_key:"threadRef",ref:l,class:"flex max-h-72 flex-col gap-2 overflow-y-auto bg-[var(--bg)] px-4 py-3"},[y[3]||(y[3]=A('<div class="flex gap-2"><div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="8" width="16" height="11" rx="4"></rect><path d="M9 8V5a3 3 0 0 1 6 0v3"></path></svg></div><div class="max-w-[75%] rounded-2xl rounded-tl-sm bg-white px-3.5 py-2.5 text-[13px] text-[var(--ink)] shadow-sm"> Hey! I'm Catbot 👋 I'm here to make your IELTS prep effective. Ask me anything or pick a quick question below. </div></div>',1)),(t(!0),s(M,null,C(v.value,(k,J)=>(t(),s(M,{key:J},[k.role==="user"?(t(),s("div",de,[e("div",ce,a(k.content),1)])):(t(),s("div",ue,[y[1]||(y[1]=e("div",{class:"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white"},[e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("rect",{x:"4",y:"8",width:"16",height:"11",rx:"4"}),e("path",{d:"M9 8V5a3 3 0 0 1 6 0v3"})])],-1)),e("div",ve,a(k.content),1)]))],64))),128)),x.value?(t(),s("div",he,[y[2]||(y[2]=e("div",{class:"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white"},[e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("rect",{x:"4",y:"8",width:"16",height:"11",rx:"4"}),e("path",{d:"M9 8V5a3 3 0 0 1 6 0v3"})])],-1)),e("div",ge,[(t(),s(M,null,C(3,k=>e("span",{key:k,class:"h-1.5 w-1.5 rounded-full bg-[#34d399] animate-bounce",style:I({animationDelay:`${(k-1)*.15}s`})},null,4)),64))])])):D("",!0)],512),e("div",pe,[(t(),s(M,null,C($,k=>e("button",{key:k,class:"rounded-full border border-[var(--border2)] bg-white px-2.5 py-1 text-[11px] text-[var(--ink2)] transition hover:border-[#34d399] hover:text-[#059669]",disabled:x.value,onClick:J=>j(k)},a(k),9,xe)),64))]),e("div",fe,[Z(e("input",{"onUpdate:modelValue":y[0]||(y[0]=k=>f.value=k),class:"ct-input flex-1 text-[13px]",placeholder:"Ask anything about IELTS…",disabled:x.value,onKeydown:ee(te(L,["prevent"]),["enter"])},null,40,be),[[G,f.value]]),e("button",{class:"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[#34d399] text-white transition hover:bg-[#059669] disabled:opacity-40",disabled:x.value||!f.value.trim(),onClick:L},[...y[4]||(y[4]=[e("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.2"},[e("path",{d:"M22 2L11 13"}),e("path",{d:"M22 2L15 22l-4-9-9-4z"})],-1)])],8,ye)])]),e("div",ke,[y[6]||(y[6]=e("div",{class:"mb-3 text-sm font-bold text-[var(--ink)]"},"Kỹ năng IELTS",-1)),e("div",me,[(t(),s(M,null,C(g,k=>z(W,{key:k.path,to:k.path,class:"group flex flex-col items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3 text-center transition hover:border-[#34d399]/60 hover:bg-[#f0fdf4]"},{default:K(()=>[e("div",{class:"flex h-9 w-9 items-center justify-center rounded-lg text-white",style:I({background:k.color})},[e("span",{innerHTML:k.icon},null,8,we)],4),e("span",_e,a(k.label),1)]),_:2},1032,["to"])),64))])]),e("div",$e,[e("div",Me,[y[7]||(y[7]=e("div",{class:"text-base font-bold text-[var(--ink)]"},"Getting Started",-1)),e("span",Se,a(d.value)+"%",1)]),y[10]||(y[10]=e("p",{class:"mb-4 text-[12px] text-[var(--ink3)]"},"Complete these steps to set up your IELTS journey",-1)),e("div",Ce,[e("div",{class:"h-1.5 rounded-full bg-[#34d399] transition-all duration-500",style:I({width:`${d.value}%`})},null,4)]),e("div",Le,[(t(!0),s(M,null,C(p.value,k=>(t(),R(W,{key:k.label,to:k.route,class:S(["group flex items-center justify-between rounded-xl border border-[var(--border)] bg-[var(--bg)] px-4 py-3 transition hover:border-[#34d399]/50 hover:bg-[#f0fdf4]",k.done?"opacity-70":""])},{default:K(()=>[e("div",Be,[e("div",{class:S(["flex h-8 w-8 shrink-0 items-center justify-center rounded-lg",k.done?"bg-[#34d399]/15 text-[#059669]":"bg-[var(--bg2)] text-[var(--ink3)] group-hover:bg-[#34d399]/10 group-hover:text-[#34d399]"])},[e("span",{innerHTML:k.icon},null,8,je)],2),e("div",Te,[e("div",{class:S(["text-[13px] font-medium text-[var(--ink)] transition",k.done?"line-through decoration-[var(--rose)] decoration-2":""])},a(k.label),3),e("div",De,a(k.hint),1)])]),e("div",ze,[k.done?(t(),s("div",He,[...y[8]||(y[8]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"#059669","stroke-width":"2.5"},[e("path",{d:"M20 6L9 17l-5-5"})],-1)])])):(t(),s("div",Pe,[...y[9]||(y[9]=[e("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("path",{d:"M9 18l6-6-6-6"})],-1)])]))])]),_:2},1032,["to","class"]))),128))])])])}}},Re={class:"score-label"},Ne={class:"score-target"},Ae={key:0,class:"score-progress"},Ve={__name:"BandScoreCard",props:{label:{type:String,required:!0},score:{type:Number,default:null},target:{type:Number,required:!0},colorHex:{type:String,default:"var(--ink)"},overall:{type:Boolean,default:!1}},setup(_){const n=_,u=m(()=>n.target>0?Math.min(100,n.score/n.target*100):0);return(f,x)=>(t(),s("div",{class:S(["score-card",{"score-card--overall":_.overall}])},[e("div",Re,a(_.label),1),e("div",{class:"score-val",style:I({color:_.overall?"var(--green-l)":_.colorHex})},a(_.score??"—"),5),e("div",Ne,"Mục tiêu: "+a(_.target),1),_.overall?(t(),s("div",Ae,[e("div",{class:"score-progress-fill",style:I({width:u.value+"%"})},null,4)])):D("",!0)],2))}},O=Q(Ve,[["__scopeId","data-v-06126783"]]),qe={class:"flex flex-col items-center gap-4"},Oe=["width","height","viewBox"],Ee=["points"],We=["x1","y1","x2","y2"],Ke=["points"],Fe=["points"],Ue=["points"],Ye=["cx","cy"],Je=["x","y"],Qe=["x","y"],Xe=["x","y"],Ze={class:"flex flex-wrap items-center justify-center gap-4 text-[11px]"},Ge={class:"flex items-center gap-1.5"},et={class:"text-[var(--ink3)]"},q=9,tt={__name:"SkillRadarChart",props:{scores:{type:Object,default:()=>({reading:0,listening:0,writing:0,speaking:0})},target:{type:Number,default:6.5},size:{type:Number,default:280}},setup(_){const n=_,u=[2,4,6,8,9],f=[{key:"listening",label:"Listening"},{key:"reading",label:"Reading"},{key:"writing",label:"Writing"},{key:"speaking",label:"Speaking"}],x=m(()=>n.size/2),v=m(()=>n.size/2),l=m(()=>n.size*.36);function $(w){return Math.PI*2*w/f.length-Math.PI/2}function j(w){const p=$(w);return{x:x.value+l.value*Math.cos(p),y:v.value+l.value*Math.sin(p)}}function L(w){const p=$(w),d=l.value+22;return{x:x.value+d*Math.cos(p),y:v.value+d*Math.sin(p)}}function b(w){return f.map((p,d)=>{const o=$(d),y=l.value*w;return`${x.value+y*Math.cos(o)},${v.value+y*Math.sin(o)}`}).join(" ")}function c(w){return f.map((p,d)=>{const o=$(d),y=l.value*(Math.min(w[d],q)/q);return`${x.value+y*Math.cos(o)},${v.value+y*Math.sin(o)}`}).join(" ")}const r=m(()=>f.map(w=>Number(n.scores[w.key]||0))),h=m(()=>Number(n.target||6.5)),i=m(()=>f.map(()=>h.value)),H=[2,2,2,2],V=m(()=>f.map((w,p)=>{const d=$(p),o=l.value*(Math.min(r.value[p],q)/q);return{x:x.value+o*Math.cos(d),y:v.value+o*Math.sin(d)}}));function T(w){const p=$(w);return{x:Math.cos(p)*14,y:Math.sin(p)*14}}function g(w){return w?Number(w).toFixed(1):"—"}return(w,p)=>(t(),s("div",qe,[(t(),s("svg",{width:_.size,height:_.size,viewBox:`0 0 ${_.size} ${_.size}`,class:"overflow-visible"},[(t(),s(M,null,C(u,d=>e("g",{key:d},[e("polygon",{points:b(d/q),fill:"none",stroke:"var(--border)","stroke-width":"1"},null,8,Ee)])),64)),(t(),s(M,null,C(f,(d,o)=>e("line",{key:o,x1:x.value,y1:v.value,x2:j(o).x,y2:j(o).y,stroke:"var(--border)","stroke-width":"1"},null,8,We)),64)),e("polygon",{points:c(H),fill:"rgba(209,213,219,0.15)",stroke:"#d1d5db","stroke-width":"1.5","stroke-dasharray":"4 3"},null,8,Ke),e("polygon",{points:c(i.value),fill:"rgba(52,211,153,0.08)",stroke:"#34d399","stroke-width":"1.5","stroke-dasharray":"5 3"},null,8,Fe),e("polygon",{points:c(r.value),fill:"rgba(5,150,105,0.15)",stroke:"#059669","stroke-width":"2"},null,8,Ue),(t(!0),s(M,null,C(V.value,(d,o)=>(t(),s("circle",{key:`dot-${o}`,cx:d.x,cy:d.y,r:"4",fill:"#059669",stroke:"white","stroke-width":"1.5"},null,8,Ye))),128)),(t(),s(M,null,C(f,(d,o)=>e("text",{key:`lbl-${o}`,x:L(o).x,y:L(o).y,"text-anchor":"middle","dominant-baseline":"middle",class:"text-[11px] font-semibold",style:{fontSize:"11px",fontWeight:"600",fill:"var(--ink2)"}},a(d.label),9,Je)),64)),(t(!0),s(M,null,C(V.value,(d,o)=>(t(),s("text",{key:`score-${o}`,x:d.x+T(o).x,y:d.y+T(o).y,"text-anchor":"middle","dominant-baseline":"middle",style:{fontSize:"10px",fontWeight:"700",fill:"#059669"}},a(g(r.value[o])),9,Qe))),128)),(t(),s(M,null,C(u,d=>e("text",{key:`rv-${d}`,x:x.value+l.value*(d/q)+4,y:v.value,"dominant-baseline":"middle",style:{fontSize:"9px",fill:"var(--ink3)"}},a(d),9,Xe)),64))],8,Oe)),e("div",Ze,[p[1]||(p[1]=e("div",{class:"flex items-center gap-1.5"},[e("span",{class:"inline-block h-2.5 w-5 rounded-sm border border-dashed border-[#d1d5db] bg-[#d1d5db]/20"}),e("span",{class:"text-[var(--ink3)]"},"Min (2.0)")],-1)),e("div",Ge,[p[0]||(p[0]=e("span",{class:"inline-block h-2.5 w-5 rounded-sm border border-dashed border-[#34d399] bg-[#34d399]/10"},null,-1)),e("span",et,"Target ("+a(h.value)+")",1)]),p[2]||(p[2]=e("div",{class:"flex items-center gap-1.5"},[e("span",{class:"inline-block h-2.5 w-5 rounded-sm border border-[#059669] bg-[#059669]/15"}),e("span",{class:"text-[var(--ink3)]"},"Actual")],-1))])]))}},st={class:"space-y-4"},nt={class:"grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-5"},ot={class:"grid grid-cols-1 gap-4 xl:grid-cols-[320px_1fr]"},rt={class:"ct-card p-5"},at={class:"mb-1 flex items-center justify-between"},it={key:0,class:"text-[11px] text-[var(--ink3)]"},lt={key:1,class:"ct-badge",style:{background:"var(--green-bg)",color:"var(--green)"}},dt={key:0,class:"flex justify-center"},ct={key:1,class:"flex flex-col items-center justify-center py-10 text-center"},ut={key:2,class:"mt-3 grid grid-cols-2 gap-1.5 border-t border-[var(--border)] pt-3"},vt={class:"text-[11px] font-medium text-[var(--ink2)] capitalize"},ht={class:"flex items-center gap-1.5"},gt={class:"text-[10px] text-[var(--ink3)]"},pt={class:"ct-card overflow-hidden"},xt={class:"flex items-center justify-between border-b border-[var(--border)] px-4 py-3"},ft={key:0,class:"flex flex-col items-center justify-center py-12 text-center"},bt={key:1,class:"overflow-x-auto"},yt={class:"w-full border-collapse text-[12px]"},kt={class:"px-4 py-2.5 text-[var(--ink3)]"},mt={class:"px-4 py-2.5"},wt={class:"px-4 py-2.5 font-medium text-[var(--ink)]"},_t={class:"px-4 py-2.5"},$t={class:"px-4 py-2.5"},Mt={__name:"DashboardReports",setup(_){const n=E(),u=F(),f=N(!1),x=m(()=>{var c;return Number(((c=u.profile)==null?void 0:c.target_band)||7)}),v=m(()=>({reading:n.skillRadar.reading||0,listening:n.skillRadar.listening||0,writing:n.skillRadar.writing||0,speaking:n.skillRadar.speaking||0})),l=m(()=>Object.values(v.value).some(c=>c>0)),$=[{key:"reading",label:"Reading"},{key:"listening",label:"Listening"},{key:"writing",label:"Writing"},{key:"speaking",label:"Speaking"}];function j(c){return c?String(c).slice(0,10):"—"}function L(c){const r=Number(c);return r?r>=7?"text-[#059669]":r>=5?"text-[#d97706]":"text-[var(--rose)]":"text-[var(--ink3)]"}function b(c){return{reading:"bg-[#eff6ff] text-[#2563eb]",listening:"bg-[#f5f3ff] text-[#7c3aed]",writing:"bg-[#fff7ed] text-[#d97706]",speaking:"bg-[#f0fdf4] text-[#059669]"}[c]||"bg-[var(--bg2)] text-[var(--ink3)]"}return U(async()=>{Object.values(v.value).some(c=>c>0)||(f.value=!0,await n.fetchSkillRadar(),f.value=!1)}),(c,r)=>{const h=Y("RouterLink");return t(),s("div",st,[e("div",nt,[z(O,{label:"Overall",score:B(n).bandScores.overall,target:x.value,overall:!0},null,8,["score","target"]),z(O,{label:"Reading",score:B(n).bandScores.reading,target:x.value},null,8,["score","target"]),z(O,{label:"Listening",score:B(n).bandScores.listening,target:x.value},null,8,["score","target"]),z(O,{label:"Writing",score:B(n).bandScores.writing,target:x.value},null,8,["score","target"]),z(O,{label:"Speaking",score:B(n).bandScores.speaking,target:x.value},null,8,["score","target"])]),e("div",ot,[e("div",rt,[e("div",at,[r[0]||(r[0]=e("div",{class:"text-sm font-bold text-[var(--ink)]"},"Skill Radar",-1)),f.value?(t(),s("span",it,"Loading…")):(t(),s("span",lt,"Band 1–9"))]),r[2]||(r[2]=e("p",{class:"mb-4 text-[11px] text-[var(--ink3)]"},"Average band from your first attempt per quiz",-1)),l.value?(t(),s("div",dt,[z(tt,{scores:v.value,target:x.value,size:260},null,8,["scores","target"])])):(t(),s("div",ct,[...r[1]||(r[1]=[A('<div class="mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-[var(--bg2)] text-[var(--ink3)]"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 19h16M7 16V8M12 16V5M17 16v-3"></path></svg></div><p class="text-[13px] font-medium text-[var(--ink2)]">No data yet</p><p class="mt-1 text-[11px] text-[var(--ink3)]">Complete at least one quiz per skill to see your radar chart</p>',3)])])),l.value?(t(),s("div",ut,[(t(),s(M,null,C($,i=>{var H;return e("div",{key:i.key,class:"flex items-center justify-between rounded-lg bg-[var(--bg)] px-2.5 py-1.5"},[e("span",vt,a(i.label),1),e("div",ht,[e("span",{class:S(["text-[12px] font-bold",L(v.value[i.key])])},a(v.value[i.key]?v.value[i.key].toFixed(1):"—"),3),e("span",gt,"("+a(((H=B(n).skillRadar.attempts)==null?void 0:H[i.key])||0)+" quizzes)",1)])])}),64))])):D("",!0)]),e("div",pt,[e("div",xt,[r[4]||(r[4]=e("div",{class:"text-sm font-bold text-[var(--ink)]"},"Recent Attempts",-1)),z(h,{to:"/history",class:"text-[11px] font-medium text-[#34d399] hover:text-[#059669]"},{default:K(()=>[...r[3]||(r[3]=[P("View all →",-1)])]),_:1})]),B(n).history.length?(t(),s("div",bt,[e("table",yt,[r[6]||(r[6]=e("thead",null,[e("tr",{class:"border-b border-[var(--border)] bg-[var(--bg)] text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},[e("th",{class:"px-4 py-2.5"},"Date"),e("th",{class:"px-4 py-2.5"},"Skill"),e("th",{class:"px-4 py-2.5"},"Score"),e("th",{class:"px-4 py-2.5"},"Band"),e("th",{class:"px-4 py-2.5"},"Mode")])],-1)),e("tbody",null,[(t(!0),s(M,null,C(B(n).history.slice(0,15),i=>(t(),s("tr",{key:i.id,class:"border-b border-[var(--border)] transition hover:bg-[#f0fdf4]"},[e("td",kt,a(j(i.date)),1),e("td",mt,[e("span",{class:S(["inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium capitalize",b(i.skill)])},a(i.skill),3)]),e("td",wt,a(i.score??"—"),1),e("td",_t,[e("span",{class:S(["font-bold",L(i.band_score??i.score)])},a(i.band_score??i.score??"—"),3)]),e("td",$t,[e("span",{class:S(["rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase",i.mode==="exam"?"bg-[#111] text-white":"bg-[var(--bg2)] text-[var(--ink3)]"])},a(i.mode||"practice"),3)])]))),128))])])])):(t(),s("div",ft,[...r[5]||(r[5]=[A('<div class="mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-[var(--bg2)] text-[var(--ink3)]"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg></div><p class="text-[13px] text-[var(--ink2)]">No attempts yet</p><p class="mt-1 text-[11px] text-[var(--ink3)]">Start practicing to see your history here</p>',3)])]))])])])}}};function St(_={}){const n=new Date,u=m(()=>{const x=n.getFullYear(),v=n.getMonth(),l=new Date(x,v+1,0).getDate(),$=n.getDate();let j=new Date(x,v,1).getDay();j=j===0?6:j-1;const L=[];for(let b=0;b<j;b++)L.push({day:null,empty:!0,level:0});for(let b=1;b<=l;b++){const c=`${x}-${String(v+1).padStart(2,"0")}-${String(b).padStart(2,"0")}`,r=_[c]??0;let h=0;r>=3?h=3:r===2?h=2:r===1&&(h=1),L.push({day:b,empty:!1,isToday:b===$,level:h,count:r,key:c})}return L}),f=m(()=>n.toLocaleDateString("vi-VN",{month:"long",year:"numeric"}));return{calendarData:u,monthLabel:f}}const Ct={class:"heatmap-calendar"},Lt={class:"month-label"},Bt=["title"],jt={key:0},Tt={__name:"HeatmapCalendar",props:{activityMap:{type:Object,default:()=>({})},compact:{type:Boolean,default:!1}},setup(_){const n=_,{calendarData:u,monthLabel:f}=St(n.activityMap);return(x,v)=>(t(),s("div",Ct,[v[0]||(v[0]=A('<div class="heatmap-header" data-v-41c348c9><div data-v-41c348c9><div class="card-title font-display" data-v-41c348c9><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-v-41c348c9><rect x="3" y="4" width="18" height="18" rx="2" ry="2" data-v-41c348c9></rect><line x1="16" y1="2" x2="16" y2="6" data-v-41c348c9></line><line x1="8" y1="2" x2="8" y2="6" data-v-41c348c9></line><line x1="3" y1="10" x2="21" y2="10" data-v-41c348c9></line></svg> Study days </div><div class="card-sub" data-v-41c348c9>Đánh dấu ngày đã học</div></div><div class="legend" data-v-41c348c9><span class="legend-dot" data-v-41c348c9></span> Có nộp bài </div></div>',1)),e("div",Lt,a(B(f)),1),e("div",{class:S(["day-headers",{compact:n.compact}])},[(t(),s(M,null,C(["T2","T3","T4","T5","T6","T7","CN"],l=>e("div",{key:l,class:"day-header"},a(l),1)),64))],2),e("div",{class:S(["heatmap-grid",{compact:n.compact}])},[(t(!0),s(M,null,C(B(u),(l,$)=>(t(),s("div",{key:$,class:S(["heatmap-day",{empty:l.empty,today:l.isToday,"done-1":!l.isToday&&l.level===1,"done-2":!l.isToday&&l.level===2,"done-3":!l.isToday&&l.level===3}]),title:l.day?`${l.day}: ${l.count??0} bài`:""},[l.empty?D("",!0):(t(),s("span",jt,a(l.day),1))],10,Bt))),128))],2)]))}},Dt=Q(Tt,[["__scopeId","data-v-41c348c9"]]),zt={class:"grid grid-cols-1 gap-4 xl:grid-cols-[320px_1fr]"},Ht={class:"ct-card p-4"},Pt={class:"mb-3 flex items-center justify-between"},It={class:"ct-badge",style:{background:"var(--green-bg)",color:"var(--green)"}},Rt={key:0,class:"mt-4 border-t border-[var(--border)] pt-3"},Nt={class:"grid grid-cols-7 gap-0.5"},At={class:"flex h-8 w-full items-end justify-center overflow-hidden rounded-sm",title:""},Vt={class:"text-[9px] text-[var(--ink3)]"},qt={class:"ct-card p-4"},Ot={key:0,class:"flex flex-col items-center justify-center py-10 text-center"},Et={key:1,class:"space-y-3"},Wt={class:"mb-2 flex items-center justify-between"},Kt={class:"flex items-center gap-2"},Ft=["innerHTML"],Ut={class:"text-[13px] font-semibold text-[var(--ink)]"},Yt={class:"flex items-center gap-2"},Jt={class:"text-[12px] font-bold text-[var(--ink2)]"},Qt={class:"h-2 overflow-hidden rounded-full bg-white"},Xt={class:"mt-1.5 flex justify-between text-[10px] text-[var(--ink3)]"},Zt={key:0},Gt={__name:"DashboardProgress",setup(_){const n=E(),u=F(),f=m(()=>n.streak||0),x=m(()=>{var r;return Number(((r=u.profile)==null?void 0:r.target_band)||7)});function v(r){return Math.min(100,Math.round(Number(r.percentage||0)))}function l(r){const h=parseInt(r.time)||0;return Math.min(28,Math.round(h/90*28))}function $(r){const h=Number(r);return h?h>=7?"text-[#059669]":h>=5?"text-[#d97706]":"text-[var(--rose)]":"text-[var(--ink3)]"}const j={Reading:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',Listening:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>',Writing:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',Speaking:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></svg>'},L={Reading:"bg-[#2563eb]",Listening:"bg-[#7c3aed]",Writing:"bg-[#d97706]",Speaking:"bg-[#059669]"};function b(r){const h=Object.keys(j).find(i=>i.toLowerCase()===(r||"").toLowerCase());return j[h]||j.Reading}function c(r){const h=Object.keys(L).find(i=>i.toLowerCase()===(r||"").toLowerCase());return L[h]||"bg-[var(--ink3)]"}return(r,h)=>(t(),s("div",zt,[e("div",Ht,[e("div",Pt,[h[0]||(h[0]=e("div",{class:"text-sm font-bold text-[var(--ink)]"},"Activity",-1)),e("span",It,a(f.value)+" day streak",1)]),z(Dt,{"activity-map":B(n).activityMap,compact:""},null,8,["activity-map"]),B(n).weeklyStats.length?(t(),s("div",Rt,[h[1]||(h[1]=e("p",{class:"mb-2 text-[11px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},"This Week",-1)),e("div",Nt,[(t(!0),s(M,null,C(B(n).weeklyStats,i=>(t(),s("div",{key:i.date,class:"flex flex-col items-center gap-1"},[e("div",At,[e("div",{class:"w-full rounded-sm bg-[#34d399] transition-all",style:I({height:`${l(i)}px`,minHeight:i.time!=="0m"?"4px":"0"})},null,4)]),e("span",Vt,a(i.date),1)]))),128))])])):D("",!0)]),e("div",qt,[h[3]||(h[3]=e("div",{class:"mb-4 text-sm font-bold text-[var(--ink)]"},"Progress by Skill",-1)),B(n).progress.length?(t(),s("div",Et,[(t(!0),s(M,null,C(B(n).progress,i=>(t(),s("div",{key:i.id||i.subject,class:"rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3.5 transition hover:border-[#34d399]/40 hover:bg-[#f0fdf4]"},[e("div",Wt,[e("div",Kt,[e("div",{class:S(["flex h-7 w-7 items-center justify-center rounded-lg text-white",c(i.subject)])},[e("span",{innerHTML:b(i.subject)},null,8,Ft)],2),e("span",Ut,a(i.subject),1)]),e("div",Yt,[i.band_score?(t(),s("span",{key:0,class:S(["text-[11px] font-bold",$(i.band_score)])}," Band "+a(i.band_score),3)):D("",!0),e("span",Jt,a(v(i))+"%",1)])]),e("div",Qt,[e("div",{class:"h-2 rounded-full bg-[#34d399] transition-all duration-700",style:I({width:`${v(i)}%`})},null,4)]),e("div",Xt,[e("span",null,a(i.completed_questions??0)+" / "+a(i.total_questions??0)+" questions",1),i.band_score?(t(),s("span",Zt,"Target: "+a(x.value),1)):D("",!0)])]))),128))])):(t(),s("div",Ot,[...h[2]||(h[2]=[A('<div class="mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-[var(--bg2)] text-[var(--ink3)]"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h4l2-5 3 10 2-5h3"></path></svg></div><p class="text-[13px] text-[var(--ink2)]">No progress data yet</p><p class="mt-1 text-[11px] text-[var(--ink3)]">Complete IELTS practice sessions to track your progress</p>',3)])]))])]))}},es={class:"space-y-4"},ts={class:"ct-card flex flex-wrap items-center justify-between gap-3 px-5 py-4"},ss={class:"flex items-center gap-2"},ns={key:0,class:"ct-badge",style:{background:"var(--green-bg)",color:"var(--green)"}},os={class:"flex items-center gap-2"},rs=["disabled"],as={key:0,class:"mr-1.5 inline h-3.5 w-3.5 animate-spin",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},is={key:1,class:"mr-1.5 inline h-3.5 w-3.5",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},ls=["disabled"],ds={key:0,class:"mr-1.5 inline h-3.5 w-3.5 animate-spin",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},cs={key:1,class:"mr-1.5 inline h-3.5 w-3.5",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},us={key:0,class:"ct-card px-5 py-3"},vs={class:"mb-1.5 flex items-center justify-between text-[12px]"},hs={class:"font-bold text-[#059669]"},gs={class:"h-2 overflow-hidden rounded-full bg-[var(--bg2)]"},ps={key:1,class:"ct-card flex flex-col items-center justify-center py-16 text-center"},xs={key:2,class:"space-y-3"},fs={key:3,class:"space-y-3"},bs={class:"flex items-center gap-2"},ys={class:"text-[13px] font-bold text-[var(--ink)]"},ks={key:0,class:"ml-2 text-[11px] text-[var(--ink3)]"},ms={class:"flex items-center gap-2"},ws={key:0,class:"flex h-5 w-5 items-center justify-center rounded-full bg-[#34d399]"},_s={class:"divide-y divide-[var(--border)]"},$s=["onClick"],Ms={key:0,width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"white","stroke-width":"3.5"},Ss={class:"min-w-0 flex-1"},Cs={class:"mt-1 flex items-center gap-2.5 text-[11px] text-[var(--ink3)]"},Ls={class:"flex items-center gap-1"},Bs={class:"flex items-center gap-1 capitalize"},js={key:0,class:"text-[var(--rose)]"},Ts={__name:"DashboardStudyPlan",setup(_){const n=E(),u=N(!1),f=N(!1),x=m(()=>(n.studyPlanData.days||[]).length>0),v=m(()=>n.studyPlanData.total_tasks||0),l=m(()=>n.studyPlanData.completed_tasks||0);async function $(){u.value=!0,await n.generateStudyPlan(),u.value=!1}async function j(){f.value=!0,await n.extendStudyPlan(),f.value=!1}async function L(T){await n.completeStudyTask(T.id)}function b(T){return T.tasks.length>0&&T.tasks.every(g=>g.is_completed)}function c(T){var g;return((g=T.tasks[0])==null?void 0:g.focus_skill)||"reading"}function r(T){return T?new Date(T).toLocaleDateString("en-US",{weekday:"short",month:"short",day:"numeric"}):""}const h={reading:"bg-[#eff6ff] text-[#2563eb]",listening:"bg-[#f5f3ff] text-[#7c3aed]",writing:"bg-[#fff7ed] text-[#d97706]",speaking:"bg-[#f0fdf4] text-[#059669]"},i={reading:"bg-[#2563eb]",listening:"bg-[#7c3aed]",writing:"bg-[#d97706]",speaking:"bg-[#34d399]"};function H(T){return h[T]||"bg-[var(--bg2)] text-[var(--ink3)]"}function V(T){return i[T]||"bg-[var(--ink3)]"}return(T,g)=>{const w=Y("RouterLink");return t(),s("div",es,[e("div",ts,[e("div",null,[e("div",ss,[g[0]||(g[0]=e("div",{class:"text-base font-bold text-[var(--ink)]"},"Study Plan",-1)),v.value?(t(),s("span",ns,a(l.value)+"/"+a(v.value)+" done ",1)):D("",!0)]),g[1]||(g[1]=e("p",{class:"mt-0.5 text-[12px] text-[var(--ink3)]"},"AI-generated personalised roadmap based on your history",-1))]),e("div",os,[e("button",{class:"ct-btn px-3.5 py-2 text-[12px]",disabled:u.value,onClick:$},[u.value?(t(),s("svg",as,[...g[2]||(g[2]=[e("path",{d:"M21 12a9 9 0 1 1-6.219-8.56"},null,-1)])])):(t(),s("svg",is,[...g[3]||(g[3]=[e("path",{d:"M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"},null,-1)])])),P(" "+a(x.value?"Regenerate":"Generate Plan"),1)],8,rs),x.value?(t(),s("button",{key:0,class:"ct-btn px-3.5 py-2 text-[12px]",disabled:f.value,onClick:j},[f.value?(t(),s("svg",ds,[...g[4]||(g[4]=[e("path",{d:"M21 12a9 9 0 1 1-6.219-8.56"},null,-1)])])):(t(),s("svg",cs,[...g[5]||(g[5]=[e("path",{d:"M12 5v14M5 12h14"},null,-1)])])),g[6]||(g[6]=P(" Add 5 more days ",-1))],8,ls)):D("",!0)])]),v.value?(t(),s("div",us,[e("div",vs,[g[7]||(g[7]=e("span",{class:"font-medium text-[var(--ink2)]"},"Overall completion",-1)),e("span",hs,a(Math.round(l.value/v.value*100))+"%",1)]),e("div",gs,[e("div",{class:"h-2 rounded-full bg-[#34d399] transition-all duration-700",style:I({width:`${l.value/v.value*100}%`})},null,4)])])):D("",!0),!x.value&&!u.value?(t(),s("div",ps,[...g[8]||(g[8]=[A('<div class="mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-[#f0fdf4] text-[#34d399]"><svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"></path></svg></div><p class="text-[15px] font-semibold text-[var(--ink)]">No plan yet</p><p class="mt-1.5 max-w-xs text-[13px] text-[var(--ink3)]">Click "Generate Plan" to create a personalised AI study plan based on your skill levels and history.</p>',3)])])):D("",!0),u.value?(t(),s("div",xs,[(t(),s(M,null,C(5,p=>e("div",{key:p,class:"ct-card animate-pulse p-4"},[...g[9]||(g[9]=[e("div",{class:"mb-2 h-4 w-24 rounded bg-[var(--bg2)]"},null,-1),e("div",{class:"h-3 w-full rounded bg-[var(--bg2)]"},null,-1),e("div",{class:"mt-1.5 h-3 w-3/4 rounded bg-[var(--bg2)]"},null,-1)])])),64))])):D("",!0),!u.value&&x.value?(t(),s("div",fs,[(t(!0),s(M,null,C(B(n).studyPlanData.days,p=>(t(),s("div",{key:p.day_number,class:"ct-card overflow-hidden"},[e("div",{class:S(["flex items-center justify-between border-b border-[var(--border)] px-4 py-3",b(p)?"bg-[#f0fdf4]":"bg-[var(--bg)]"])},[e("div",bs,[e("div",{class:S(["flex h-7 w-7 items-center justify-center rounded-full text-[12px] font-bold",b(p)?"bg-[#34d399] text-white":"bg-[var(--bg2)] text-[var(--ink2)]"])},a(p.day_number),3),e("div",null,[e("span",ys,"Day "+a(p.day_number),1),p.plan_date?(t(),s("span",ks,a(r(p.plan_date)),1)):D("",!0)])]),e("div",ms,[e("span",{class:S(["rounded-md px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide",H(c(p))])},a(c(p)),3),b(p)?(t(),s("div",ws,[...g[10]||(g[10]=[e("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"white","stroke-width":"3"},[e("path",{d:"M20 6L9 17l-5-5"})],-1)])])):D("",!0)])],2),e("div",_s,[(t(!0),s(M,null,C(p.tasks,d=>(t(),s("div",{key:d.id,class:S(["group flex items-start gap-3 px-4 py-3 transition",d.is_completed?"bg-[#fafffe]":"hover:bg-[#f8fffe]"])},[e("button",{class:S(["mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded border-2 transition",d.is_completed?"border-[#34d399] bg-[#34d399]":"border-[var(--border2)] hover:border-[#34d399]"]),onClick:o=>L(d)},[d.is_completed?(t(),s("svg",Ms,[...g[11]||(g[11]=[e("path",{d:"M20 6L9 17l-5-5"},null,-1)])])):D("",!0)],10,$s),e("div",Ss,[e("div",{class:S(["text-[13px] font-medium leading-snug text-[var(--ink)] transition",d.is_completed?"line-through decoration-[var(--rose)] decoration-2 opacity-60":""])},a(d.task_description),3),e("div",Cs,[e("span",Ls,[g[12]||(g[12]=e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("circle",{cx:"12",cy:"12",r:"10"}),e("polyline",{points:"12 6 12 12 16 14"})],-1)),P(" "+a(d.duration_minutes)+" min ",1)]),e("span",Bs,[e("span",{class:S(["inline-block h-1.5 w-1.5 rounded-full",V(d.focus_skill)])},null,2),P(" "+a(d.focus_skill),1)]),d.is_completed?(t(),s("span",js,"✓ Completed")):D("",!0)])]),d.route_path&&!d.is_completed?(t(),R(w,{key:0,to:d.route_path,class:"ml-2 shrink-0 rounded-lg border border-[var(--border2)] bg-white px-2.5 py-1 text-[11px] font-medium text-[var(--ink2)] opacity-0 transition group-hover:opacity-100 hover:border-[#34d399] hover:text-[#059669]"},{default:K(()=>[...g[13]||(g[13]=[P(" Go → ",-1)])]),_:1},8,["to"])):D("",!0)],2))),128))])]))),128))])):D("",!0)])}}},Ds={class:"space-y-4"},zs={class:"ct-card flex flex-wrap items-center justify-between gap-3 px-5 py-3.5"},Hs={class:"text-[12px] text-[var(--ink3)]"},Ps={key:0,class:"ml-1 font-medium text-[var(--ink2)]"},Is={class:"flex items-center gap-3"},Rs={class:"flex items-center gap-1.5 rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-1.5 text-[12px]"},Ns={class:"text-[var(--ink)]"},As={class:"flex items-center gap-1.5 rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-1.5 text-[12px]"},Vs={class:"text-[var(--ink)]"},qs={class:"inline-flex rounded-xl border border-[var(--border)] bg-white p-1 shadow-sm"},Os=["onClick"],Es=["innerHTML"],Fs={__name:"Dashboard",setup(_){const n=F(),u=E(),f=oe(),x=re(),v=new Set(["home","reports","progress","study"]),l=m({get(){const b=f.query.tab;return v.has(b)?b:"home"},set(b){x.replace({query:{...f.query,tab:b}})}}),$={home:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/><polyline points="9 21 9 12 15 12 15 21"/></svg>',reports:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19h16M7 16V8M12 16V5M17 16v-3"/></svg>',progress:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h4l2-5 3 10 2-5h3"/></svg>',study:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>'},j=[{id:"home",label:"Home",icon:$.home},{id:"reports",label:"Reports",icon:$.reports},{id:"progress",label:"Progress",icon:$.progress},{id:"study",label:"Study Plan",icon:$.study}],L=m(()=>{var c;const b=(c=n.profile)==null?void 0:c.exam_date;return b?new Date(b).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}):"No exam date set"});return U(async()=>{var b;n.profile||await n.fetchProfile(),u.targetScores.overall=Number(((b=n.profile)==null?void 0:b.target_band)||7),u.targetScores.reading=u.targetScores.overall,u.targetScores.listening=u.targetScores.overall,u.targetScores.writing=u.targetScores.overall,u.targetScores.speaking=u.targetScores.overall,await Promise.all([u.fetchStats(),u.fetchHistory(),u.fetchProgress(),u.fetchPracticeAnalytics(),u.fetchStudyPlan(),u.fetchSkillRadar()])}),(b,c)=>{var r;return t(),s("div",Ds,[e("div",zs,[e("div",null,[c[0]||(c[0]=e("div",{class:"text-base font-bold text-[var(--ink)]"},"IELTS Academic",-1)),e("div",Hs,[P(a(L.value)+" ",1),B(u).daysToExam!==null?(t(),s("span",Ps,"("+a(B(u).daysToExam)+" days left)",1)):D("",!0)])]),e("div",Is,[e("div",Rs,[c[1]||(c[1]=e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"#34d399","stroke-width":"2"},[e("path",{d:"M13 2L3 14h9l-1 8 10-12h-9l1-8z"})],-1)),c[2]||(c[2]=e("span",{class:"text-[var(--ink3)]"},"Streak",-1)),e("strong",Ns,a(B(u).streak),1)]),e("div",As,[c[3]||(c[3]=e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"#34d399","stroke-width":"2"},[e("path",{d:"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"})],-1)),c[4]||(c[4]=e("span",{class:"text-[var(--ink3)]"},"Target",-1)),e("strong",Vs,a(((r=B(n).profile)==null?void 0:r.target_band)??"—"),1)])])]),e("div",qs,[(t(),s(M,null,C(j,h=>e("button",{key:h.id,class:S(["flex items-center gap-1.5 rounded-lg px-4 py-2 text-[13px] font-medium transition-all",l.value===h.id?"bg-[#111] text-white shadow-sm":"text-[var(--ink3)] hover:bg-[var(--bg2)] hover:text-[var(--ink)]"]),onClick:i=>l.value=h.id},[e("span",{innerHTML:h.icon,class:"shrink-0"},null,8,Es),P(" "+a(h.label),1)],10,Os)),64))]),(t(),R(ne,null,[l.value==="home"?(t(),R(Ie,{key:"home"})):l.value==="reports"?(t(),R(Mt,{key:"reports"})):l.value==="progress"?(t(),R(Gt,{key:"progress"})):(t(),R(Ts,{key:"study"}))],1024))])}}};export{Fs as default};
|
|
|
|
|
|
fronted/dist/assets/Dashboard-C9eprd66.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
.score-card[data-v-06126783]{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:18px;transition:box-shadow .18s,transform .18s;cursor:pointer}.score-card[data-v-06126783]:hover{box-shadow:var(--shadow);transform:translateY(-2px)}.score-card--overall[data-v-06126783]{background:var(--ink);border-color:transparent;color:#fff}.score-label[data-v-06126783]{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;opacity:.55}.score-card--overall .score-label[data-v-06126783]{color:#fff9;opacity:1}.score-val[data-v-06126783]{font-family:var(--font-display);font-size:32px;font-weight:700;margin:6px 0 4px;color:var(--ink);line-height:1}.score-target[data-v-06126783]{font-size:12px;color:var(--ink3)}.score-card--overall .score-target[data-v-06126783]{color:#ffffff73}.score-progress[data-v-06126783]{height:6px;background:#ffffff26;border-radius:99px;overflow:hidden;margin-top:10px}.score-progress-fill[data-v-06126783]{height:100%;border-radius:99px;background:var(--green-l);transition:width .6s ease}.heatmap-calendar[data-v-41c348c9]{width:100%}.heatmap-header[data-v-41c348c9]{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:14px}.card-title[data-v-41c348c9]{font-size:15px;font-weight:600;color:var(--ink);display:flex;align-items:center;gap:8px}.card-sub[data-v-41c348c9]{font-size:12px;color:var(--ink3);margin-top:3px}.legend[data-v-41c348c9]{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ink3)}.legend-dot[data-v-41c348c9]{width:10px;height:10px;border-radius:2px;background:var(--green-l);display:inline-block}.month-label[data-v-41c348c9]{font-size:12px;font-weight:700;color:var(--ink3);margin-bottom:8px;letter-spacing:.08em;text-transform:uppercase}.day-headers[data-v-41c348c9]{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;margin-bottom:6px}.day-header[data-v-41c348c9]{font-size:10px;text-align:center;color:var(--ink3);padding:3px;font-weight:600}.heatmap-grid[data-v-41c348c9]{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}.day-headers.compact .day-header[data-v-41c348c9]{font-size:9px;padding:1px}.heatmap-grid.compact[data-v-41c348c9]{gap:3px}.heatmap-grid.compact .heatmap-day[data-v-41c348c9]{min-height:22px;font-size:10px}
|
|
|
|
|
|
fronted/dist/assets/Dashboard-JNo6Dw71.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{u as U,p as Q,q as X,o as t,c as s,a as e,b as V,F as C,r as L,t as i,n as I,f as D,d as Z,v as G,s as ee,w as te,h as P,i as F,x as A,y as M,j as N,k as Y,z as k,A as se,e as j,g as R,B as ne,K as re,C as oe,l as ie}from"./index-CId0hMaT.js";import{u as W,i as ae}from"./ielts-4A3ec5AY.js";import"./historyService-y1ubbIgp.js";const le={class:"space-y-4"},de={class:"ct-card overflow-hidden"},ce={key:0,class:"flex justify-end"},ue={class:"max-w-[75%] rounded-2xl rounded-tr-sm bg-[#34d399] px-3.5 py-2.5 text-[13px] text-white shadow-sm"},ve={key:1,class:"flex gap-2"},he={class:"max-w-[75%] rounded-2xl rounded-tl-sm bg-white px-3.5 py-2.5 text-[13px] leading-relaxed text-[var(--ink)] shadow-sm",style:{"white-space":"pre-wrap"}},ge={key:0,class:"flex gap-2"},pe={class:"flex items-center gap-1 rounded-2xl rounded-tl-sm bg-white px-3.5 py-3 shadow-sm"},xe={class:"flex flex-wrap gap-1.5 border-t border-[var(--border)] px-4 py-2"},fe=["disabled","onClick"],be={class:"flex gap-2 border-t border-[var(--border)] bg-white px-3 py-3"},ye=["disabled","onKeydown"],ke=["disabled"],me={class:"ct-card p-5"},we={class:"grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-5"},_e=["innerHTML"],$e={class:"text-[12px] font-semibold text-[var(--ink)]"},Me={class:"ct-card p-5"},Se={class:"mb-1 flex items-center justify-between"},Ce={class:"ct-badge",style:{background:"var(--green-bg)",color:"var(--green)"}},Le={class:"mb-4 h-1.5 overflow-hidden rounded-full bg-[var(--bg2)]"},Be={class:"space-y-2"},je={class:"flex items-center gap-3 min-w-0"},Te=["innerHTML"],ze={class:"min-w-0"},De={class:"text-[11px] text-[var(--ink3)]"},He={class:"ml-3 shrink-0"},Pe={key:0,class:"flex h-6 w-6 items-center justify-center rounded-full bg-[#34d399]/15"},Re={key:1,class:"flex h-6 w-6 items-center justify-center rounded-full border border-[var(--border2)] text-[var(--ink3)] transition group-hover:border-[#34d399] group-hover:text-[#34d399]"},Ie={__name:"DashboardHome",setup(m){const r=W(),d=U(),f=N(""),x=N(!1),v=N([]),a=N(null),$=["How to improve speaking?","Give me a plan for today","How do I raise writing score?","What should I focus on first?"];async function T(u){f.value=u,await B()}async function B(){const u=f.value.trim();if(!(!u||x.value)){x.value=!0,v.value.push({role:"user",content:u}),f.value="",await _();try{const b=await ae.askDashboardCoach({userMessage:u,history:v.value.slice(-10)});v.value.push({role:"assistant",content:b.reply||b.error||"No response."})}catch{v.value.push({role:"assistant",content:"Catbot is unavailable right now. Please try again later."})}finally{x.value=!1,await _()}}}async function _(){await se(),a.value&&(a.value.scrollTop=a.value.scrollHeight)}Q(()=>v.value.length,_);function h(){return new Date().toISOString().slice(0,10)}function n(u){return u?String(u).slice(0,10)===h():!1}const g=k(()=>r.history.some(u=>u.skill==="speaking"&&n(u.date))),o=k(()=>r.history.some(u=>u.skill==="writing"&&n(u.date))),H=k(()=>r.history.some(u=>u.skill==="reading"&&n(u.date))),q=k(()=>r.history.some(u=>u.skill==="listening"&&n(u.date))),z=k(()=>r.history.some(u=>u.skill==="vocabulary"&&n(u.date))),p=k(()=>{var u;return!!((u=d.profile)!=null&&u.target_band)});X(()=>{const u=new Date,K=new Date(u.getFullYear(),u.getMonth(),u.getDate()+1,0,0,0)-u;setTimeout(async()=>{await r.fetchHistory()},K)});const S=[{label:"Reading",path:"/reading",color:"#2563eb",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>'},{label:"Listening",path:"/listening",color:"#7c3aed",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>'},{label:"Writing",path:"/writing",color:"#d97706",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>'},{label:"Speaking",path:"/speaking",color:"#059669",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'},{label:"Từ vựng",path:"/vocabulary",color:"#0891b2",icon:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'}],c={profile:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>',speaking:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',writing:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',reading:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',listening:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>',vocabulary:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'},l=k(()=>[{label:"Set your target band score",hint:"Go to Profile and set your IELTS goal",route:"/profile",done:p.value,icon:c.profile},{label:"Complete a Speaking session",hint:"Find out your speaking level",route:"/speaking",done:g.value,icon:c.speaking},{label:"Complete a Writing session",hint:"Get AI feedback on your writing",route:"/writing",done:o.value,icon:c.writing},{label:"Complete a Reading test",hint:"Practice Cambridge-style passages",route:"/reading",done:H.value,icon:c.reading},{label:"Complete a Listening test",hint:"Train your listening comprehension",route:"/listening",done:q.value,icon:c.listening},{label:"Ôn từ vựng hôm nay",hint:"SRS flashcard · 10 phút = 1 XP",route:"/vocabulary",done:z.value,icon:c.vocabulary}]),w=k(()=>{const u=l.value.filter(b=>b.done).length;return Math.round(u/l.value.length*100)});return(u,b)=>{const K=Y("RouterLink");return t(),s("div",le,[e("div",de,[b[5]||(b[5]=V('<div class="flex items-center gap-3 border-b border-[var(--border)] bg-[#f0fdf4] px-5 py-4"><div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white shadow-sm"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="8" width="16" height="11" rx="4"></rect><path d="M9 8V5a3 3 0 0 1 6 0v3"></path><circle cx="9.5" cy="13" r="1"></circle><circle cx="14.5" cy="13" r="1"></circle><path d="M9 16c1 .8 2 .8 3 .8s2 0 3-.8"></path></svg></div><div><div class="text-base font-bold text-[var(--ink)]">Catbot</div><div class="flex items-center gap-1.5 text-[11px] text-[var(--ink3)]"><span class="inline-block h-1.5 w-1.5 rounded-full bg-[#34d399]"></span> IELTS Coach · always online </div></div></div>',1)),e("div",{ref_key:"threadRef",ref:a,class:"flex max-h-72 flex-col gap-2 overflow-y-auto bg-[var(--bg)] px-4 py-3"},[b[3]||(b[3]=V('<div class="flex gap-2"><div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="8" width="16" height="11" rx="4"></rect><path d="M9 8V5a3 3 0 0 1 6 0v3"></path></svg></div><div class="max-w-[75%] rounded-2xl rounded-tl-sm bg-white px-3.5 py-2.5 text-[13px] text-[var(--ink)] shadow-sm"> Hey! I'm Catbot 👋 I'm here to make your IELTS prep effective. Ask me anything or pick a quick question below. </div></div>',1)),(t(!0),s(C,null,L(v.value,(y,J)=>(t(),s(C,{key:J},[y.role==="user"?(t(),s("div",ce,[e("div",ue,i(y.content),1)])):(t(),s("div",ve,[b[1]||(b[1]=e("div",{class:"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white"},[e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("rect",{x:"4",y:"8",width:"16",height:"11",rx:"4"}),e("path",{d:"M9 8V5a3 3 0 0 1 6 0v3"})])],-1)),e("div",he,i(y.content),1)]))],64))),128)),x.value?(t(),s("div",ge,[b[2]||(b[2]=e("div",{class:"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white"},[e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("rect",{x:"4",y:"8",width:"16",height:"11",rx:"4"}),e("path",{d:"M9 8V5a3 3 0 0 1 6 0v3"})])],-1)),e("div",pe,[(t(),s(C,null,L(3,y=>e("span",{key:y,class:"h-1.5 w-1.5 rounded-full bg-[#34d399] animate-bounce",style:I({animationDelay:`${(y-1)*.15}s`})},null,4)),64))])])):D("",!0)],512),e("div",xe,[(t(),s(C,null,L($,y=>e("button",{key:y,class:"rounded-full border border-[var(--border2)] bg-white px-2.5 py-1 text-[11px] text-[var(--ink2)] transition hover:border-[#34d399] hover:text-[#059669]",disabled:x.value,onClick:J=>T(y)},i(y),9,fe)),64))]),e("div",be,[Z(e("input",{"onUpdate:modelValue":b[0]||(b[0]=y=>f.value=y),class:"ct-input flex-1 text-[13px]",placeholder:"Ask anything about IELTS…",disabled:x.value,onKeydown:ee(te(B,["prevent"]),["enter"])},null,40,ye),[[G,f.value]]),e("button",{class:"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[#34d399] text-white transition hover:bg-[#059669] disabled:opacity-40",disabled:x.value||!f.value.trim(),onClick:B},[...b[4]||(b[4]=[e("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.2"},[e("path",{d:"M22 2L11 13"}),e("path",{d:"M22 2L15 22l-4-9-9-4z"})],-1)])],8,ke)])]),e("div",me,[b[6]||(b[6]=e("div",{class:"mb-3 text-sm font-bold text-[var(--ink)]"},"Kỹ năng IELTS",-1)),e("div",we,[(t(),s(C,null,L(S,y=>P(K,{key:y.path,to:y.path,class:"group flex flex-col items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3 text-center transition hover:border-[#34d399]/60 hover:bg-[#f0fdf4]"},{default:F(()=>[e("div",{class:"flex h-9 w-9 items-center justify-center rounded-lg text-white",style:I({background:y.color})},[e("span",{innerHTML:y.icon},null,8,_e)],4),e("span",$e,i(y.label),1)]),_:2},1032,["to"])),64))])]),e("div",Me,[e("div",Se,[b[7]||(b[7]=e("div",{class:"text-base font-bold text-[var(--ink)]"},"Getting Started",-1)),e("span",Ce,i(w.value)+"%",1)]),b[10]||(b[10]=e("p",{class:"mb-4 text-[12px] text-[var(--ink3)]"},"Hoàn thành các bước IELTS và ôn từ vựng (10 phút = 1 XP)",-1)),e("div",Le,[e("div",{class:"h-1.5 rounded-full bg-[#34d399] transition-all duration-500",style:I({width:`${w.value}%`})},null,4)]),e("div",Be,[(t(!0),s(C,null,L(l.value,y=>(t(),A(K,{key:y.label,to:y.route,class:M(["group flex items-center justify-between rounded-xl border border-[var(--border)] bg-[var(--bg)] px-4 py-3 transition hover:border-[#34d399]/50 hover:bg-[#f0fdf4]",y.done?"opacity-70":""])},{default:F(()=>[e("div",je,[e("div",{class:M(["flex h-8 w-8 shrink-0 items-center justify-center rounded-lg",y.done?"bg-[#34d399]/15 text-[#059669]":"bg-[var(--bg2)] text-[var(--ink3)] group-hover:bg-[#34d399]/10 group-hover:text-[#34d399]"])},[e("span",{innerHTML:y.icon},null,8,Te)],2),e("div",ze,[e("div",{class:M(["text-[13px] font-medium text-[var(--ink)] transition",y.done?"line-through decoration-[var(--rose)] decoration-2":""])},i(y.label),3),e("div",De,i(y.hint),1)])]),e("div",He,[y.done?(t(),s("div",Pe,[...b[8]||(b[8]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"#059669","stroke-width":"2.5"},[e("path",{d:"M20 6L9 17l-5-5"})],-1)])])):(t(),s("div",Re,[...b[9]||(b[9]=[e("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("path",{d:"M9 18l6-6-6-6"})],-1)])]))])]),_:2},1032,["to","class"]))),128))])])])}}},Ae={key:0,class:"mt-2.5 h-1.5 overflow-hidden rounded-full bg-white/15"},E={__name:"BandScoreCard",props:{label:{type:String,required:!0},score:{type:Number,default:null},target:{type:Number,required:!0},colorHex:{type:String,default:"var(--ink)"},overall:{type:Boolean,default:!1}},setup(m){const r=m,d=k(()=>r.target>0?Math.min(100,r.score/r.target*100):0);return(f,x)=>(t(),s("div",{class:M(["cursor-pointer rounded-[var(--r)] border p-[18px] transition-all hover:-translate-y-0.5 hover:shadow-[var(--shadow)]",m.overall?"border-transparent bg-[var(--ink)] text-white":"border-[var(--border)] bg-[var(--surface)]"])},[e("div",{class:M(["text-[11px] font-semibold uppercase tracking-wider",m.overall?"text-white/60":"text-[var(--ink3)] opacity-55"])},i(m.label),3),e("div",{class:"font-display my-1.5 text-[32px] font-bold leading-none",style:I({color:m.overall?"var(--green-l)":m.colorHex})},i(m.score??"—"),5),e("div",{class:M(m.overall?"text-xs text-white/45":"text-xs text-[var(--ink3)]")},"Mục tiêu: "+i(m.target),3),m.overall?(t(),s("div",Ae,[e("div",{class:"h-full rounded-full bg-[var(--green-l)] transition-[width] duration-500 ease-out",style:I({width:d.value+"%"})},null,4)])):D("",!0)],2))}},Ne={class:"flex flex-col items-center gap-4"},Ve=["width","height","viewBox"],qe=["points"],Oe=["x1","y1","x2","y2"],Ee=["points"],We=["points"],Ke=["points"],Fe=["cx","cy"],Ue=["x","y"],Xe=["x","y"],Ye=["x","y"],Je={class:"flex flex-wrap items-center justify-center gap-4 text-[11px]"},Qe={class:"flex items-center gap-1.5"},Ze={class:"text-[var(--ink3)]"},O=9,Ge={__name:"SkillRadarChart",props:{scores:{type:Object,default:()=>({reading:0,listening:0,writing:0,speaking:0})},target:{type:Number,default:6.5},size:{type:Number,default:280}},setup(m){const r=m,d=[2,4,6,8,9],f=[{key:"listening",label:"Listening"},{key:"reading",label:"Reading"},{key:"writing",label:"Writing"},{key:"speaking",label:"Speaking"}],x=k(()=>r.size/2),v=k(()=>r.size/2),a=k(()=>r.size*.36);function $(S){return Math.PI*2*S/f.length-Math.PI/2}function T(S){const c=$(S);return{x:x.value+a.value*Math.cos(c),y:v.value+a.value*Math.sin(c)}}function B(S){const c=$(S),l=a.value+22;return{x:x.value+l*Math.cos(c),y:v.value+l*Math.sin(c)}}function _(S){return f.map((c,l)=>{const w=$(l),u=a.value*S;return`${x.value+u*Math.cos(w)},${v.value+u*Math.sin(w)}`}).join(" ")}function h(S){return f.map((c,l)=>{const w=$(l),u=a.value*(Math.min(S[l],O)/O);return`${x.value+u*Math.cos(w)},${v.value+u*Math.sin(w)}`}).join(" ")}const n=k(()=>f.map(S=>Number(r.scores[S.key]||0))),g=k(()=>Number(r.target||6.5)),o=k(()=>f.map(()=>g.value)),H=[2,2,2,2],q=k(()=>f.map((S,c)=>{const l=$(c),w=a.value*(Math.min(n.value[c],O)/O);return{x:x.value+w*Math.cos(l),y:v.value+w*Math.sin(l)}}));function z(S){const c=$(S);return{x:Math.cos(c)*14,y:Math.sin(c)*14}}function p(S){return S?Number(S).toFixed(1):"—"}return(S,c)=>(t(),s("div",Ne,[(t(),s("svg",{width:m.size,height:m.size,viewBox:`0 0 ${m.size} ${m.size}`,class:"overflow-visible"},[(t(),s(C,null,L(d,l=>e("g",{key:l},[e("polygon",{points:_(l/O),fill:"none",stroke:"var(--border)","stroke-width":"1"},null,8,qe)])),64)),(t(),s(C,null,L(f,(l,w)=>e("line",{key:w,x1:x.value,y1:v.value,x2:T(w).x,y2:T(w).y,stroke:"var(--border)","stroke-width":"1"},null,8,Oe)),64)),e("polygon",{points:h(H),fill:"rgba(209,213,219,0.15)",stroke:"#d1d5db","stroke-width":"1.5","stroke-dasharray":"4 3"},null,8,Ee),e("polygon",{points:h(o.value),fill:"rgba(52,211,153,0.08)",stroke:"#34d399","stroke-width":"1.5","stroke-dasharray":"5 3"},null,8,We),e("polygon",{points:h(n.value),fill:"rgba(5,150,105,0.15)",stroke:"#059669","stroke-width":"2"},null,8,Ke),(t(!0),s(C,null,L(q.value,(l,w)=>(t(),s("circle",{key:`dot-${w}`,cx:l.x,cy:l.y,r:"4",fill:"#059669",stroke:"white","stroke-width":"1.5"},null,8,Fe))),128)),(t(),s(C,null,L(f,(l,w)=>e("text",{key:`lbl-${w}`,x:B(w).x,y:B(w).y,"text-anchor":"middle","dominant-baseline":"middle",class:"text-[11px] font-semibold",style:{fontSize:"11px",fontWeight:"600",fill:"var(--ink2)"}},i(l.label),9,Ue)),64)),(t(!0),s(C,null,L(q.value,(l,w)=>(t(),s("text",{key:`score-${w}`,x:l.x+z(w).x,y:l.y+z(w).y,"text-anchor":"middle","dominant-baseline":"middle",style:{fontSize:"10px",fontWeight:"700",fill:"#059669"}},i(p(n.value[w])),9,Xe))),128)),(t(),s(C,null,L(d,l=>e("text",{key:`rv-${l}`,x:x.value+a.value*(l/O)+4,y:v.value,"dominant-baseline":"middle",style:{fontSize:"9px",fill:"var(--ink3)"}},i(l),9,Ye)),64))],8,Ve)),e("div",Je,[c[1]||(c[1]=e("div",{class:"flex items-center gap-1.5"},[e("span",{class:"inline-block h-2.5 w-5 rounded-sm border border-dashed border-[#d1d5db] bg-[#d1d5db]/20"}),e("span",{class:"text-[var(--ink3)]"},"Min (2.0)")],-1)),e("div",Qe,[c[0]||(c[0]=e("span",{class:"inline-block h-2.5 w-5 rounded-sm border border-dashed border-[#34d399] bg-[#34d399]/10"},null,-1)),e("span",Ze,"Target ("+i(g.value)+")",1)]),c[2]||(c[2]=e("div",{class:"flex items-center gap-1.5"},[e("span",{class:"inline-block h-2.5 w-5 rounded-sm border border-[#059669] bg-[#059669]/15"}),e("span",{class:"text-[var(--ink3)]"},"Actual")],-1))])]))}},et={class:"space-y-4"},tt={class:"grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-5"},st={class:"grid grid-cols-1 gap-4 xl:grid-cols-[320px_1fr]"},nt={class:"ct-card p-5"},rt={class:"mb-1 flex items-center justify-between"},ot={key:0,class:"text-[11px] text-[var(--ink3)]"},it={key:1,class:"ct-badge",style:{background:"var(--green-bg)",color:"var(--green)"}},at={key:0,class:"flex justify-center"},lt={key:1,class:"flex flex-col items-center justify-center py-10 text-center"},dt={key:2,class:"mt-3 grid grid-cols-2 gap-1.5 border-t border-[var(--border)] pt-3"},ct={class:"text-[11px] font-medium text-[var(--ink2)] capitalize"},ut={class:"flex items-center gap-1.5"},vt={class:"text-[10px] text-[var(--ink3)]"},ht={class:"ct-card overflow-hidden"},gt={class:"flex items-center justify-between border-b border-[var(--border)] px-4 py-3"},pt={key:0,class:"flex flex-col items-center justify-center py-12 text-center"},xt={key:1,class:"overflow-x-auto"},ft={class:"w-full border-collapse text-[12px]"},bt={class:"px-4 py-2.5 text-[var(--ink3)]"},yt={class:"px-4 py-2.5"},kt={class:"px-4 py-2.5 font-medium text-[var(--ink)]"},mt={class:"px-4 py-2.5"},wt={class:"px-4 py-2.5"},_t={__name:"DashboardReports",setup(m){const r=W(),d=U(),f=N(!1),x=k(()=>{var h;return Number(((h=d.profile)==null?void 0:h.target_band)||7)}),v=k(()=>({reading:r.skillRadar.reading||0,listening:r.skillRadar.listening||0,writing:r.skillRadar.writing||0,speaking:r.skillRadar.speaking||0})),a=k(()=>Object.values(v.value).some(h=>h>0)),$=[{key:"reading",label:"Reading"},{key:"listening",label:"Listening"},{key:"writing",label:"Writing"},{key:"speaking",label:"Speaking"}];function T(h){return h?String(h).slice(0,10):"—"}function B(h){const n=Number(h);return n?n>=7?"text-[#059669]":n>=5?"text-[#d97706]":"text-[var(--rose)]":"text-[var(--ink3)]"}function _(h){return{reading:"bg-[#eff6ff] text-[#2563eb]",listening:"bg-[#f5f3ff] text-[#7c3aed]",writing:"bg-[#fff7ed] text-[#d97706]",speaking:"bg-[#f0fdf4] text-[#059669]",vocabulary:"bg-[#ecfeff] text-[#0891b2]"}[h]||"bg-[var(--bg2)] text-[var(--ink3)]"}return X(async()=>{Object.values(v.value).some(h=>h>0)||(f.value=!0,await r.fetchSkillRadar(),f.value=!1)}),(h,n)=>{const g=Y("RouterLink");return t(),s("div",et,[e("div",tt,[P(E,{label:"Overall",score:j(r).bandScores.overall,target:x.value,overall:!0},null,8,["score","target"]),P(E,{label:"Reading",score:j(r).bandScores.reading,target:x.value},null,8,["score","target"]),P(E,{label:"Listening",score:j(r).bandScores.listening,target:x.value},null,8,["score","target"]),P(E,{label:"Writing",score:j(r).bandScores.writing,target:x.value},null,8,["score","target"]),P(E,{label:"Speaking",score:j(r).bandScores.speaking,target:x.value},null,8,["score","target"])]),e("div",st,[e("div",nt,[e("div",rt,[n[0]||(n[0]=e("div",{class:"text-sm font-bold text-[var(--ink)]"},"Skill Radar",-1)),f.value?(t(),s("span",ot,"Loading…")):(t(),s("span",it,"Band 1–9"))]),n[2]||(n[2]=e("p",{class:"mb-4 text-[11px] text-[var(--ink3)]"},"Average band from your first attempt per quiz",-1)),a.value?(t(),s("div",at,[P(Ge,{scores:v.value,target:x.value,size:260},null,8,["scores","target"])])):(t(),s("div",lt,[...n[1]||(n[1]=[V('<div class="mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-[var(--bg2)] text-[var(--ink3)]"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 19h16M7 16V8M12 16V5M17 16v-3"></path></svg></div><p class="text-[13px] font-medium text-[var(--ink2)]">No data yet</p><p class="mt-1 text-[11px] text-[var(--ink3)]">Complete at least one quiz per skill to see your radar chart</p>',3)])])),a.value?(t(),s("div",dt,[(t(),s(C,null,L($,o=>{var H;return e("div",{key:o.key,class:"flex items-center justify-between rounded-lg bg-[var(--bg)] px-2.5 py-1.5"},[e("span",ct,i(o.label),1),e("div",ut,[e("span",{class:M(["text-[12px] font-bold",B(v.value[o.key])])},i(v.value[o.key]?v.value[o.key].toFixed(1):"—"),3),e("span",vt,"("+i(((H=j(r).skillRadar.attempts)==null?void 0:H[o.key])||0)+" quizzes)",1)])])}),64))])):D("",!0)]),e("div",ht,[e("div",gt,[n[4]||(n[4]=e("div",{class:"text-sm font-bold text-[var(--ink)]"},"Recent Attempts",-1)),P(g,{to:"/history",class:"text-[11px] font-medium text-[#34d399] hover:text-[#059669]"},{default:F(()=>[...n[3]||(n[3]=[R("View all →",-1)])]),_:1})]),j(r).history.length?(t(),s("div",xt,[e("table",ft,[n[6]||(n[6]=e("thead",null,[e("tr",{class:"border-b border-[var(--border)] bg-[var(--bg)] text-left text-[11px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},[e("th",{class:"px-4 py-2.5"},"Date"),e("th",{class:"px-4 py-2.5"},"Skill"),e("th",{class:"px-4 py-2.5"},"Score"),e("th",{class:"px-4 py-2.5"},"Band"),e("th",{class:"px-4 py-2.5"},"Mode")])],-1)),e("tbody",null,[(t(!0),s(C,null,L(j(r).history.slice(0,15),o=>(t(),s("tr",{key:o.id,class:"border-b border-[var(--border)] transition hover:bg-[#f0fdf4]"},[e("td",bt,i(T(o.date)),1),e("td",yt,[e("span",{class:M(["inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium capitalize",_(o.skill)])},i(o.skill),3)]),e("td",kt,i(o.score??"—"),1),e("td",mt,[e("span",{class:M(["font-bold",B(o.band_score??o.score)])},i(o.band_score??o.score??"—"),3)]),e("td",wt,[e("span",{class:M(["rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase",o.mode==="exam"?"bg-[#111] text-white":"bg-[var(--bg2)] text-[var(--ink3)]"])},i(o.mode||"practice"),3)])]))),128))])])])):(t(),s("div",pt,[...n[5]||(n[5]=[V('<div class="mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-[var(--bg2)] text-[var(--ink3)]"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg></div><p class="text-[13px] text-[var(--ink2)]">No attempts yet</p><p class="mt-1 text-[11px] text-[var(--ink3)]">Start practicing to see your history here</p>',3)])]))])])])}}};function $t(m={}){const r=new Date,d=k(()=>{const x=r.getFullYear(),v=r.getMonth(),a=new Date(x,v+1,0).getDate(),$=r.getDate();let T=new Date(x,v,1).getDay();T=T===0?6:T-1;const B=[];for(let _=0;_<T;_++)B.push({day:null,empty:!0,level:0});for(let _=1;_<=a;_++){const h=`${x}-${String(v+1).padStart(2,"0")}-${String(_).padStart(2,"0")}`,n=m[h]??0;let g=0;n>=3?g=3:n===2?g=2:n===1&&(g=1),B.push({day:_,empty:!1,isToday:_===$,level:g,count:n,key:h})}return B}),f=k(()=>r.toLocaleDateString("vi-VN",{month:"long",year:"numeric"}));return{calendarData:d,monthLabel:f}}const Mt={class:"w-full"},St={class:"mb-2 text-xs font-bold uppercase tracking-wider text-[var(--ink3)]"},Ct=["title"],Lt={key:0},Bt={__name:"HeatmapCalendar",props:{activityMap:{type:Object,default:()=>({})},compact:{type:Boolean,default:!1}},setup(m){const r=m,{calendarData:d,monthLabel:f}=$t(r.activityMap);return(x,v)=>(t(),s("div",Mt,[v[0]||(v[0]=V('<div class="mb-3.5 flex items-start justify-between"><div><div class="font-display flex items-center gap-2 text-[15px] font-semibold text-[var(--ink)]"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> Study days </div><div class="mt-0.5 text-xs text-[var(--ink3)]">Đánh dấu ngày đã học</div></div><div class="flex items-center gap-1.5 text-xs text-[var(--ink3)]"><span class="inline-block h-2.5 w-2.5 rounded-sm bg-[var(--green-l)]"></span> Có nộp bài </div></div>',1)),e("div",St,i(j(f)),1),e("div",{class:M(["mb-1.5 grid grid-cols-7 gap-1",m.compact?"[&_.day-hdr]:text-[9px] [&_.day-hdr]:p-px":""])},[(t(),s(C,null,L(["T2","T3","T4","T5","T6","T7","CN"],a=>e("div",{key:a,class:"day-hdr py-0.5 text-center text-[10px] font-semibold text-[var(--ink3)]"},i(a),1)),64))],2),e("div",{class:M(["grid grid-cols-7 gap-1",m.compact?"gap-[3px] [&_.heatmap-day]:min-h-[22px] [&_.heatmap-day]:text-[10px]":""])},[(t(!0),s(C,null,L(j(d),(a,$)=>(t(),s("div",{key:$,class:M(["heatmap-day",{empty:a.empty,today:a.isToday,"done-1":!a.isToday&&a.level===1,"done-2":!a.isToday&&a.level===2,"done-3":!a.isToday&&a.level===3}]),title:a.day?`${a.day}: ${a.count??0} bài`:""},[a.empty?D("",!0):(t(),s("span",Lt,i(a.day),1))],10,Ct))),128))],2)]))}},jt={class:"grid grid-cols-1 gap-4 xl:grid-cols-[320px_1fr]"},Tt={class:"ct-card p-4"},zt={class:"mb-3 flex items-center justify-between"},Dt={class:"ct-badge",style:{background:"var(--green-bg)",color:"var(--green)"}},Ht={key:0,class:"mt-4 border-t border-[var(--border)] pt-3"},Pt={class:"grid grid-cols-7 gap-0.5"},Rt={class:"flex h-8 w-full items-end justify-center overflow-hidden rounded-sm",title:""},It={class:"text-[9px] text-[var(--ink3)]"},At={class:"ct-card p-4"},Nt={key:0,class:"flex flex-col items-center justify-center py-10 text-center"},Vt={key:1,class:"space-y-3"},qt={class:"mb-2 flex items-center justify-between"},Ot={class:"flex items-center gap-2"},Et=["innerHTML"],Wt={class:"text-[13px] font-semibold text-[var(--ink)]"},Kt={class:"flex items-center gap-2"},Ft={class:"text-[12px] font-bold text-[var(--ink2)]"},Ut={class:"h-2 overflow-hidden rounded-full bg-white"},Xt={class:"mt-1.5 flex justify-between text-[10px] text-[var(--ink3)]"},Yt={key:0},Jt={__name:"DashboardProgress",setup(m){const r=W(),d=U(),f=k(()=>r.streak||0),x=k(()=>{var n;return Number(((n=d.profile)==null?void 0:n.target_band)||7)});function v(n){return Math.min(100,Math.round(Number(n.percentage||0)))}function a(n){const g=parseInt(n.time)||0;return Math.min(28,Math.round(g/90*28))}function $(n){const g=Number(n);return g?g>=7?"text-[#059669]":g>=5?"text-[#d97706]":"text-[var(--rose)]":"text-[var(--ink3)]"}const T={Reading:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',Listening:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>',Writing:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',Speaking:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></svg>',Vocabulary:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'},B={Reading:"bg-[#2563eb]",Listening:"bg-[#7c3aed]",Writing:"bg-[#d97706]",Speaking:"bg-[#059669]",Vocabulary:"bg-[#0891b2]"};function _(n){const g=Object.keys(T).find(o=>o.toLowerCase()===(n||"").toLowerCase());return T[g]||T.Reading}function h(n){const g=Object.keys(B).find(o=>o.toLowerCase()===(n||"").toLowerCase());return B[g]||"bg-[var(--ink3)]"}return(n,g)=>(t(),s("div",jt,[e("div",Tt,[e("div",zt,[g[0]||(g[0]=e("div",{class:"text-sm font-bold text-[var(--ink)]"},"Activity",-1)),e("span",Dt,i(f.value)+" day streak",1)]),P(Bt,{"activity-map":j(r).activityMap,compact:""},null,8,["activity-map"]),j(r).weeklyStats.length?(t(),s("div",Ht,[g[1]||(g[1]=e("p",{class:"mb-2 text-[11px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},"This Week",-1)),e("div",Pt,[(t(!0),s(C,null,L(j(r).weeklyStats,o=>(t(),s("div",{key:o.date,class:"flex flex-col items-center gap-1"},[e("div",Rt,[e("div",{class:"w-full rounded-sm bg-[#34d399] transition-all",style:I({height:`${a(o)}px`,minHeight:o.time!=="0m"?"4px":"0"})},null,4)]),e("span",It,i(o.date),1)]))),128))])])):D("",!0)]),e("div",At,[g[3]||(g[3]=e("div",{class:"mb-4 text-sm font-bold text-[var(--ink)]"},"Progress by Skill",-1)),j(r).progress.length?(t(),s("div",Vt,[(t(!0),s(C,null,L(j(r).progress,o=>(t(),s("div",{key:o.id||o.subject,class:"rounded-xl border border-[var(--border)] bg-[var(--bg)] p-3.5 transition hover:border-[#34d399]/40 hover:bg-[#f0fdf4]"},[e("div",qt,[e("div",Ot,[e("div",{class:M(["flex h-7 w-7 items-center justify-center rounded-lg text-white",h(o.subject)])},[e("span",{innerHTML:_(o.subject)},null,8,Et)],2),e("span",Wt,i(o.subject),1)]),e("div",Kt,[o.band_score?(t(),s("span",{key:0,class:M(["text-[11px] font-bold",$(o.band_score)])}," Band "+i(o.band_score),3)):D("",!0),e("span",Ft,i(v(o))+"%",1)])]),e("div",Ut,[e("div",{class:"h-2 rounded-full bg-[#34d399] transition-all duration-700",style:I({width:`${v(o)}%`})},null,4)]),e("div",Xt,[e("span",null,i(o.completed_questions??0)+" / "+i(o.total_questions??0)+" questions",1),o.band_score?(t(),s("span",Yt,"Target: "+i(x.value),1)):D("",!0)])]))),128))])):(t(),s("div",Nt,[...g[2]||(g[2]=[V('<div class="mb-2 flex h-10 w-10 items-center justify-center rounded-full bg-[var(--bg2)] text-[var(--ink3)]"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h4l2-5 3 10 2-5h3"></path></svg></div><p class="text-[13px] text-[var(--ink2)]">No progress data yet</p><p class="mt-1 text-[11px] text-[var(--ink3)]">Luyện IELTS hoặc ôn từ vựng để theo dõi tiến độ</p>',3)])]))])]))}},Qt={class:"space-y-4"},Zt={class:"ct-card flex flex-wrap items-center justify-between gap-3 px-5 py-4"},Gt={class:"flex items-center gap-2"},es={key:0,class:"ct-badge",style:{background:"var(--green-bg)",color:"var(--green)"}},ts={class:"flex items-center gap-2"},ss=["disabled"],ns={key:0,class:"mr-1.5 inline h-3.5 w-3.5 animate-spin",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},rs={key:1,class:"mr-1.5 inline h-3.5 w-3.5",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},os=["disabled"],is={key:0,class:"mr-1.5 inline h-3.5 w-3.5 animate-spin",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},as={key:1,class:"mr-1.5 inline h-3.5 w-3.5",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},ls={key:0,class:"ct-card px-5 py-3"},ds={class:"mb-1.5 flex items-center justify-between text-[12px]"},cs={class:"font-bold text-[#059669]"},us={class:"h-2 overflow-hidden rounded-full bg-[var(--bg2)]"},vs={key:1,class:"ct-card flex flex-col items-center justify-center py-16 text-center"},hs={key:2,class:"space-y-3"},gs={key:3,class:"space-y-3"},ps={class:"flex items-center gap-2"},xs={class:"text-[13px] font-bold text-[var(--ink)]"},fs={key:0,class:"ml-2 text-[11px] text-[var(--ink3)]"},bs={class:"flex items-center gap-2"},ys={key:0,class:"flex h-5 w-5 items-center justify-center rounded-full bg-[#34d399]"},ks={class:"divide-y divide-[var(--border)]"},ms=["onClick"],ws={key:0,width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"white","stroke-width":"3.5"},_s={class:"min-w-0 flex-1"},$s={class:"mt-1 flex items-center gap-2.5 text-[11px] text-[var(--ink3)]"},Ms={class:"flex items-center gap-1"},Ss={class:"flex items-center gap-1 capitalize"},Cs={key:0,class:"text-[var(--rose)]"},Ls={__name:"DashboardStudyPlan",setup(m){const r=W(),d=N(!1),f=N(!1),x=k(()=>(r.studyPlanData.days||[]).length>0),v=k(()=>r.studyPlanData.total_tasks||0),a=k(()=>r.studyPlanData.completed_tasks||0);async function $(){d.value=!0,await r.generateStudyPlan(),d.value=!1}async function T(){f.value=!0,await r.extendStudyPlan(),f.value=!1}async function B(z){await r.completeStudyTask(z.id)}function _(z){return z.tasks.length>0&&z.tasks.every(p=>p.is_completed)}function h(z){var p;return((p=z.tasks[0])==null?void 0:p.focus_skill)||"reading"}function n(z){return z?new Date(z).toLocaleDateString("en-US",{weekday:"short",month:"short",day:"numeric"}):""}const g={reading:"bg-[#eff6ff] text-[#2563eb]",listening:"bg-[#f5f3ff] text-[#7c3aed]",writing:"bg-[#fff7ed] text-[#d97706]",speaking:"bg-[#f0fdf4] text-[#059669]",vocabulary:"bg-[#ecfeff] text-[#0891b2]"},o={reading:"bg-[#2563eb]",listening:"bg-[#7c3aed]",writing:"bg-[#d97706]",speaking:"bg-[#34d399]",vocabulary:"bg-[#0891b2]"};function H(z){return g[z]||"bg-[var(--bg2)] text-[var(--ink3)]"}function q(z){return o[z]||"bg-[var(--ink3)]"}return(z,p)=>{const S=Y("RouterLink");return t(),s("div",Qt,[e("div",Zt,[e("div",null,[e("div",Gt,[p[0]||(p[0]=e("div",{class:"text-base font-bold text-[var(--ink)]"},"Study Plan",-1)),v.value?(t(),s("span",es,i(a.value)+"/"+i(v.value)+" done ",1)):D("",!0)]),p[1]||(p[1]=e("p",{class:"mt-0.5 text-[12px] text-[var(--ink3)]"},"AI-generated personalised roadmap based on your history",-1))]),e("div",ts,[e("button",{class:"ct-btn px-3.5 py-2 text-[12px]",disabled:d.value,onClick:$},[d.value?(t(),s("svg",ns,[...p[2]||(p[2]=[e("path",{d:"M21 12a9 9 0 1 1-6.219-8.56"},null,-1)])])):(t(),s("svg",rs,[...p[3]||(p[3]=[e("path",{d:"M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"},null,-1)])])),R(" "+i(x.value?"Regenerate":"Generate Plan"),1)],8,ss),x.value?(t(),s("button",{key:0,class:"ct-btn px-3.5 py-2 text-[12px]",disabled:f.value,onClick:T},[f.value?(t(),s("svg",is,[...p[4]||(p[4]=[e("path",{d:"M21 12a9 9 0 1 1-6.219-8.56"},null,-1)])])):(t(),s("svg",as,[...p[5]||(p[5]=[e("path",{d:"M12 5v14M5 12h14"},null,-1)])])),p[6]||(p[6]=R(" Add 5 more days ",-1))],8,os)):D("",!0)])]),v.value?(t(),s("div",ls,[e("div",ds,[p[7]||(p[7]=e("span",{class:"font-medium text-[var(--ink2)]"},"Overall completion",-1)),e("span",cs,i(Math.round(a.value/v.value*100))+"%",1)]),e("div",us,[e("div",{class:"h-2 rounded-full bg-[#34d399] transition-all duration-700",style:I({width:`${a.value/v.value*100}%`})},null,4)])])):D("",!0),!x.value&&!d.value?(t(),s("div",vs,[...p[8]||(p[8]=[V('<div class="mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-[#f0fdf4] text-[#34d399]"><svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"></path></svg></div><p class="text-[15px] font-semibold text-[var(--ink)]">No plan yet</p><p class="mt-1.5 max-w-xs text-[13px] text-[var(--ink3)]">Click "Generate Plan" to create a personalised AI study plan based on your skill levels and history.</p>',3)])])):D("",!0),d.value?(t(),s("div",hs,[(t(),s(C,null,L(5,c=>e("div",{key:c,class:"ct-card animate-pulse p-4"},[...p[9]||(p[9]=[e("div",{class:"mb-2 h-4 w-24 rounded bg-[var(--bg2)]"},null,-1),e("div",{class:"h-3 w-full rounded bg-[var(--bg2)]"},null,-1),e("div",{class:"mt-1.5 h-3 w-3/4 rounded bg-[var(--bg2)]"},null,-1)])])),64))])):D("",!0),!d.value&&x.value?(t(),s("div",gs,[(t(!0),s(C,null,L(j(r).studyPlanData.days,c=>(t(),s("div",{key:c.day_number,class:"ct-card overflow-hidden"},[e("div",{class:M(["flex items-center justify-between border-b border-[var(--border)] px-4 py-3",_(c)?"bg-[#f0fdf4]":"bg-[var(--bg)]"])},[e("div",ps,[e("div",{class:M(["flex h-7 w-7 items-center justify-center rounded-full text-[12px] font-bold",_(c)?"bg-[#34d399] text-white":"bg-[var(--bg2)] text-[var(--ink2)]"])},i(c.day_number),3),e("div",null,[e("span",xs,"Day "+i(c.day_number),1),c.plan_date?(t(),s("span",fs,i(n(c.plan_date)),1)):D("",!0)])]),e("div",bs,[e("span",{class:M(["rounded-md px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide",H(h(c))])},i(h(c)),3),_(c)?(t(),s("div",ys,[...p[10]||(p[10]=[e("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"white","stroke-width":"3"},[e("path",{d:"M20 6L9 17l-5-5"})],-1)])])):D("",!0)])],2),e("div",ks,[(t(!0),s(C,null,L(c.tasks,l=>(t(),s("div",{key:l.id,class:M(["group flex items-start gap-3 px-4 py-3 transition",l.is_completed?"bg-[#fafffe]":"hover:bg-[#f8fffe]"])},[e("button",{class:M(["mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded border-2 transition",l.is_completed?"border-[#34d399] bg-[#34d399]":"border-[var(--border2)] hover:border-[#34d399]"]),onClick:w=>B(l)},[l.is_completed?(t(),s("svg",ws,[...p[11]||(p[11]=[e("path",{d:"M20 6L9 17l-5-5"},null,-1)])])):D("",!0)],10,ms),e("div",_s,[e("div",{class:M(["text-[13px] font-medium leading-snug text-[var(--ink)] transition",l.is_completed?"line-through decoration-[var(--rose)] decoration-2 opacity-60":""])},i(l.task_description),3),e("div",$s,[e("span",Ms,[p[12]||(p[12]=e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("circle",{cx:"12",cy:"12",r:"10"}),e("polyline",{points:"12 6 12 12 16 14"})],-1)),R(" "+i(l.duration_minutes)+" min ",1)]),e("span",Ss,[e("span",{class:M(["inline-block h-1.5 w-1.5 rounded-full",q(l.focus_skill)])},null,2),R(" "+i(l.focus_skill),1)]),l.is_completed?(t(),s("span",Cs,"✓ Completed")):D("",!0)])]),l.route_path&&!l.is_completed?(t(),A(S,{key:0,to:l.route_path,class:"ml-2 shrink-0 rounded-lg border border-[var(--border2)] bg-white px-2.5 py-1 text-[11px] font-medium text-[var(--ink2)] opacity-0 transition group-hover:opacity-100 hover:border-[#34d399] hover:text-[#059669]"},{default:F(()=>[...p[13]||(p[13]=[R(" Go → ",-1)])]),_:1},8,["to"])):D("",!0)],2))),128))])]))),128))])):D("",!0)])}}},Bs={class:"space-y-4"},js={class:"ct-card flex flex-wrap items-center justify-between gap-3 px-5 py-3.5"},Ts={class:"text-[12px] text-[var(--ink3)]"},zs={key:0,class:"ml-1 font-medium text-[var(--ink2)]"},Ds={class:"flex items-center gap-3"},Hs={class:"flex items-center gap-1.5 rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-1.5 text-[12px]"},Ps={class:"text-[var(--ink)]"},Rs={class:"flex items-center gap-1.5 rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-1.5 text-[12px]"},Is={class:"text-[var(--ink)]"},As={class:"inline-flex rounded-xl border border-[var(--border)] bg-white p-1 shadow-sm"},Ns=["onClick"],Vs=["innerHTML"],Ws={__name:"Dashboard",setup(m){const r=U(),d=W(),f=oe(),x=ie(),v=new Set(["home","reports","progress","study"]),a=k({get(){const h=f.query.tab;return v.has(h)?h:"home"},set(h){x.replace({query:{...f.query,tab:h}})}}),$={home:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/><polyline points="9 21 9 12 15 12 15 21"/></svg>',reports:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19h16M7 16V8M12 16V5M17 16v-3"/></svg>',progress:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h4l2-5 3 10 2-5h3"/></svg>',study:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>'},T=[{id:"home",label:"Home",icon:$.home},{id:"reports",label:"Reports",icon:$.reports},{id:"progress",label:"Progress",icon:$.progress},{id:"study",label:"Study Plan",icon:$.study}],B=k(()=>{var n;const h=(n=r.profile)==null?void 0:n.exam_date;return h?new Date(h).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}):"No exam date set"});async function _(h=!0){var g;r.profile||await r.fetchProfile(),d.targetScores.overall=Number(((g=r.profile)==null?void 0:g.target_band)||7),d.targetScores.reading=d.targetScores.overall,d.targetScores.listening=d.targetScores.overall,d.targetScores.writing=d.targetScores.overall,d.targetScores.speaking=d.targetScores.overall;const n=[d.fetchStats(),d.fetchHistory(),d.fetchProgress(),r.fetchProfile()];h&&n.push(d.fetchPracticeAnalytics(),d.fetchStudyPlan(),d.fetchSkillRadar()),await Promise.all(n)}return X(()=>_(!0)),ne(()=>_(!1)),(h,n)=>{var g;return t(),s("div",Bs,[e("div",js,[e("div",null,[n[2]||(n[2]=e("div",{class:"text-base font-bold text-[var(--ink)]"},"IELTS Academic",-1)),e("div",Ts,[R(i(B.value)+" ",1),j(d).daysToExam!==null?(t(),s("span",zs,"("+i(j(d).daysToExam)+" days left)",1)):D("",!0),n[0]||(n[0]=e("span",{class:"mx-1"},"·",-1)),n[1]||(n[1]=e("span",null,"Ôn từ vựng: 10 phút = 1 XP",-1))])]),e("div",Ds,[e("div",Hs,[n[3]||(n[3]=e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"#34d399","stroke-width":"2"},[e("path",{d:"M13 2L3 14h9l-1 8 10-12h-9l1-8z"})],-1)),n[4]||(n[4]=e("span",{class:"text-[var(--ink3)]"},"Streak",-1)),e("strong",Ps,i(j(d).streak),1)]),e("div",Rs,[n[5]||(n[5]=e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"#34d399","stroke-width":"2"},[e("path",{d:"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"})],-1)),n[6]||(n[6]=e("span",{class:"text-[var(--ink3)]"},"Target",-1)),e("strong",Is,i(((g=j(r).profile)==null?void 0:g.target_band)??"—"),1)])])]),e("div",As,[(t(),s(C,null,L(T,o=>e("button",{key:o.id,class:M(["flex items-center gap-1.5 rounded-lg px-4 py-2 text-[13px] font-medium transition-all",a.value===o.id?"bg-[#111] text-white shadow-sm":"text-[var(--ink3)] hover:bg-[var(--bg2)] hover:text-[var(--ink)]"]),onClick:H=>a.value=o.id},[e("span",{innerHTML:o.icon,class:"shrink-0"},null,8,Vs),R(" "+i(o.label),1)],10,Ns)),64))]),(t(),A(re,null,[a.value==="home"?(t(),A(Ie,{key:"home"})):a.value==="reports"?(t(),A(_t,{key:"reports"})):a.value==="progress"?(t(),A(Jt,{key:"progress"})):(t(),A(Ls,{key:"study"}))],1024))])}}};export{Ws as default};
|
fronted/dist/assets/History-B_R9pOYA.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{h as N,m as A}from"./historyService-y1ubbIgp.js";import{o as r,c as s,a as t,n as L,t as l,N as D,z as w,y as $,k as q,h as C,i as y,g,p as R,q as E,F as z,r as B,x as m,e as F,f as H,j as v}from"./index-CId0hMaT.js";import{_ as X}from"./Paginator-C1T4CVWL.js";const V=[{id:"reading",name:"Reading",nameVi:"Đọc",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',color:"blue",colorHex:"#4895ef",colorDark:"#1a4e8f",colorBg:"#dbeafe",route:"/reading",totalLabel:"48 bài",pendingLabel:"12 chưa làm",progress:75},{id:"listening",name:"Listening",nameVi:"Nghe",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>',color:"green",colorHex:"#52b788",colorDark:"#2d6a4f",colorBg:"#d8f3dc",route:"/listening",totalLabel:"36 bài",pendingLabel:"20 chưa làm",progress:44},{id:"writing",name:"Writing",nameVi:"Viết",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',color:"amber",colorHex:"#f4845f",colorDark:"#b5450b",colorBg:"#fde8dc",route:"/writing",totalLabel:"24 đề",pendingLabel:"15 chưa làm",progress:38},{id:"speaking",name:"Speaking",nameVi:"Nói",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',color:"rose",colorHex:"#e05780",colorDark:"#9b1d3a",colorBg:"#fce4ec",route:"/speaking",totalLabel:"30 topic",pendingLabel:"18 chưa làm",progress:40},{id:"vocabulary",name:"Từ vựng",nameVi:"Từ vựng",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',color:"violet",colorHex:"#9b72cf",colorDark:"#5c35a8",colorBg:"#ede9ff",route:"/vocabulary",totalLabel:"320 từ",pendingLabel:"45 cần ôn",progress:62}];function P(i){return V.find(n=>n.id===i)??V[0]}const U=["innerHTML"],G={class:"min-w-0 flex-1"},K={class:"truncate text-[13.5px] font-semibold text-[var(--ink)]"},W={class:"mt-0.5 text-xs text-[var(--ink3)]"},Q={__name:"HistoryItem",props:{skillId:{type:String,required:!0},title:{type:String,required:!0},date:{type:String,required:!0},duration:{type:String,default:""},score:{type:[Number,String],default:"—"},mode:{type:String,default:"practice"}},emits:["click"],setup(i){const n=i,a=w(()=>P(n.skillId)),c=w(()=>({practice:"Luyện tập",exam:"Thi thật",flashcard:"Flashcard"})[n.mode]??n.mode);return(d,u)=>(r(),s("div",{class:"flex cursor-pointer items-center gap-3.5 border-b border-[var(--border)] px-[18px] py-3.5 transition-colors last:border-b-0 hover:bg-[var(--bg)]",onClick:u[0]||(u[0]=k=>d.$emit("click"))},[t("div",{class:"flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px]",style:L({background:a.value.colorBg})},[t("span",{innerHTML:a.value.icon,style:L({color:a.value.colorHex})},null,12,U)],4),t("div",G,[t("div",K,l(i.title),1),t("div",W,l(i.date)+" · "+l(i.duration)+" · "+l(c.value),1)]),t("div",{class:"shrink-0 font-display text-lg font-bold",style:L({color:a.value.colorHex})},l(i.score),5),D(d.$slots,"actions")]))}},Z=["value","placeholder","id"],J={__name:"SearchInput",props:{modelValue:{type:String,default:""},placeholder:{type:String,default:"Tìm kiếm..."},id:{type:String,default:void 0},wrapperClass:{type:String,default:""}},emits:["update:modelValue"],setup(i){return(n,a)=>(r(),s("div",{class:$(["relative",i.wrapperClass])},[a[1]||(a[1]=t("span",{class:"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[var(--ink3)]"},[t("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[t("circle",{cx:"11",cy:"11",r:"8"}),t("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})])],-1)),t("input",{value:i.modelValue,placeholder:i.placeholder,id:i.id,class:"w-full rounded-[var(--r-sm)] border border-[var(--border2)] bg-[var(--surface)] px-3 py-2 pl-8 text-[13px] text-[var(--ink)] outline-none transition-colors focus:border-[var(--green-l)]",onInput:a[0]||(a[0]=c=>n.$emit("update:modelValue",c.target.value))},null,40,Z)],2))}},O={class:"rounded-[var(--r)] border border-[var(--border)] bg-[var(--surface)] px-5 py-14 text-center"},Y={class:"mb-4 text-5xl"},ee={class:"font-display mb-2 text-xl font-semibold text-[var(--ink)]"},te={class:"mb-5 text-[13px] text-[var(--ink3)]"},oe={__name:"EmptyState",props:{icon:{type:String,default:"📋"},title:{type:String,required:!0},description:{type:String,required:!0},actionLabel:{type:String,required:!0},actionTo:{type:String,required:!0}},setup(i){return(n,a)=>{const c=q("router-link");return r(),s("div",O,[t("div",Y,l(i.icon),1),t("div",ee,l(i.title),1),t("div",te,l(i.description),1),C(c,{to:i.actionTo,class:"inline-flex items-center rounded-[var(--r-sm)] bg-[var(--green)] px-5 py-2.5 text-[13px] font-semibold text-white transition-colors hover:bg-[#245c42]"},{default:y(()=>[g(l(i.actionLabel),1)]),_:1},8,["to"])])}}},ie={class:"mb-5 flex flex-wrap items-center gap-3"},re={class:"flex flex-wrap gap-1.5"},ae=["onClick"],ne=["innerHTML"],le={class:"ml-auto w-full max-w-[220px]"},se={key:0,class:"py-12 text-center text-[13px] text-[var(--ink3)]"},de={key:1,class:"rounded-xl border border-[var(--rose-l)] bg-[var(--rose-bg)] p-4 text-[13px] text-[var(--rose)]"},ce={key:0,class:"overflow-hidden rounded-[var(--r)] border border-[var(--border)] bg-[var(--surface)] shadow-[var(--shadow-sm)]"},ue={key:2,class:"history-action-btn"},he={key:2,class:"mt-5 flex flex-wrap items-center justify-between gap-3"},ve={class:"text-[12px] text-[var(--ink3)]"},ge=15,me={__name:"History",setup(i){const n=v(""),a=v("all"),c=v([]),d=v(0),u=v(1),k=ge,_=v(!1),b=v(""),x={all:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>',reading:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',listening:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>',writing:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',speaking:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'},T=[{id:"all",label:"Tất cả",icon:x.all},{id:"reading",label:"Reading",icon:x.reading},{id:"listening",label:"Listening",icon:x.listening},{id:"writing",label:"Writing",icon:x.writing},{id:"speaking",label:"Speaking",icon:x.speaking}],M=w(()=>c.value.filter(h=>!n.value||h.title.toLowerCase().includes(n.value.toLowerCase()))),I=w(()=>{if(!d.value)return"0";const h=(u.value-1)*k+1,o=Math.min(u.value*k,d.value);return`${h}–${o}`});async function f(h=1){var o,p;_.value=!0,b.value="",u.value=h;try{const e={page:h,page_size:k};a.value!=="all"&&(e.subject=a.value.charAt(0).toUpperCase()+a.value.slice(1));const S=await N.list(e);c.value=(S.items||[]).map(A),d.value=S.total??0}catch(e){b.value=((p=(o=e.response)==null?void 0:o.data)==null?void 0:p.detail)||"Không thể tải lịch sử",c.value=[],d.value=0}finally{_.value=!1}}function j(h){a.value=h,f(1)}return R(n,()=>{}),E(()=>f(1)),(h,o)=>{const p=q("RouterLink");return r(),s("div",null,[t("div",ie,[t("div",re,[(r(),s(z,null,B(T,e=>t("button",{key:e.id,class:$(["history-filter-btn",{active:a.value===e.id}]),onClick:S=>j(e.id)},[t("span",{innerHTML:e.icon,class:"filter-icon"},null,8,ne),g(" "+l(e.label),1)],10,ae)),64))]),t("div",le,[C(J,{modelValue:n.value,"onUpdate:modelValue":o[0]||(o[0]=e=>n.value=e),placeholder:"Tìm kiếm..."},null,8,["modelValue"])])]),_.value?(r(),s("div",se,"Đang tải lịch sử...")):b.value?(r(),s("div",de,[g(l(b.value)+" ",1),t("button",{class:"link-btn ml-2",onClick:o[1]||(o[1]=e=>f(u.value))},"Thử lại")])):(r(),s(z,{key:2},[M.value.length?(r(),s("div",ce,[(r(!0),s(z,null,B(M.value,e=>(r(),m(Q,{key:e.id,"skill-id":e.skill,title:e.title,date:e.date,duration:e.duration,score:e.score,mode:e.mode},{actions:y(()=>[e.session_id?(r(),m(p,{key:0,to:{name:"ReviewAnswer",params:{sessionId:e.session_id}},class:"history-action-btn history-action-btn--primary"},{default:y(()=>[...o[3]||(o[3]=[t("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"}),t("circle",{cx:"12",cy:"12",r:"3"})],-1),g(" Xem lời giải ",-1)])]),_:1},8,["to"])):e.quiz_id&&(e.skill==="reading"||e.skill==="listening")?(r(),m(p,{key:1,to:{name:"ReviewAnswerByQuiz",params:{quizId:e.quiz_id}},class:"history-action-btn history-action-btn--primary"},{default:y(()=>[...o[4]||(o[4]=[t("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"}),t("circle",{cx:"12",cy:"12",r:"3"})],-1),g(" Xem lời giải ",-1)])]),_:1},8,["to"])):(r(),s("button",ue,[...o[5]||(o[5]=[t("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("circle",{cx:"12",cy:"12",r:"10"}),t("polyline",{points:"12 6 12 12 16 14"})],-1),g(" Xem lại ",-1)])])),e.skill==="speaking"&&e.quiz_id?(r(),m(p,{key:3,to:{path:"/speaking/result",state:{fetchSummary:!0,quiz_id:e.quiz_id,question:e.title}},class:"history-action-btn history-action-btn--primary"},{default:y(()=>[...o[6]||(o[6]=[t("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("path",{d:"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"}),t("path",{d:"M19 10v2a7 7 0 0 1-14 0v-2"})],-1),g(" Xem kết quả ",-1)])]),_:1},8,["to"])):H("",!0)]),_:2},1032,["skill-id","title","date","duration","score","mode"]))),128))])):(r(),m(oe,{key:1,title:"Chưa có lịch sử",description:"Hãy bắt đầu luyện tập để theo dõi tiến độ của bạn!","action-label":"Bắt đầu ngay","action-to":"/dashboard"})),d.value>0?(r(),s("div",he,[t("p",ve," Hiển thị "+l(I.value)+" / "+l(d.value)+" bài ",1),C(X,{modelValue:u.value,"onUpdate:modelValue":[o[2]||(o[2]=e=>u.value=e),f],total:d.value,"page-size":F(k)},null,8,["modelValue","total","page-size"])])):H("",!0)],64))])}}};export{me as default};
|
fronted/dist/assets/History-CCzhCIbk.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{u as B}from"./ielts-lE7MGpiZ.js";import{_ as M,o as r,c,a as e,n as m,t as s,E as C,z as f,y as S,k as L,h as H,i as g,g as u,q as z,F as w,r as b,x as v,j as x,f as V}from"./index-BnN_E-ke.js";const _=[{id:"reading",name:"Reading",nameVi:"Đọc",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',color:"blue",colorHex:"#4895ef",colorDark:"#1a4e8f",colorBg:"#dbeafe",route:"/reading",totalLabel:"48 bài",pendingLabel:"12 chưa làm",progress:75},{id:"listening",name:"Listening",nameVi:"Nghe",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>',color:"green",colorHex:"#52b788",colorDark:"#2d6a4f",colorBg:"#d8f3dc",route:"/listening",totalLabel:"36 bài",pendingLabel:"20 chưa làm",progress:44},{id:"writing",name:"Writing",nameVi:"Viết",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',color:"amber",colorHex:"#f4845f",colorDark:"#b5450b",colorBg:"#fde8dc",route:"/writing",totalLabel:"24 đề",pendingLabel:"15 chưa làm",progress:38},{id:"speaking",name:"Speaking",nameVi:"Nói",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',color:"rose",colorHex:"#e05780",colorDark:"#9b1d3a",colorBg:"#fce4ec",route:"/speaking",totalLabel:"30 topic",pendingLabel:"18 chưa làm",progress:40},{id:"vocabulary",name:"Từ vựng",nameVi:"Từ vựng",icon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',color:"violet",colorHex:"#9b72cf",colorDark:"#5c35a8",colorBg:"#ede9ff",route:"/vocabulary",totalLabel:"320 từ",pendingLabel:"45 cần ôn",progress:62}];function $(o){return _.find(a=>a.id===o)??_[0]}const q=["innerHTML"],T={class:"hist-info"},I={class:"hist-title"},A={class:"hist-meta"},j={__name:"HistoryItem",props:{skillId:{type:String,required:!0},title:{type:String,required:!0},date:{type:String,required:!0},duration:{type:String,default:""},score:{type:[Number,String],default:"—"},mode:{type:String,default:"practice"}},emits:["click"],setup(o){const a=o,i=f(()=>$(a.skillId)),l=f(()=>({practice:"Luyện tập",exam:"Thi thật",flashcard:"Flashcard"})[a.mode]??a.mode);return(d,p)=>(r(),c("div",{class:"history-item",onClick:p[0]||(p[0]=k=>d.$emit("click"))},[e("div",{class:"hist-icon",style:m({background:i.value.colorBg})},[e("span",{innerHTML:i.value.icon,style:m({color:i.value.colorHex})},null,12,q)],4),e("div",T,[e("div",I,s(o.title),1),e("div",A,s(o.date)+" · "+s(o.duration)+" · "+s(l.value),1)]),e("div",{class:"hist-score",style:m({color:i.value.colorHex})},s(o.score),5),C(d.$slots,"actions",{},void 0,!0)]))}},N=M(j,[["__scopeId","data-v-261b94f3"]]),D=["value","placeholder","id"],R={__name:"SearchInput",props:{modelValue:{type:String,default:""},placeholder:{type:String,default:"Tìm kiếm..."},id:{type:String,default:void 0},wrapperClass:{type:String,default:""}},emits:["update:modelValue"],setup(o){return(a,i)=>(r(),c("div",{class:S(["relative",o.wrapperClass])},[i[1]||(i[1]=e("span",{class:"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[var(--ink3)]"},[e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("circle",{cx:"11",cy:"11",r:"8"}),e("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})])],-1)),e("input",{value:o.modelValue,placeholder:o.placeholder,id:o.id,class:"w-full rounded-[var(--r-sm)] border border-[var(--border2)] bg-[var(--surface)] px-3 py-2 pl-8 text-[13px] text-[var(--ink)] outline-none transition-colors focus:border-[var(--green-l)]",onInput:i[0]||(i[0]=l=>a.$emit("update:modelValue",l.target.value))},null,40,D)],2))}},F={class:"rounded-[var(--r)] border border-[var(--border)] bg-[var(--surface)] px-5 py-14 text-center"},X={class:"mb-4 text-5xl"},E={class:"font-display mb-2 text-xl font-semibold text-[var(--ink)]"},W={class:"mb-5 text-[13px] text-[var(--ink3)]"},G={__name:"EmptyState",props:{icon:{type:String,default:"📋"},title:{type:String,required:!0},description:{type:String,required:!0},actionLabel:{type:String,required:!0},actionTo:{type:String,required:!0}},setup(o){return(a,i)=>{const l=L("router-link");return r(),c("div",F,[e("div",X,s(o.icon),1),e("div",E,s(o.title),1),e("div",W,s(o.description),1),H(l,{to:o.actionTo,class:"inline-flex items-center rounded-[var(--r-sm)] bg-[var(--green)] px-5 py-2.5 text-[13px] font-semibold text-white transition-colors hover:bg-[#245c42]"},{default:g(()=>[u(s(o.actionLabel),1)]),_:1},8,["to"])])}}},K={class:"mb-5 flex flex-wrap items-center gap-3"},Q={class:"flex flex-wrap gap-1.5"},U=["onClick"],J=["innerHTML"],O={class:"ml-auto w-full max-w-[220px]"},P={key:0,class:"overflow-hidden rounded-[var(--r)] border border-[var(--border)] bg-[var(--surface)] shadow-[var(--shadow-sm)]"},Y={key:2,class:"history-action-btn"},Z={__name:"History",setup(o){const a=B(),i=x(""),l=x("all"),d={all:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>',reading:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',listening:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>',writing:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',speaking:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>',vocabulary:'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'},p=[{id:"all",label:"Tất cả",icon:d.all},{id:"reading",label:"Reading",icon:d.reading},{id:"listening",label:"Listening",icon:d.listening},{id:"writing",label:"Writing",icon:d.writing},{id:"speaking",label:"Speaking",icon:d.speaking},{id:"vocabulary",label:"Từ vựng",icon:d.vocabulary}],k=f(()=>a.history.filter(y=>{const n=l.value==="all"||y.skill===l.value,h=!i.value||y.title.toLowerCase().includes(i.value.toLowerCase());return n&&h}));return z(()=>a.fetchHistory()),(y,n)=>{const h=L("RouterLink");return r(),c("div",null,[e("div",K,[e("div",Q,[(r(),c(w,null,b(p,t=>e("button",{key:t.id,class:S(["history-filter-btn",{active:l.value===t.id}]),onClick:ee=>l.value=t.id},[e("span",{innerHTML:t.icon,class:"filter-icon"},null,8,J),u(" "+s(t.label),1)],10,U)),64))]),e("div",O,[H(R,{modelValue:i.value,"onUpdate:modelValue":n[0]||(n[0]=t=>i.value=t),placeholder:"Tìm kiếm..."},null,8,["modelValue"])])]),k.value.length?(r(),c("div",P,[(r(!0),c(w,null,b(k.value,t=>(r(),v(N,{key:t.id,"skill-id":t.skill,title:t.title,date:t.date,duration:t.duration,score:t.score,mode:t.mode},{actions:g(()=>[t.session_id?(r(),v(h,{key:0,to:{name:"ReviewAnswer",params:{sessionId:t.session_id}},class:"history-action-btn history-action-btn--primary"},{default:g(()=>[...n[1]||(n[1]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"}),e("circle",{cx:"12",cy:"12",r:"3"})],-1),u(" Xem lời giải ",-1)])]),_:1},8,["to"])):t.quiz_id&&(t.skill==="reading"||t.skill==="listening")?(r(),v(h,{key:1,to:{name:"ReviewAnswerByQuiz",params:{quizId:t.quiz_id}},class:"history-action-btn history-action-btn--primary"},{default:g(()=>[...n[2]||(n[2]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"}),e("circle",{cx:"12",cy:"12",r:"3"})],-1),u(" Xem lời giải ",-1)])]),_:1},8,["to"])):(r(),c("button",Y,[...n[3]||(n[3]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("circle",{cx:"12",cy:"12",r:"10"}),e("polyline",{points:"12 6 12 12 16 14"})],-1),u(" Xem lại ",-1)])])),t.skill==="speaking"&&t.quiz_id?(r(),v(h,{key:3,to:{path:"/speaking/result",state:{fetchSummary:!0,quiz_id:t.quiz_id,question:t.title}},class:"history-action-btn history-action-btn--primary"},{default:g(()=>[...n[4]||(n[4]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"}),e("path",{d:"M19 10v2a7 7 0 0 1-14 0v-2"})],-1),u(" Xem kết quả ",-1)])]),_:1},8,["to"])):V("",!0)]),_:2},1032,["skill-id","title","date","duration","score","mode"]))),128))])):(r(),v(G,{key:1,title:"Chưa có lịch sử",description:"Hãy bắt đầu luyện tập để theo dõi tiến độ của bạn!","action-label":"Bắt đầu ngay","action-to":"/dashboard"}))])}}},ie=M(Z,[["__scopeId","data-v-dfe07a91"]]);export{ie as default};
|
|
|
|
|
|
fronted/dist/assets/History-PkzicY-T.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
.history-item[data-v-261b94f3]{display:flex;align-items:center;gap:14px;padding:14px 18px;border-bottom:1px solid var(--border);transition:background .15s;cursor:pointer}.history-item[data-v-261b94f3]:last-child{border-bottom:none}.history-item[data-v-261b94f3]:hover{background:var(--bg)}.hist-icon[data-v-261b94f3]{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.hist-info[data-v-261b94f3]{flex:1;min-width:0}.hist-title[data-v-261b94f3]{font-size:13.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ink)}.hist-meta[data-v-261b94f3]{font-size:12px;color:var(--ink3);margin-top:2px}.hist-score[data-v-261b94f3]{font-family:var(--font-display);font-size:18px;font-weight:700;flex-shrink:0}.history-filter-btn[data-v-dfe07a91]{display:inline-flex;align-items:center;gap:5px;padding:5px 12px;border-radius:20px;border:1.5px solid var(--border2);background:var(--surface);color:var(--ink2);font-size:12px;font-weight:500;cursor:pointer;transition:all .15s}.history-filter-btn[data-v-dfe07a91]:hover{border-color:#34d399;color:#15803d}.history-filter-btn.active[data-v-dfe07a91]{border-color:#34d399;background:#f0fdf4;color:#15803d;font-weight:600}.filter-icon[data-v-dfe07a91]{display:flex;align-items:center}.history-action-btn[data-v-dfe07a91]{display:inline-flex;align-items:center;gap:5px;margin-left:8px;white-space:nowrap;border-radius:var(--r-sm);border:1px solid var(--border2);background:transparent;padding:6px 12px;font-size:12px;font-weight:600;color:var(--ink2);cursor:pointer;transition:all .15s;text-decoration:none}.history-action-btn[data-v-dfe07a91]:hover{background:var(--bg2)}.history-action-btn--primary[data-v-dfe07a91]{border-color:#34d399;background:#f0fdf4;color:#15803d}.history-action-btn--primary[data-v-dfe07a91]:hover{background:#dcfce7}
|
|
|
|
|
|
fronted/dist/assets/Leaderboard-BRWxu-5I.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
.lb-page[data-v-b9997b76]{max-width:700px}.podium[data-v-b9997b76]{display:flex;align-items:flex-end;justify-content:center;gap:12px;padding:0 16px}.podium-item[data-v-b9997b76]{display:flex;flex-direction:column;align-items:center;gap:6px;flex:1;max-width:160px}.podium-crown[data-v-b9997b76]{margin-bottom:-4px}.podium-avatar-wrap[data-v-b9997b76]{position:relative}.podium-avatar[data-v-b9997b76]{width:56px;height:56px;border-radius:50%;-o-object-fit:cover;object-fit:cover;border:2px solid var(--border)}.podium-avatar--large[data-v-b9997b76]{width:68px;height:68px}.podium-rank[data-v-b9997b76]{position:absolute;bottom:-4px;right:-4px;width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:#fff;border:1.5px solid white}.rank-1[data-v-b9997b76]{background:#f59e0b}.rank-2[data-v-b9997b76]{background:#9ca3af}.rank-3[data-v-b9997b76]{background:#b45309}.podium-name[data-v-b9997b76]{font-size:12px;font-weight:500;color:var(--ink);text-align:center;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.podium-xp[data-v-b9997b76]{font-size:11px;font-weight:600;color:var(--ink3)}.xp-gold[data-v-b9997b76]{color:#d97706}.podium-bar[data-v-b9997b76]{width:100%;border-radius:8px 8px 0 0}.bar-1[data-v-b9997b76]{height:64px;background:linear-gradient(180deg,#fde68a,#f59e0b)}.bar-2[data-v-b9997b76]{height:48px;background:linear-gradient(180deg,#e5e7eb,#9ca3af)}.bar-3[data-v-b9997b76]{height:36px;background:linear-gradient(180deg,#fde8c8,#b45309)}.lb-row[data-v-b9997b76]{display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--border);padding:12px 20px;transition:background .15s}.lb-row[data-v-b9997b76]:last-child{border-bottom:none}.lb-row[data-v-b9997b76]:hover{background:var(--bg)}.lb-row--me[data-v-b9997b76]{background:#f0fdf4}.lb-rank[data-v-b9997b76]{width:32px;text-align:right}.lb-rank-num[data-v-b9997b76]{font-size:14px;font-weight:700;color:var(--ink3)}.lb-avatar[data-v-b9997b76]{width:36px;height:36px;border-radius:50%;-o-object-fit:cover;object-fit:cover;flex-shrink:0}.lb-info[data-v-b9997b76]{flex:1;min-width:0}.lb-name[data-v-b9997b76]{font-size:13px;font-weight:600;color:var(--ink);display:flex;align-items:center;gap:6px}.lb-streak[data-v-b9997b76]{font-size:11px;color:var(--ink3);display:flex;align-items:center;gap:3px;margin-top:2px}.lb-you-badge[data-v-b9997b76]{font-size:10px;font-weight:700;background:#d1fae5;color:#065f46;padding:1px 6px;border-radius:20px}.lb-xp-col[data-v-b9997b76]{display:flex;flex-direction:column;align-items:flex-end;gap:4px;min-width:100px}.lb-xp-bar-wrap[data-v-b9997b76]{width:80px;height:4px;background:var(--bg2);border-radius:2px;overflow:hidden}.lb-xp-bar[data-v-b9997b76]{height:100%;background:#34d399;border-radius:2px;transition:width .8s ease}.lb-xp-num[data-v-b9997b76]{font-size:12px;font-weight:700;color:var(--ink2)}
|
|
|
|
|
|
fronted/dist/assets/Leaderboard-Cklwb0qQ.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{G as w,_ as y,q as L,c as i,a,t as l,f as v,F as _,r as b,j as u,z as g,o as d,b as S,y as h,g as x,n as N}from"./index-BnN_E-ke.js";class V{async getLeaderboard(o=50){const{data:r}=await w.get("/leaderboard",{params:{limit:o}});return r}}const B=new V,j={class:"lb-page"},z={key:0,class:"podium mb-6"},P={class:"podium-item podium-2"},X={class:"podium-avatar-wrap"},C=["src","alt"],M={class:"podium-name"},T={class:"podium-xp"},F={class:"podium-item podium-1"},q={class:"podium-avatar-wrap"},D=["src","alt"],E={class:"podium-name font-bold"},G={class:"podium-xp xp-gold"},I={class:"podium-item podium-3"},K={class:"podium-avatar-wrap"},$=["src","alt"],A={class:"podium-name"},H={class:"podium-xp"},J={key:1,class:"ct-card overflow-hidden"},O={key:2,class:"ct-card overflow-hidden"},Q={class:"lb-rank"},R=["src","alt"],U={class:"lb-info"},W={class:"lb-name"},Y={key:0,class:"lb-you-badge"},Z={class:"lb-streak"},aa={class:"lb-xp-col"},sa={class:"lb-xp-bar-wrap"},ea={class:"lb-xp-num"},ta={key:0,class:"py-12 text-center text-[13px] text-[var(--ink3)]"},la={key:3,class:"mt-4 rounded-xl border border-[var(--rose-l)] bg-[var(--rose-bg)] p-4 text-[13px] text-[var(--rose)]"},ia={__name:"Leaderboard",setup(k){const o=u(!1),r=u(""),n=u([]),t=g(()=>n.value.slice(0,3)),p=g(()=>n.value.slice(3));async function f(){var c,s;o.value=!0,r.value="";try{const e=await B.getLeaderboard(50);n.value=e.items||[]}catch(e){r.value=((s=(c=e.response)==null?void 0:c.data)==null?void 0:s.detail)||"Không thể tải bảng xếp hạng"}finally{o.value=!1}}return L(f),(c,s)=>(d(),i("div",j,[s[9]||(s[9]=a("div",{class:"mb-6"},[a("h1",{class:"text-xl font-bold text-[var(--ink)]"},"Bảng xếp hạng"),a("p",{class:"mt-0.5 text-[13px] text-[var(--ink3)]"},"Top học viên theo điểm XP tích lũy")],-1)),!o.value&&t.value.length>=3?(d(),i("div",z,[a("div",P,[a("div",X,[a("img",{src:t.value[1].avatar_url||"/icon_profile.jpg",alt:t.value[1].display_name,class:"podium-avatar"},null,8,C),s[0]||(s[0]=a("div",{class:"podium-rank rank-2"},"2",-1))]),a("div",M,l(t.value[1].display_name),1),a("div",T,l(t.value[1].xp.toLocaleString("vi-VN"))+" XP",1),s[1]||(s[1]=a("div",{class:"podium-bar bar-2"},null,-1))]),a("div",F,[s[3]||(s[3]=a("div",{class:"podium-crown"},[a("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"#f59e0b",stroke:"#f59e0b","stroke-width":"1.5"},[a("path",{d:"M3 17h18l-3-9-4.5 5L12 7l-1.5 6L6 8l-3 9z"})])],-1)),a("div",q,[a("img",{src:t.value[0].avatar_url||"/icon_profile.jpg",alt:t.value[0].display_name,class:"podium-avatar podium-avatar--large"},null,8,D),s[2]||(s[2]=a("div",{class:"podium-rank rank-1"},"1",-1))]),a("div",E,l(t.value[0].display_name),1),a("div",G,l(t.value[0].xp.toLocaleString("vi-VN"))+" XP",1),s[4]||(s[4]=a("div",{class:"podium-bar bar-1"},null,-1))]),a("div",I,[a("div",K,[a("img",{src:t.value[2].avatar_url||"/icon_profile.jpg",alt:t.value[2].display_name,class:"podium-avatar"},null,8,$),s[5]||(s[5]=a("div",{class:"podium-rank rank-3"},"3",-1))]),a("div",A,l(t.value[2].display_name),1),a("div",H,l(t.value[2].xp.toLocaleString("vi-VN"))+" XP",1),s[6]||(s[6]=a("div",{class:"podium-bar bar-3"},null,-1))])])):v("",!0),o.value?(d(),i("div",J,[(d(),i(_,null,b(10,e=>a("div",{key:e,class:"flex items-center gap-4 border-b border-[var(--border)] px-5 py-3.5 last:border-0"},[...s[7]||(s[7]=[S('<div class="h-4 w-6 animate-pulse rounded bg-[var(--bg2)]" data-v-b9997b76></div><div class="h-9 w-9 animate-pulse rounded-full bg-[var(--bg2)]" data-v-b9997b76></div><div class="flex-1 space-y-1.5" data-v-b9997b76><div class="h-3.5 w-32 animate-pulse rounded bg-[var(--bg2)]" data-v-b9997b76></div><div class="h-3 w-20 animate-pulse rounded bg-[var(--bg2)]" data-v-b9997b76></div></div><div class="h-4 w-16 animate-pulse rounded bg-[var(--bg2)]" data-v-b9997b76></div>',4)])])),64))])):(d(),i("div",O,[(d(!0),i(_,null,b(p.value,e=>{var m;return d(),i("div",{key:e.user_id,class:h(["lb-row",{"lb-row--me":e.is_current_user}])},[a("div",Q,[a("span",{class:h(["lb-rank-num",{"text-[#f59e0b]":e.rank===1,"text-[#9ca3af]":e.rank===2,"text-[#b45309]":e.rank===3}])},l(e.rank),3)]),a("img",{src:e.avatar_url||"/icon_profile.jpg",alt:e.display_name,class:"lb-avatar"},null,8,R),a("div",U,[a("div",W,[x(l(e.display_name)+" ",1),e.is_current_user?(d(),i("span",Y,"Bạn")):v("",!0)]),a("div",Z,[s[8]||(s[8]=a("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"#f97316","stroke-width":"2"},[a("path",{d:"M12 2c0 0-5 6-5 10a5 5 0 0 0 10 0c0-4-5-10-5-10z"})],-1)),x(" "+l(e.streak)+" ngày streak ",1)])]),a("div",aa,[a("div",sa,[a("div",{class:"lb-xp-bar",style:N({width:`${Math.min(100,e.xp/(((m=n.value[0])==null?void 0:m.xp)||1)*100)}%`})},null,4)]),a("div",ea,l(e.xp.toLocaleString("vi-VN"))+" XP",1)])],2)}),128)),!p.value.length&&!o.value?(d(),i("div",ta," Chưa có dữ liệu xếp hạng. ")):v("",!0)])),r.value?(d(),i("div",la,l(r.value),1)):v("",!0)]))}},oa=y(ia,[["__scopeId","data-v-b9997b76"]]);export{oa as default};
|
|
|
|
|
|
fronted/dist/assets/Leaderboard-DqCSL79X.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{M as S,q as B,c as o,a as s,t,g as m,f as v,F as g,r as h,j as p,z as _,o as i,y as k,n as N}from"./index-CId0hMaT.js";class V{async getLeaderboard(d=10){const{data:u}=await S.get("/leaderboard",{params:{top:d}});return u}}const j=new V,X={class:"lb-page"},P={key:0,class:"mb-5 flex items-center gap-4 rounded-xl border-2 border-[#34d399] bg-[#f0fdf4] px-5 py-4"},z={class:"text-2xl font-black text-[#15803d]"},M=["src"],C={class:"flex-1 min-w-0"},T={class:"text-[14px] font-bold text-[var(--ink)]"},U={class:"text-[12px] text-[var(--ink3)]"},$={key:1,class:"podium mb-6"},E={class:"podium-item podium-2"},F={class:"podium-avatar-wrap"},q=["src","alt"],D={class:"podium-name"},I={class:"podium-xp"},K={class:"podium-item podium-1"},R={class:"podium-avatar-wrap"},A=["src","alt"],G={class:"podium-name font-bold"},H={class:"podium-xp xp-gold"},J={class:"podium-item podium-3"},O={class:"podium-avatar-wrap"},Q=["src","alt"],W={class:"podium-name"},Y={class:"podium-xp"},Z={key:2,class:"ct-card overflow-hidden"},ss={key:3,class:"ct-card overflow-hidden"},as={class:"lb-rank"},es=["src","alt"],ts={class:"lb-info"},ls={class:"lb-name"},os={key:0,class:"lb-you-badge"},is={class:"lb-streak"},rs={class:"lb-xp-col"},ds={class:"lb-xp-bar-wrap"},ns={class:"lb-xp-num"},us={key:0,class:"py-12 text-center text-[13px] text-[var(--ink3)]"},cs={key:4,class:"mt-4 rounded-xl border border-[var(--rose-l)] bg-[var(--rose-bg)] p-4 text-[13px] text-[var(--rose)]"},ps={__name:"Leaderboard",setup(f){const d=p(!1),u=p(""),n=p([]),b=p(null),c=p(null),l=_(()=>n.value.slice(0,3)),x=_(()=>n.value.length<=3?n.value:n.value.slice(3)),w=_(()=>{var r;return((r=n.value[0])==null?void 0:r.xp)||1}),y=_(()=>n.value.some(r=>r.is_current_user));async function L(){var r,a;d.value=!0,u.value="";try{const e=await j.getLeaderboard(10);n.value=e.top||[],b.value=e.current_user_rank??null,c.value=e.current_user??null}catch(e){u.value=((a=(r=e.response)==null?void 0:r.data)==null?void 0:a.detail)||"Không thể tải bảng xếp hạng"}finally{d.value=!1}}return B(L),(r,a)=>(i(),o("div",X,[a[10]||(a[10]=s("div",{class:"mb-6"},[s("h1",{class:"text-xl font-bold text-[var(--ink)]"},"Bảng xếp hạng"),s("p",{class:"mt-0.5 text-[13px] text-[var(--ink3)]"},"Top 10 học viên theo điểm XP tích lũy")],-1)),!d.value&&c.value&&!y.value?(i(),o("div",P,[s("div",z,"#"+t(b.value),1),s("img",{src:c.value.avatar_url||"/icon_profile.jpg",alt:"",class:"h-11 w-11 rounded-full object-cover"},null,8,M),s("div",C,[s("div",T,[m(t(c.value.display_name)+" ",1),a[0]||(a[0]=s("span",{class:"lb-you-badge"},"Bạn",-1))]),s("div",U,t(c.value.xp.toLocaleString("vi-VN"))+" XP",1)])])):v("",!0),!d.value&&l.value.length>=3?(i(),o("div",$,[s("div",E,[s("div",F,[s("img",{src:l.value[1].avatar_url||"/icon_profile.jpg",alt:l.value[1].display_name,class:"podium-avatar"},null,8,q),a[1]||(a[1]=s("div",{class:"podium-rank rank-2"},"2",-1))]),s("div",D,t(l.value[1].display_name),1),s("div",I,t(l.value[1].xp.toLocaleString("vi-VN"))+" XP",1),a[2]||(a[2]=s("div",{class:"podium-bar bar-2"},null,-1))]),s("div",K,[a[4]||(a[4]=s("div",{class:"podium-crown"},[s("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"#f59e0b",stroke:"#f59e0b","stroke-width":"1.5"},[s("path",{d:"M3 17h18l-3-9-4.5 5L12 7l-1.5 6L6 8l-3 9z"})])],-1)),s("div",R,[s("img",{src:l.value[0].avatar_url||"/icon_profile.jpg",alt:l.value[0].display_name,class:"podium-avatar podium-avatar--large"},null,8,A),a[3]||(a[3]=s("div",{class:"podium-rank rank-1"},"1",-1))]),s("div",G,t(l.value[0].display_name),1),s("div",H,t(l.value[0].xp.toLocaleString("vi-VN"))+" XP",1),a[5]||(a[5]=s("div",{class:"podium-bar bar-1"},null,-1))]),s("div",J,[s("div",O,[s("img",{src:l.value[2].avatar_url||"/icon_profile.jpg",alt:l.value[2].display_name,class:"podium-avatar"},null,8,Q),a[6]||(a[6]=s("div",{class:"podium-rank rank-3"},"3",-1))]),s("div",W,t(l.value[2].display_name),1),s("div",Y,t(l.value[2].xp.toLocaleString("vi-VN"))+" XP",1),a[7]||(a[7]=s("div",{class:"podium-bar bar-3"},null,-1))])])):v("",!0),d.value?(i(),o("div",Z,[(i(),o(g,null,h(10,e=>s("div",{key:e,class:"flex items-center gap-4 border-b border-[var(--border)] px-5 py-3.5 last:border-0"},[...a[8]||(a[8]=[s("div",{class:"h-4 w-6 animate-pulse rounded bg-[var(--bg2)]"},null,-1),s("div",{class:"h-9 w-9 animate-pulse rounded-full bg-[var(--bg2)]"},null,-1),s("div",{class:"flex-1 space-y-1.5"},[s("div",{class:"h-3.5 w-32 animate-pulse rounded bg-[var(--bg2)]"})],-1)])])),64))])):(i(),o("div",ss,[(i(!0),o(g,null,h(x.value,e=>(i(),o("div",{key:`${e.user_id}-${e.rank}`,class:k(["lb-row",{"lb-row--me":e.is_current_user}])},[s("div",as,[s("span",{class:k(["lb-rank-num",{"text-[#f59e0b]":e.rank===1,"text-[#9ca3af]":e.rank===2,"text-[#b45309]":e.rank===3}])},t(e.rank),3)]),s("img",{src:e.avatar_url||"/icon_profile.jpg",alt:e.display_name,class:"lb-avatar"},null,8,es),s("div",ts,[s("div",ls,[m(t(e.display_name)+" ",1),e.is_current_user?(i(),o("span",os,"Bạn")):v("",!0)]),s("div",is,[a[9]||(a[9]=s("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"#f97316","stroke-width":"2"},[s("path",{d:"M12 2c0 0-5 6-5 10a5 5 0 0 0 10 0c0-4-5-10-5-10z"})],-1)),m(" "+t(e.streak)+" ngày streak ",1)])]),s("div",rs,[s("div",ds,[s("div",{class:"lb-xp-bar",style:N({width:`${Math.min(100,e.xp/(w.value||1)*100)}%`})},null,4)]),s("div",ns,t(e.xp.toLocaleString("vi-VN"))+" XP",1)])],2))),128)),x.value.length?v("",!0):(i(),o("div",us," Chưa có dữ liệu xếp hạng. "))])),u.value?(i(),o("div",cs,t(u.value),1)):v("",!0)]))}};export{ps as default};
|
fronted/dist/assets/Listening-B_vWRFIo.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{q as B,c as r,a as s,h as f,i as I,t as E,d as F,v as N,F as g,r as S,f as j,j as a,k as R,z as C,l as U,o as i,g as D,x as Q}from"./index-BnN_E-ke.js";import{l as A}from"./mockTestService-uSLqkhlt.js";import{u as G}from"./practice-BINrC2xL.js";import{_ as K}from"./SkillTestCard-CKhK5GXd.js";import{_ as O,M as W}from"./Paginator-lTh9FHNB.js";const Z={class:"mb-6"},H={class:"flex flex-wrap items-end gap-3"},J={class:"mt-0.5 text-[13px] text-[var(--ink3)]"},X={class:"ml-auto"},Y={key:0,class:"grid gap-4 sm:grid-cols-2 xl:grid-cols-3"},ee={class:"grid gap-4 sm:grid-cols-2 xl:grid-cols-3"},te={key:0,class:"col-span-3 py-16 text-center text-[var(--ink3)]"},le={class:"mt-6"},p=9,ue={__name:"Listening",setup(se){const k=U(),M=G(),v=a(!1),d=a(""),c=a([]),u=a(1),x=C(()=>d.value?c.value.filter(l=>{var e;return(e=l.title)==null?void 0:e.toLowerCase().includes(d.value.toLowerCase())}):c.value),h=C(()=>x.value.slice((u.value-1)*p,u.value*p));function P(l){return Object.entries((l==null?void 0:l.quizzes)||{}).filter(([e])=>e.startsWith("part_")).sort((e,o)=>e[0].localeCompare(o[0],void 0,{numeric:!0})).map(([e,o])=>({key:e,...o}))}const m=a(!1),_=a(""),T=a(null),z=a(null);function b(l,e){var o;T.value=l,z.value=e||((o=l.quizzes)==null?void 0:o.full),_.value=l.title,m.value=!0}async function $(l){var o,t;const e=(o=z.value)==null?void 0:o.id;if(e)if(l==="exam")k.push(`/quiz/${e}?mode=exam`);else{const n=await M.startSession("listening",e);k.push(`/quiz/${((t=n==null?void 0:n.quiz)==null?void 0:t.id)||e}?mode=practice`)}}return B(async()=>{v.value=!0;try{c.value=await A({skillId:2})}finally{v.value=!1}}),(l,e)=>{const o=R("RouterLink");return i(),r("div",null,[s("div",Z,[f(o,{to:"/dashboard",class:"mb-3 inline-flex items-center gap-1.5 text-[12px] text-[var(--ink3)] hover:text-[var(--ink)] transition-colors"},{default:I(()=>[...e[4]||(e[4]=[s("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[s("polyline",{points:"15 18 9 12 15 6"})],-1),D(" Trang chủ ",-1)])]),_:1}),s("div",H,[s("div",null,[e[5]||(e[5]=s("h1",{class:"text-xl font-bold text-[var(--ink)]"},"Listening",-1)),s("p",J,E(c.value.length)+" bộ đề · IELTS Listening",1)]),s("div",X,[F(s("input",{"onUpdate:modelValue":e[0]||(e[0]=t=>d.value=t),class:"ct-input w-64",placeholder:"Tìm đề...",onInput:e[1]||(e[1]=t=>u.value=1)},null,544),[[N,d.value]])])])]),v.value?(i(),r("div",Y,[(i(),r(g,null,S(p,t=>s("div",{key:t,class:"h-44 animate-pulse rounded-xl bg-[var(--bg2)]"})),64))])):(i(),r(g,{key:1},[s("div",ee,[(i(!0),r(g,null,S(h.value,t=>{var n,q,y,w;return i(),Q(K,{key:t.id,title:t.title,thumbnail:t.thumbnail,"book-code":t.book_code,"skill-label":"Listening","question-count":(q=(n=t.quizzes)==null?void 0:n.full)==null?void 0:q.question_count,time:(w=(y=t.quizzes)==null?void 0:y.full)==null?void 0:w.time,parts:P(t),onStartFull:V=>{var L;return b(t,(L=t.quizzes)==null?void 0:L.full)},onStartPart:V=>b(t,V)},null,8,["title","thumbnail","book-code","question-count","time","parts","onStartFull","onStartPart"])}),128)),h.value.length?j("",!0):(i(),r("p",te,"Không tìm thấy đề phù hợp."))]),s("div",le,[f(O,{modelValue:u.value,"onUpdate:modelValue":e[2]||(e[2]=t=>u.value=t),total:x.value.length,"page-size":p},null,8,["modelValue","total"])])],64)),f(W,{modelValue:m.value,"onUpdate:modelValue":e[3]||(e[3]=t=>m.value=t),"test-title":_.value,onConfirm:$},null,8,["modelValue","test-title"])])}}};export{ue as default};
|
|
|
|
|
|
fronted/dist/assets/Listening-CsI8kE6C.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{q as I,c as r,a as s,h as f,i as M,t as E,d as F,v as N,F as g,r as S,f as j,j as a,k as R,z as C,l as U,o as i,g as D,x as Q}from"./index-CId0hMaT.js";import{l as A}from"./mockTestService-ByZrQuTM.js";import{u as G}from"./practice-nWRX1lS1.js";import{_ as K}from"./SkillTestCard-B586wTHe.js";import{_ as O}from"./ModePickerModal-CUJKgPsz.js";import{_ as W}from"./Paginator-C1T4CVWL.js";const Z={class:"mb-6"},H={class:"flex flex-wrap items-end gap-3"},J={class:"mt-0.5 text-[13px] text-[var(--ink3)]"},X={class:"ml-auto"},Y={key:0,class:"grid gap-4 sm:grid-cols-2 xl:grid-cols-3"},ee={class:"grid gap-4 sm:grid-cols-2 xl:grid-cols-3"},te={key:0,class:"col-span-3 py-16 text-center text-[var(--ink3)]"},le={class:"mt-6"},p=9,de={__name:"Listening",setup(se){const k=U(),T=G(),v=a(!1),d=a(""),c=a([]),u=a(1),_=C(()=>d.value?c.value.filter(l=>{var e;return(e=l.title)==null?void 0:e.toLowerCase().includes(d.value.toLowerCase())}):c.value),x=C(()=>_.value.slice((u.value-1)*p,u.value*p));function $(l){return Object.entries((l==null?void 0:l.quizzes)||{}).filter(([e])=>e.startsWith("part_")).sort((e,o)=>e[0].localeCompare(o[0],void 0,{numeric:!0})).map(([e,o])=>({key:e,...o}))}const m=a(!1),h=a(""),P=a(null),z=a(null);function b(l,e){var o;P.value=l,z.value=e||((o=l.quizzes)==null?void 0:o.full),h.value=l.title,m.value=!0}async function B(l){var o,t;const e=(o=z.value)==null?void 0:o.id;if(e)if(l==="exam")k.push(`/quiz/${e}?mode=exam`);else{const n=await T.startSession("listening",e);k.push(`/quiz/${((t=n==null?void 0:n.quiz)==null?void 0:t.id)||e}?mode=practice`)}}return I(async()=>{v.value=!0;try{c.value=await A({skillId:2})}finally{v.value=!1}}),(l,e)=>{const o=R("RouterLink");return i(),r("div",null,[s("div",Z,[f(o,{to:"/dashboard",class:"mb-3 inline-flex items-center gap-1.5 text-[12px] text-[var(--ink3)] hover:text-[var(--ink)] transition-colors"},{default:M(()=>[...e[4]||(e[4]=[s("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[s("polyline",{points:"15 18 9 12 15 6"})],-1),D(" Trang chủ ",-1)])]),_:1}),s("div",H,[s("div",null,[e[5]||(e[5]=s("h1",{class:"text-xl font-bold text-[var(--ink)]"},"Listening",-1)),s("p",J,E(c.value.length)+" bộ đề · IELTS Listening",1)]),s("div",X,[F(s("input",{"onUpdate:modelValue":e[0]||(e[0]=t=>d.value=t),class:"ct-input w-64",placeholder:"Tìm đề...",onInput:e[1]||(e[1]=t=>u.value=1)},null,544),[[N,d.value]])])])]),v.value?(i(),r("div",Y,[(i(),r(g,null,S(p,t=>s("div",{key:t,class:"h-44 animate-pulse rounded-xl bg-[var(--bg2)]"})),64))])):(i(),r(g,{key:1},[s("div",ee,[(i(!0),r(g,null,S(x.value,t=>{var n,q,y,w;return i(),Q(K,{key:t.id,title:t.title,thumbnail:t.thumbnail,"book-code":t.book_code,"skill-label":"Listening","question-count":(q=(n=t.quizzes)==null?void 0:n.full)==null?void 0:q.question_count,time:(w=(y=t.quizzes)==null?void 0:y.full)==null?void 0:w.time,parts:$(t),onStartFull:V=>{var L;return b(t,(L=t.quizzes)==null?void 0:L.full)},onStartPart:V=>b(t,V)},null,8,["title","thumbnail","book-code","question-count","time","parts","onStartFull","onStartPart"])}),128)),x.value.length?j("",!0):(i(),r("p",te,"Không tìm thấy đề phù hợp."))]),s("div",le,[f(W,{modelValue:u.value,"onUpdate:modelValue":e[2]||(e[2]=t=>u.value=t),total:_.value.length,"page-size":p},null,8,["modelValue","total"])])],64)),f(O,{modelValue:m.value,"onUpdate:modelValue":e[3]||(e[3]=t=>m.value=t),"test-title":h.value,onConfirm:B},null,8,["modelValue","test-title"])])}}};export{de as default};
|
fronted/dist/assets/Login-C03J8r1H.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
.auth-page[data-v-0ce8f6c6]{min-height:100vh;display:grid;grid-template-columns:1fr 1fr;background:var(--bg)}.auth-card[data-v-0ce8f6c6]{display:flex;flex-direction:column;justify-content:center;padding:60px 64px;background:var(--surface)}.auth-logo[data-v-0ce8f6c6]{margin-bottom:36px}.logo-mark[data-v-0ce8f6c6]{font-size:24px;font-weight:700;color:var(--ink);letter-spacing:-.02em}.logo-mark span[data-v-0ce8f6c6]{color:var(--green-l)}.logo-sub[data-v-0ce8f6c6]{font-size:11px;color:var(--ink3);letter-spacing:.1em;text-transform:uppercase;margin-top:3px}.auth-title[data-v-0ce8f6c6]{font-size:28px;font-weight:700;color:var(--ink);margin-bottom:6px}.auth-sub[data-v-0ce8f6c6]{font-size:14px;color:var(--ink3);margin-bottom:32px}.form-group[data-v-0ce8f6c6]{margin-bottom:16px}.form-label[data-v-0ce8f6c6]{display:block;font-size:12px;font-weight:600;color:var(--ink2);margin-bottom:6px}.form-input[data-v-0ce8f6c6]{width:100%;padding:11px 14px;border:1.5px solid var(--border2);border-radius:var(--r-sm);font-size:14px;font-family:inherit;color:var(--ink);background:var(--bg);outline:none;transition:border-color .18s}.form-input[data-v-0ce8f6c6]:focus{border-color:var(--green-l);background:#fff}.error-msg[data-v-0ce8f6c6]{font-size:13px;color:var(--rose);background:var(--rose-bg);padding:10px 14px;border-radius:var(--r-sm);margin-bottom:12px}.btn-primary[data-v-0ce8f6c6]{width:100%;padding:12px;border-radius:var(--r-sm);background:var(--green);color:#fff;font-size:15px;font-weight:600;border:none;cursor:pointer;font-family:inherit;transition:all .18s;margin-top:4px}.btn-primary[data-v-0ce8f6c6]:hover{background:#245c42;transform:translateY(-1px);box-shadow:0 4px 12px #2d6a4f4d}.btn-primary[data-v-0ce8f6c6]:disabled{opacity:.6;cursor:not-allowed;transform:none}.auth-footer[data-v-0ce8f6c6]{margin-top:24px;font-size:13px;color:var(--ink3);text-align:center}.auth-link[data-v-0ce8f6c6]{color:var(--green);font-weight:600;text-decoration:none;margin-left:4px}.auth-link[data-v-0ce8f6c6]:hover{text-decoration:underline}.auth-deco[data-v-0ce8f6c6]{background:var(--ink);display:flex;align-items:center;justify-content:center;padding:60px;position:relative;overflow:hidden}.auth-deco[data-v-0ce8f6c6]:before{content:"";position:absolute;bottom:-80px;right:-60px;width:400px;height:400px;background:radial-gradient(circle,rgba(82,183,136,.2) 0%,transparent 70%)}.auth-deco[data-v-0ce8f6c6]:after{content:"";position:absolute;top:-80px;left:-60px;width:300px;height:300px;background:radial-gradient(circle,rgba(72,149,239,.15) 0%,transparent 70%)}.deco-content[data-v-0ce8f6c6]{position:relative;z-index:1}.deco-badge[data-v-0ce8f6c6]{display:inline-block;padding:5px 14px;border-radius:20px;background:#52b78833;color:var(--green-l);font-size:12px;font-weight:600;margin-bottom:20px;border:1px solid rgba(82,183,136,.3)}.deco-title[data-v-0ce8f6c6]{font-size:36px;font-weight:700;color:#fff;line-height:1.2;margin-bottom:28px}.deco-features[data-v-0ce8f6c6]{display:flex;flex-direction:column;gap:12px}.deco-feature[data-v-0ce8f6c6]{display:flex;align-items:center;gap:10px;font-size:14px;color:#ffffffb3}.feature-check[data-v-0ce8f6c6]{width:20px;height:20px;border-radius:50%;background:var(--green-l);color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}@media (max-width: 768px){.auth-page[data-v-0ce8f6c6]{grid-template-columns:1fr}.auth-deco[data-v-0ce8f6c6]{display:none}.auth-card[data-v-0ce8f6c6]{padding:40px 24px}}
|
|
|
|
|
|
fronted/dist/assets/Login-CipjpBTk.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{u as b,c as n,a as o,b as w,w as y,d as p,v as h,e as a,t as u,f as _,g as e,h as L,i as S,F as x,r as C,j as c,k as V,l as T,o as l}from"./index-CId0hMaT.js";const B={class:"auth-page"},E={class:"auth-card"},I={class:"form-group"},N={class:"form-group"},M={key:0,class:"error-msg"},R=["disabled"],j={class:"auth-footer"},A={class:"auth-deco"},F={class:"deco-content"},q={class:"deco-badge"},D={width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",style:{display:"inline","margin-right":"5px"}},P={class:"deco-features"},W={__name:"Login",setup(U){const i=b(),g=T(),r=c(""),d=c(""),m=["Reading với 200+ bài đọc thực chiến","Listening với âm thanh chất lượng cao","Writing với AI feedback chi tiết","Speaking với phân tích phát âm","Từ vựng học theo thuật toán FSRS"];async function v(){await i.login(r.value,d.value)&&g.push("/dashboard")}return(f,t)=>{const k=V("router-link");return l(),n("div",B,[o("div",E,[t[6]||(t[6]=w('<div class="auth-logo"><div class="logo-mark font-display">Lingua<span>IELTS</span></div><div class="logo-sub">AI-Powered Learning Platform</div></div><h1 class="auth-title font-display">Chào mừng trở lại!</h1><p class="auth-sub">Đăng nhập để tiếp tục hành trình luyện thi IELTS của bạn.</p>',3)),o("form",{onSubmit:y(v,["prevent"])},[o("div",I,[t[2]||(t[2]=o("label",{class:"form-label",for:"login-email"},"Email",-1)),p(o("input",{id:"login-email","onUpdate:modelValue":t[0]||(t[0]=s=>r.value=s),type:"email",class:"form-input",placeholder:"you@example.com",required:"",autocomplete:"email"},null,512),[[h,r.value]])]),o("div",N,[t[3]||(t[3]=o("label",{class:"form-label",for:"login-password"},"Mật khẩu",-1)),p(o("input",{id:"login-password","onUpdate:modelValue":t[1]||(t[1]=s=>d.value=s),type:"password",class:"form-input",placeholder:"••••••••",required:"",autocomplete:"current-password"},null,512),[[h,d.value]])]),a(i).error?(l(),n("div",M,u(a(i).error),1)):_("",!0),o("button",{type:"submit",class:"btn-primary",disabled:a(i).loading,id:"login-btn"},u(a(i).loading?"Đang đăng nhập...":"Đăng nhập →"),9,R)],32),o("div",j,[t[5]||(t[5]=e(" Chưa có tài khoản? ",-1)),L(k,{to:"/register",class:"auth-link"},{default:S(()=>[...t[4]||(t[4]=[e("Đăng ký miễn phí",-1)])]),_:1})])]),o("div",A,[o("div",F,[o("div",q,[(l(),n("svg",D,[...t[7]||(t[7]=[o("path",{d:"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"},null,-1)])])),t[8]||(t[8]=e(" IELTS Ready ",-1))]),t[10]||(t[10]=o("h2",{class:"deco-title font-display"},[e("Chinh phục band"),o("br"),e("điểm mục tiêu")],-1)),o("div",P,[(l(),n(x,null,C(m,s=>o("div",{key:s,class:"deco-feature"},[t[9]||(t[9]=o("span",{class:"feature-check"},[o("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"white","stroke-width":"3","stroke-linecap":"round","stroke-linejoin":"round"},[o("polyline",{points:"20 6 9 17 4 12"})])],-1)),e(" "+u(s),1)])),64))])])])])}}};export{W as default};
|
fronted/dist/assets/Login-CrChhjRP.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{_ as b,u as _,c as i,a as e,b as w,w as y,d as c,v as p,e as n,t as u,f as L,g as s,h as x,i as S,F as C,r as V,j as h,k as I,l as T,o as l}from"./index-BnN_E-ke.js";const B={class:"auth-page"},E={class:"auth-card"},N={class:"form-group"},M={class:"form-group"},R={key:0,class:"error-msg"},j=["disabled"],A={class:"auth-footer"},F={class:"auth-deco"},q={class:"deco-content"},D={class:"deco-badge"},P={width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",style:{display:"inline","margin-right":"5px"}},U={class:"deco-features"},z={__name:"Login",setup(W){const a=_(),v=T(),r=h(""),d=h(""),g=["Reading với 200+ bài đọc thực chiến","Listening với âm thanh chất lượng cao","Writing với AI feedback chi tiết","Speaking với phân tích phát âm","Từ vựng học theo thuật toán FSRS"];async function m(){await a.login(r.value,d.value)&&v.push("/dashboard")}return(f,t)=>{const k=I("router-link");return l(),i("div",B,[e("div",E,[t[6]||(t[6]=w('<div class="auth-logo" data-v-0ce8f6c6><div class="logo-mark font-display" data-v-0ce8f6c6>Lingua<span data-v-0ce8f6c6>IELTS</span></div><div class="logo-sub" data-v-0ce8f6c6>AI-Powered Learning Platform</div></div><h1 class="auth-title font-display" data-v-0ce8f6c6>Chào mừng trở lại!</h1><p class="auth-sub" data-v-0ce8f6c6>Đăng nhập để tiếp tục hành trình luyện thi IELTS của bạn.</p>',3)),e("form",{onSubmit:y(m,["prevent"])},[e("div",N,[t[2]||(t[2]=e("label",{class:"form-label",for:"login-email"},"Email",-1)),c(e("input",{id:"login-email","onUpdate:modelValue":t[0]||(t[0]=o=>r.value=o),type:"email",class:"form-input",placeholder:"you@example.com",required:"",autocomplete:"email"},null,512),[[p,r.value]])]),e("div",M,[t[3]||(t[3]=e("label",{class:"form-label",for:"login-password"},"Mật khẩu",-1)),c(e("input",{id:"login-password","onUpdate:modelValue":t[1]||(t[1]=o=>d.value=o),type:"password",class:"form-input",placeholder:"••••••••",required:"",autocomplete:"current-password"},null,512),[[p,d.value]])]),n(a).error?(l(),i("div",R,u(n(a).error),1)):L("",!0),e("button",{type:"submit",class:"btn-primary",disabled:n(a).loading,id:"login-btn"},u(n(a).loading?"Đang đăng nhập...":"Đăng nhập →"),9,j)],32),e("div",A,[t[5]||(t[5]=s(" Chưa có tài khoản? ",-1)),x(k,{to:"/register",class:"auth-link"},{default:S(()=>[...t[4]||(t[4]=[s("Đăng ký miễn phí",-1)])]),_:1})])]),e("div",F,[e("div",q,[e("div",D,[(l(),i("svg",P,[...t[7]||(t[7]=[e("path",{d:"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"},null,-1)])])),t[8]||(t[8]=s(" IELTS Ready ",-1))]),t[10]||(t[10]=e("h2",{class:"deco-title font-display"},[s("Chinh phục band"),e("br"),s("điểm mục tiêu")],-1)),e("div",U,[(l(),i(C,null,V(g,o=>e("div",{key:o,class:"deco-feature"},[t[9]||(t[9]=e("span",{class:"feature-check"},[e("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"white","stroke-width":"3","stroke-linecap":"round","stroke-linejoin":"round"},[e("polyline",{points:"20 6 9 17 4 12"})])],-1)),s(" "+u(o),1)])),64))])])])])}}},H=b(z,[["__scopeId","data-v-0ce8f6c6"]]);export{H as default};
|
|
|
|
|
|
fronted/dist/assets/MockTestMode-DE-_sNCc.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{q as w,c as r,a as s,h as _,i as f,F as x,t as a,r as z,j as p,C,k as M,z as k,l as S,o as l,g as b}from"./index-CId0hMaT.js";import{g as R}from"./mockTestService-ByZrQuTM.js";const T={class:"page-wrapper"},F={class:"container",style:{"max-width":"980px"}},V={key:0,class:"card p-6 text-center text-[var(--ink2)]"},$={key:1,class:"card p-6 text-center"},j={class:"mb-5"},B={class:"text-xl font-semibold mt-2"},N={class:"text-sm text-[var(--ink2)] mt-1"},P={class:"grid gap-4 md:grid-cols-2"},D={class:"text-lg font-semibold"},E={class:"card p-5"},O={class:"grid gap-2"},W=["onClick"],A={class:"flex items-center justify-between gap-3"},G={class:"font-semibold"},H={class:"text-xs text-[var(--ink2)]"},X={__name:"MockTestMode",setup(J){const h=C(),v=S(),c=p(!1),n=p(null);function g(t){return String(t)==="1"?"Reading":String(t)==="2"?"Listening":String(t)==="8"?"Speaking":`Skill ${t}`}const u=k(()=>{var t,e;return(e=(t=n.value)==null?void 0:t.quizzes)==null?void 0:e.full}),y=k(()=>{var e;const t=((e=n.value)==null?void 0:e.quizzes)||{};return Object.entries(t).filter(([o])=>o.startsWith("part_")).sort((o,i)=>o[0].localeCompare(i[0],void 0,{numeric:!0})).map(([o,i])=>({key:o,label:o.replace("_"," ").replace("part","Part"),...i}))});function q(){var t;(t=u.value)!=null&&t.id&&v.push(`/quiz/${u.value.id}`)}function L(t){t!=null&&t.id&&v.push(`/quiz/${t.id}`)}return w(async()=>{c.value=!0;try{n.value=await R(h.params.id)}finally{c.value=!1}}),(t,e)=>{var i,m;const o=M("RouterLink");return l(),r("div",T,[s("div",F,[c.value?(l(),r("div",V,"Loading…")):n.value?(l(),r(x,{key:2},[s("div",j,[_(o,{to:"/",class:"text-sm text-[var(--ink2)] hover:text-[var(--ink)]"},{default:f(()=>[...e[2]||(e[2]=[b("← Danh sách đề",-1)])]),_:1}),s("h1",B,a(n.value.title),1),s("div",N,a(g(n.value.skill_id))+" · "+a(n.value.book_code)+" · #"+a(n.value.id),1)]),s("div",P,[s("button",{class:"card p-5 text-left hover:shadow transition-shadow",onClick:q},[e[3]||(e[3]=s("div",{class:"text-xs font-semibold text-[var(--ink2)] mb-1"},"Full Test",-1)),s("div",D,a((i=u.value)==null?void 0:i.question_count)+" câu · "+a((m=u.value)==null?void 0:m.time)+" phút",1),e[4]||(e[4]=s("div",{class:"text-sm text-[var(--ink2)] mt-2"},"Làm full test như thi thật",-1))]),s("div",E,[e[5]||(e[5]=s("div",{class:"text-xs font-semibold text-[var(--ink2)] mb-3"},"Luyện theo Part",-1)),s("div",O,[(l(!0),r(x,null,z(y.value,d=>(l(),r("button",{key:d.key,class:"rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-4 py-3 text-left hover:border-[var(--ink3)]",onClick:K=>L(d)},[s("div",A,[s("div",G,a(d.label),1),s("div",H,a(d.question_count)+" câu · "+a(d.time)+" phút",1)])],8,W))),128))])])])],64)):(l(),r("div",$,[e[1]||(e[1]=s("div",{class:"text-lg font-semibold mb-2"},"Mock test not found",-1)),_(o,{to:"/",class:"btn btn-primary"},{default:f(()=>[...e[0]||(e[0]=[b("Về trang chủ",-1)])]),_:1})]))])])}}};export{X as default};
|
fronted/dist/assets/MockTestMode-DYXoE6qd.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{q as w,c as r,a as s,h as _,i as f,F as x,t as a,r as z,j as p,B as M,k as C,z as k,l as S,o as l,g as b}from"./index-BnN_E-ke.js";import{g as R}from"./mockTestService-uSLqkhlt.js";const T={class:"page-wrapper"},B={class:"container",style:{"max-width":"980px"}},F={key:0,class:"card p-6 text-center text-[var(--ink2)]"},V={key:1,class:"card p-6 text-center"},$={class:"mb-5"},j={class:"text-xl font-semibold mt-2"},N={class:"text-sm text-[var(--ink2)] mt-1"},P={class:"grid gap-4 md:grid-cols-2"},D={class:"text-lg font-semibold"},E={class:"card p-5"},O={class:"grid gap-2"},W=["onClick"],A={class:"flex items-center justify-between gap-3"},G={class:"font-semibold"},H={class:"text-xs text-[var(--ink2)]"},X={__name:"MockTestMode",setup(J){const h=M(),v=S(),c=p(!1),n=p(null);function g(t){return String(t)==="1"?"Reading":String(t)==="2"?"Listening":String(t)==="8"?"Speaking":`Skill ${t}`}const u=k(()=>{var t,e;return(e=(t=n.value)==null?void 0:t.quizzes)==null?void 0:e.full}),y=k(()=>{var e;const t=((e=n.value)==null?void 0:e.quizzes)||{};return Object.entries(t).filter(([o])=>o.startsWith("part_")).sort((o,i)=>o[0].localeCompare(i[0],void 0,{numeric:!0})).map(([o,i])=>({key:o,label:o.replace("_"," ").replace("part","Part"),...i}))});function q(){var t;(t=u.value)!=null&&t.id&&v.push(`/quiz/${u.value.id}`)}function L(t){t!=null&&t.id&&v.push(`/quiz/${t.id}`)}return w(async()=>{c.value=!0;try{n.value=await R(h.params.id)}finally{c.value=!1}}),(t,e)=>{var i,m;const o=C("RouterLink");return l(),r("div",T,[s("div",B,[c.value?(l(),r("div",F,"Loading…")):n.value?(l(),r(x,{key:2},[s("div",$,[_(o,{to:"/",class:"text-sm text-[var(--ink2)] hover:text-[var(--ink)]"},{default:f(()=>[...e[2]||(e[2]=[b("← Danh sách đề",-1)])]),_:1}),s("h1",j,a(n.value.title),1),s("div",N,a(g(n.value.skill_id))+" · "+a(n.value.book_code)+" · #"+a(n.value.id),1)]),s("div",P,[s("button",{class:"card p-5 text-left hover:shadow transition-shadow",onClick:q},[e[3]||(e[3]=s("div",{class:"text-xs font-semibold text-[var(--ink2)] mb-1"},"Full Test",-1)),s("div",D,a((i=u.value)==null?void 0:i.question_count)+" câu · "+a((m=u.value)==null?void 0:m.time)+" phút",1),e[4]||(e[4]=s("div",{class:"text-sm text-[var(--ink2)] mt-2"},"Làm full test như thi thật",-1))]),s("div",E,[e[5]||(e[5]=s("div",{class:"text-xs font-semibold text-[var(--ink2)] mb-3"},"Luyện theo Part",-1)),s("div",O,[(l(!0),r(x,null,z(y.value,d=>(l(),r("button",{key:d.key,class:"rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-4 py-3 text-left hover:border-[var(--ink3)]",onClick:K=>L(d)},[s("div",A,[s("div",G,a(d.label),1),s("div",H,a(d.question_count)+" câu · "+a(d.time)+" phút",1)])],8,W))),128))])])])],64)):(l(),r("div",V,[e[1]||(e[1]=s("div",{class:"text-lg font-semibold mb-2"},"Mock test not found",-1)),_(o,{to:"/",class:"btn btn-primary"},{default:f(()=>[...e[0]||(e[0]=[b("Về trang chủ",-1)])]),_:1})]))])])}}};export{X as default};
|
|
|
|
|
|
fronted/dist/assets/ModePickerModal-CUJKgPsz.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{p as b,o as n,x,h as p,i as m,c as f,a as e,t as a,y as d,f as g,T as h,D as k,j as w}from"./index-CId0hMaT.js";const y={key:0,class:"fixed inset-0 z-[200] flex items-center justify-center p-4"},C={class:"relative z-10 w-full max-w-sm rounded-xl bg-white shadow-xl"},V={class:"flex items-start justify-between border-b border-[var(--border)] px-5 py-4"},B={class:"mt-0.5 text-[14px] font-bold text-[var(--ink)] leading-snug line-clamp-2"},j={class:"grid grid-cols-2 gap-3 p-4"},T={class:"border-t border-[var(--border)] px-4 pb-4 pt-3"},$=["disabled"],z={__name:"ModePickerModal",props:{modelValue:Boolean,testTitle:{type:String,default:""}},emits:["update:modelValue","confirm"],setup(l,{emit:u}){const c=l,s=u,r=w(null);b(()=>c.modelValue,o=>{o&&(r.value=null)});function v(){r.value&&(s("confirm",r.value),s("update:modelValue",!1))}return(o,t)=>(n(),x(k,{to:"body"},[p(h,{name:"modal"},{default:m(()=>[l.modelValue?(n(),f("div",y,[e("div",{class:"absolute inset-0 bg-black/50",onClick:t[0]||(t[0]=i=>o.$emit("update:modelValue",!1))}),e("div",C,[e("div",V,[e("div",null,[t[4]||(t[4]=e("div",{class:"text-[11px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},"Chọn chế độ luyện tập",-1)),e("div",B,a(l.testTitle),1)]),e("button",{class:"ml-3 flex h-7 w-7 shrink-0 items-center justify-center rounded text-[var(--ink3)] hover:bg-[var(--bg2)]",onClick:t[1]||(t[1]=i=>o.$emit("update:modelValue",!1))},[...t[5]||(t[5]=[e("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"})],-1)])])]),e("div",j,[e("button",{class:d(["flex flex-col items-start gap-2 rounded-lg border-2 p-4 text-left transition-colors",r.value==="practice"?"border-[var(--ink)] bg-[var(--bg)]":"border-[var(--border)] hover:border-[var(--ink2)]"]),onClick:t[2]||(t[2]=i=>r.value="practice")},[...t[6]||(t[6]=[e("div",{class:"flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--border)] bg-white"},[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round"},[e("circle",{cx:"12",cy:"12",r:"10"}),e("polyline",{points:"12 6 12 12 16 14"})])],-1),e("div",{class:"text-[13px] font-bold text-[var(--ink)]"},"Luyện tập",-1),e("div",{class:"text-[11px] leading-relaxed text-[var(--ink3)]"},"Xem đáp án, highlight, ghi chú, tra từ",-1)])],2),e("button",{class:d(["flex flex-col items-start gap-2 rounded-lg border-2 p-4 text-left transition-colors",r.value==="exam"?"border-[var(--ink)] bg-[var(--bg)]":"border-[var(--border)] hover:border-[var(--ink2)]"]),onClick:t[3]||(t[3]=i=>r.value="exam")},[...t[7]||(t[7]=[e("div",{class:"flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--border)] bg-white"},[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round"},[e("path",{d:"M9 11l3 3L22 4"}),e("path",{d:"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"})])],-1),e("div",{class:"text-[13px] font-bold text-[var(--ink)]"},"Thi thật",-1),e("div",{class:"text-[11px] leading-relaxed text-[var(--ink3)]"},"Đếm ngược, không xem đáp án",-1)])],2)]),e("div",T,[e("button",{disabled:!r.value,class:"ct-btn ct-btn-primary w-full disabled:cursor-not-allowed disabled:opacity-40",onClick:v},a(r.value==="practice"?"Bắt đầu luyện tập":r.value==="exam"?"Vào phòng thi":"Chọn chế độ"),9,$)])])])):g("",!0)]),_:1})]))}};export{z as _};
|
fronted/dist/assets/Paginator-C1T4CVWL.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{o as s,c as i,a,F as u,r as c,y as h,t as p,f as g,z as m}from"./index-CId0hMaT.js";const f={key:0,class:"flex items-center justify-center gap-1"},k=["disabled"],y={key:0,class:"px-1 text-[12px] text-[var(--ink3)]"},x=["onClick"],w=["disabled"],C={__name:"Paginator",props:{modelValue:{type:Number,default:1},total:{type:Number,default:0},pageSize:{type:Number,default:9}},emits:["update:modelValue"],setup(r){const l=r,d=m(()=>Math.max(1,Math.ceil(l.total/l.pageSize))),b=m(()=>{const o=d.value,t=l.modelValue;if(o<=7)return Array.from({length:o},(n,v)=>v+1);const e=[];e.push(1),t>3&&e.push("...");for(let n=Math.max(2,t-1);n<=Math.min(o-1,t+1);n++)e.push(n);return t<o-2&&e.push("..."),e.push(o),e});return(o,t)=>d.value>1?(s(),i("div",f,[a("button",{class:"flex h-8 w-8 items-center justify-center rounded border border-[var(--border)] bg-white text-[var(--ink3)] transition hover:bg-[var(--bg2)] disabled:pointer-events-none disabled:opacity-30",disabled:r.modelValue===1,onClick:t[0]||(t[0]=e=>o.$emit("update:modelValue",r.modelValue-1))},[...t[2]||(t[2]=[a("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[a("polyline",{points:"15 18 9 12 15 6"})],-1)])],8,k),(s(!0),i(u,null,c(b.value,e=>(s(),i(u,{key:e},[e==="..."?(s(),i("span",y,"…")):(s(),i("button",{key:1,class:h(["flex h-8 w-8 items-center justify-center rounded border text-[12px] font-medium transition",e===r.modelValue?"border-[var(--ink)] bg-[var(--ink)] text-white":"border-[var(--border)] bg-white text-[var(--ink2)] hover:bg-[var(--bg2)]"]),onClick:n=>o.$emit("update:modelValue",e)},p(e),11,x))],64))),128)),a("button",{class:"flex h-8 w-8 items-center justify-center rounded border border-[var(--border)] bg-white text-[var(--ink3)] transition hover:bg-[var(--bg2)] disabled:pointer-events-none disabled:opacity-30",disabled:r.modelValue===d.value,onClick:t[1]||(t[1]=e=>o.$emit("update:modelValue",r.modelValue+1))},[...t[3]||(t[3]=[a("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[a("polyline",{points:"9 18 15 12 9 6"})],-1)])],8,w)])):g("",!0)}};export{C as _};
|
fronted/dist/assets/Paginator-VCw60xVa.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
.modal-enter-active[data-v-f2f146d2],.modal-leave-active[data-v-f2f146d2]{transition:opacity .15s ease}.modal-enter-from[data-v-f2f146d2],.modal-leave-to[data-v-f2f146d2]{opacity:0}
|
|
|
|
|
|
fronted/dist/assets/Paginator-lTh9FHNB.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{_ as f,p as h,o as n,x as g,h as k,i as y,c as s,a as e,t as b,y as c,f as x,T as w,C as V,j as C,F as m,r as $,z as p}from"./index-BnN_E-ke.js";const j={key:0,class:"fixed inset-0 z-[200] flex items-center justify-center p-4"},B={class:"relative z-10 w-full max-w-sm rounded-xl bg-white shadow-xl"},M={class:"flex items-start justify-between border-b border-[var(--border)] px-5 py-4"},z={class:"mt-0.5 text-[14px] font-bold text-[var(--ink)] leading-snug line-clamp-2"},N={class:"grid grid-cols-2 gap-3 p-4"},T={class:"border-t border-[var(--border)] px-4 pb-4 pt-3"},P=["disabled"],S={__name:"ModePickerModal",props:{modelValue:Boolean,testTitle:{type:String,default:""}},emits:["update:modelValue","confirm"],setup(l,{emit:a}){const d=l,v=a,r=C(null);h(()=>d.modelValue,o=>{o&&(r.value=null)});function i(){r.value&&(v("confirm",r.value),v("update:modelValue",!1))}return(o,t)=>(n(),g(V,{to:"body"},[k(w,{name:"modal"},{default:y(()=>[l.modelValue?(n(),s("div",j,[e("div",{class:"absolute inset-0 bg-black/50",onClick:t[0]||(t[0]=u=>o.$emit("update:modelValue",!1))}),e("div",B,[e("div",M,[e("div",null,[t[4]||(t[4]=e("div",{class:"text-[11px] font-semibold uppercase tracking-wider text-[var(--ink3)]"},"Chọn chế độ luyện tập",-1)),e("div",z,b(l.testTitle),1)]),e("button",{class:"ml-3 flex h-7 w-7 shrink-0 items-center justify-center rounded text-[var(--ink3)] hover:bg-[var(--bg2)]",onClick:t[1]||(t[1]=u=>o.$emit("update:modelValue",!1))},[...t[5]||(t[5]=[e("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"})],-1)])])]),e("div",N,[e("button",{class:c(["flex flex-col items-start gap-2 rounded-lg border-2 p-4 text-left transition-colors",r.value==="practice"?"border-[var(--ink)] bg-[var(--bg)]":"border-[var(--border)] hover:border-[var(--ink2)]"]),onClick:t[2]||(t[2]=u=>r.value="practice")},[...t[6]||(t[6]=[e("div",{class:"flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--border)] bg-white"},[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round"},[e("circle",{cx:"12",cy:"12",r:"10"}),e("polyline",{points:"12 6 12 12 16 14"})])],-1),e("div",{class:"text-[13px] font-bold text-[var(--ink)]"},"Luyện tập",-1),e("div",{class:"text-[11px] leading-relaxed text-[var(--ink3)]"},"Xem đáp án, highlight, ghi chú, tra từ",-1)])],2),e("button",{class:c(["flex flex-col items-start gap-2 rounded-lg border-2 p-4 text-left transition-colors",r.value==="exam"?"border-[var(--ink)] bg-[var(--bg)]":"border-[var(--border)] hover:border-[var(--ink2)]"]),onClick:t[3]||(t[3]=u=>r.value="exam")},[...t[7]||(t[7]=[e("div",{class:"flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--border)] bg-white"},[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round"},[e("path",{d:"M9 11l3 3L22 4"}),e("path",{d:"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"})])],-1),e("div",{class:"text-[13px] font-bold text-[var(--ink)]"},"Thi thật",-1),e("div",{class:"text-[11px] leading-relaxed text-[var(--ink3)]"},"Đếm ngược, không xem đáp án",-1)])],2)]),e("div",T,[e("button",{disabled:!r.value,class:"ct-btn ct-btn-primary w-full disabled:cursor-not-allowed disabled:opacity-40",onClick:i},b(r.value==="practice"?"Bắt đầu luyện tập":r.value==="exam"?"Vào phòng thi":"Chọn chế độ"),9,P)])])])):x("",!0)]),_:1})]))}},I=f(S,[["__scopeId","data-v-f2f146d2"]]),L={key:0,class:"flex items-center justify-center gap-1"},F=["disabled"],A={key:0,class:"px-1 text-[12px] text-[var(--ink3)]"},D=["onClick"],E=["disabled"],X={__name:"Paginator",props:{modelValue:{type:Number,default:1},total:{type:Number,default:0},pageSize:{type:Number,default:9}},emits:["update:modelValue"],setup(l){const a=l,d=p(()=>Math.max(1,Math.ceil(a.total/a.pageSize))),v=p(()=>{const r=d.value,i=a.modelValue;if(r<=7)return Array.from({length:r},(t,u)=>u+1);const o=[];o.push(1),i>3&&o.push("...");for(let t=Math.max(2,i-1);t<=Math.min(r-1,i+1);t++)o.push(t);return i<r-2&&o.push("..."),o.push(r),o});return(r,i)=>d.value>1?(n(),s("div",L,[e("button",{class:"flex h-8 w-8 items-center justify-center rounded border border-[var(--border)] bg-white text-[var(--ink3)] transition hover:bg-[var(--bg2)] disabled:pointer-events-none disabled:opacity-30",disabled:l.modelValue===1,onClick:i[0]||(i[0]=o=>r.$emit("update:modelValue",l.modelValue-1))},[...i[2]||(i[2]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"15 18 9 12 15 6"})],-1)])],8,F),(n(!0),s(m,null,$(v.value,o=>(n(),s(m,{key:o},[o==="..."?(n(),s("span",A,"…")):(n(),s("button",{key:1,class:c(["flex h-8 w-8 items-center justify-center rounded border text-[12px] font-medium transition",o===l.modelValue?"border-[var(--ink)] bg-[var(--ink)] text-white":"border-[var(--border)] bg-white text-[var(--ink2)] hover:bg-[var(--bg2)]"]),onClick:t=>r.$emit("update:modelValue",o)},b(o),11,D))],64))),128)),e("button",{class:"flex h-8 w-8 items-center justify-center rounded border border-[var(--border)] bg-white text-[var(--ink3)] transition hover:bg-[var(--bg2)] disabled:pointer-events-none disabled:opacity-30",disabled:l.modelValue===d.value,onClick:i[1]||(i[1]=o=>r.$emit("update:modelValue",l.modelValue+1))},[...i[3]||(i[3]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"9 18 15 12 9 6"})],-1)])],8,E)])):x("",!0)}};export{I as M,X as _};
|
|
|
|
|
|
fronted/dist/assets/Profile-CXJvjz7d.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{u as D,j as f,q as I,c,a as t,e as r,t as i,g as U,f as g,w as X,d as m,v as _,m as O,F as R,r as $,z as b,l as G,o as p}from"./index-CId0hMaT.js";import{u as J}from"./ielts-4A3ec5AY.js";import"./historyService-y1ubbIgp.js";const K={class:"profile-page"},Q={class:"profile-grid"},W={class:"card profile-card"},Y={class:"avatar-section"},Z={class:"avatar-wrapper"},tt=["src","alt"],st={class:"avatar-upload-overlay",title:"Đổi ảnh đại diện"},et=["disabled"],at={class:"avatar-info"},lt={class:"user-name font-display"},ot={class:"user-email"},it={class:"user-level-badge"},nt={key:0,class:"upload-status"},rt={key:1,class:"error-msg text-[12px]"},dt={key:2,class:"success-msg text-[12px]"},ut={class:"stats-row"},ct={class:"stat-box"},pt={class:"stat-val font-display"},vt={class:"stat-box"},ft={class:"stat-val font-display"},mt={class:"stat-box"},ht={class:"stat-val font-display"},gt={class:"card",style:{padding:"24px"}},_t={class:"form-group"},bt={class:"form-group"},yt=["value"],wt={class:"form-row"},kt={class:"form-group"},xt=["value"],Bt={class:"form-group"},St={class:"form-actions"},Ct=["disabled"],Mt={key:0,class:"error-msg"},Vt={key:1,class:"success-msg"},Ut={class:"card",style:{padding:"24px","margin-top":"20px"}},Pt={class:"form-row"},Nt={class:"form-group"},Tt={class:"form-group"},jt={class:"form-group"},Lt={__name:"Profile",setup(At){var x,B,S;const e=D(),d=J(),P=G(),y=f(!1),N=b(()=>{var a;return((a=e.profile)==null?void 0:a.avatar_url)||"/icon_profile.jpg"}),w=f(!1),h=f(""),k=f(!1);async function T(a){var u;const s=(u=a.target.files)==null?void 0:u[0];if(!s)return;if(s.size>2*1024*1024){h.value="Ảnh không được vượt quá 2MB";return}h.value="",w.value=!0;const o=await e.uploadAvatar(s);w.value=!1,o?(k.value=!0,setTimeout(()=>{k.value=!1},3e3)):h.value=e.error||"Upload thất bại"}const j=b(()=>{var s;return(((s=e.profile)==null?void 0:s.full_name)||"U").split(" ").map(o=>o[0]).join("").toUpperCase().slice(0,2)}),A=b(()=>d.history.length||0),z=b(()=>{const a=Object.values(d.bandScores).filter(Boolean);return a.length?(a.reduce((s,o)=>s+o,0)/a.length).toFixed(1):"—"}),F=b(()=>{const a=d.history.reduce((s,o)=>{const u=parseInt(o.duration)||0;return s+u},0);return a<60?"0h":`${Math.round(a/3600)}h`}),H=[5,5.5,6,6.5,7,7.5,8,8.5,9],n=f({full_name:((x=e.profile)==null?void 0:x.full_name)||"",target_band:((B=e.profile)==null?void 0:B.target_band)||7,exam_date:((S=e.profile)==null?void 0:S.exam_date)||""}),v=f({current:"",newPw:"",confirm:""});async function L(){await e.updateProfile(n.value)&&(y.value=!0,setTimeout(()=>y.value=!1,3e3))}function E(){alert("Tính năng đổi mật khẩu sẽ được tích hợp sau!")}function q(){e.logout(),P.push("/login")}return I(async()=>{var a,s,o;e.profile||await e.fetchProfile(),n.value.full_name=((a=e.profile)==null?void 0:a.full_name)||"",n.value.target_band=((s=e.profile)==null?void 0:s.target_band)||7,n.value.exam_date=((o=e.profile)==null?void 0:o.exam_date)||"",d.history.length||await d.fetchHistory(),d.bandScores.reading||await d.fetchStats()}),(a,s)=>{var o,u,C,M,V;return p(),c("div",K,[t("div",Q,[t("div",W,[t("div",Y,[t("div",Z,[t("img",{src:N.value,alt:j.value,class:"avatar-img"},null,8,tt),t("label",st,[s[6]||(s[6]=t("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("path",{d:"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"}),t("circle",{cx:"12",cy:"13",r:"4"})],-1)),t("input",{type:"file",accept:"image/*",class:"hidden",onChange:T,disabled:r(e).loading},null,40,et)])]),t("div",at,[t("div",lt,i(((o=r(e).profile)==null?void 0:o.full_name)||"Người dùng"),1),t("div",ot,i((u=r(e).profile)==null?void 0:u.email),1),t("div",it,[s[7]||(s[7]=t("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[t("path",{d:"M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"})],-1)),U(" "+i(((C=r(e).profile)==null?void 0:C.streak)??0)+" ngày streak · "+i((((M=r(e).profile)==null?void 0:M.xp)??0).toLocaleString("vi-VN"))+" XP ",1)])])]),w.value?(p(),c("div",nt,[...s[8]||(s[8]=[t("svg",{class:"animate-spin",width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("circle",{cx:"12",cy:"12",r:"10"}),t("path",{d:"M22 12a10 10 0 0 1-10 10"})],-1),U(" Đang tải ảnh... ",-1)])])):g("",!0),h.value?(p(),c("div",rt,i(h.value),1)):g("",!0),k.value?(p(),c("div",dt,"✅ Cập nhật ảnh thành công!")):g("",!0),t("div",ut,[t("div",ct,[t("div",pt,i(A.value),1),s[9]||(s[9]=t("div",{class:"stat-label"},"Bài đã làm",-1))]),t("div",vt,[t("div",ft,i(z.value),1),s[10]||(s[10]=t("div",{class:"stat-label"},"Band trung bình",-1))]),t("div",mt,[t("div",ht,i(F.value),1),s[11]||(s[11]=t("div",{class:"stat-label"},"Tổng thời gian",-1))])])]),t("div",gt,[s[16]||(s[16]=t("div",{class:"section-title font-display",style:{"margin-bottom":"20px"}},"Chỉnh sửa thông tin",-1)),t("form",{onSubmit:X(L,["prevent"])},[t("div",_t,[s[12]||(s[12]=t("label",{class:"form-label"},"Họ và tên",-1)),m(t("input",{"onUpdate:modelValue":s[0]||(s[0]=l=>n.value.full_name=l),class:"form-input",type:"text",placeholder:"Nhập họ và tên"},null,512),[[_,n.value.full_name]])]),t("div",bt,[s[13]||(s[13]=t("label",{class:"form-label"},"Email",-1)),t("input",{value:(V=r(e).profile)==null?void 0:V.email,class:"form-input",type:"email",disabled:""},null,8,yt)]),t("div",wt,[t("div",kt,[s[14]||(s[14]=t("label",{class:"form-label"},"Band mục tiêu",-1)),m(t("select",{"onUpdate:modelValue":s[1]||(s[1]=l=>n.value.target_band=l),class:"form-input"},[(p(),c(R,null,$(H,l=>t("option",{key:l,value:l},i(l),9,xt)),64))],512),[[O,n.value.target_band]])]),t("div",Bt,[s[15]||(s[15]=t("label",{class:"form-label"},"Ngày thi dự kiến",-1)),m(t("input",{"onUpdate:modelValue":s[2]||(s[2]=l=>n.value.exam_date=l),class:"form-input",type:"date"},null,512),[[_,n.value.exam_date]])])]),t("div",St,[t("button",{type:"button",class:"btn-danger",onClick:q},"Đăng xuất"),t("button",{type:"submit",class:"btn-primary",disabled:r(e).loading},i(r(e).loading?"Đang lưu...":"Lưu thay đổi"),9,Ct)]),r(e).error?(p(),c("div",Mt,i(r(e).error),1)):g("",!0),y.value?(p(),c("div",Vt,"✅ Đã lưu thành công!")):g("",!0)],32)])]),t("div",Ut,[s[20]||(s[20]=t("div",{class:"section-title font-display",style:{"margin-bottom":"20px"}},"Đổi mật khẩu",-1)),t("div",Pt,[t("div",Nt,[s[17]||(s[17]=t("label",{class:"form-label"},"Mật khẩu hiện tại",-1)),m(t("input",{"onUpdate:modelValue":s[3]||(s[3]=l=>v.value.current=l),class:"form-input",type:"password",placeholder:"••••••••"},null,512),[[_,v.value.current]])]),t("div",Tt,[s[18]||(s[18]=t("label",{class:"form-label"},"Mật khẩu mới",-1)),m(t("input",{"onUpdate:modelValue":s[4]||(s[4]=l=>v.value.newPw=l),class:"form-input",type:"password",placeholder:"••••••••"},null,512),[[_,v.value.newPw]])]),t("div",jt,[s[19]||(s[19]=t("label",{class:"form-label"},"Xác nhận mật khẩu",-1)),m(t("input",{"onUpdate:modelValue":s[5]||(s[5]=l=>v.value.confirm=l),class:"form-input",type:"password",placeholder:"••••••••"},null,512),[[_,v.value.confirm]])])]),t("button",{class:"btn-primary",onClick:E},"Đổi mật khẩu")])])}}};export{Lt as default};
|
fronted/dist/assets/Profile-C_lVTmCB.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
.profile-page[data-v-ea3c6b44]{max-width:900px}.profile-grid[data-v-ea3c6b44]{display:grid;grid-template-columns:1fr 1.4fr;gap:20px}.card[data-v-ea3c6b44]{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--shadow-sm)}.profile-card[data-v-ea3c6b44]{padding:28px}.avatar-section[data-v-ea3c6b44]{display:flex;align-items:center;gap:16px;margin-bottom:16px}.avatar-wrapper[data-v-ea3c6b44]{position:relative;width:72px;height:72px;border-radius:50%;flex-shrink:0}.avatar-img[data-v-ea3c6b44]{width:72px;height:72px;border-radius:50%;-o-object-fit:cover;object-fit:cover;display:block}.avatar-upload-overlay[data-v-ea3c6b44]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;background:#00000073;display:flex;align-items:center;justify-content:center;color:#fff;opacity:0;cursor:pointer;transition:opacity .18s}.avatar-wrapper:hover .avatar-upload-overlay[data-v-ea3c6b44]{opacity:1}.upload-status[data-v-ea3c6b44]{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ink3);margin-bottom:8px}.user-name[data-v-ea3c6b44]{font-size:18px;font-weight:600;color:var(--ink)}.user-email[data-v-ea3c6b44]{font-size:13px;color:var(--ink3);margin-top:2px}.user-level-badge[data-v-ea3c6b44]{margin-top:6px;font-size:11px;font-weight:600;color:var(--green);display:flex;align-items:center;gap:4px}.stats-row[data-v-ea3c6b44]{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;padding-top:20px;border-top:1px solid var(--border);margin-top:16px}.stat-box[data-v-ea3c6b44]{text-align:center}.stat-val[data-v-ea3c6b44]{font-size:24px;font-weight:700;color:var(--ink)}.stat-label[data-v-ea3c6b44]{font-size:11px;color:var(--ink3);margin-top:2px}.section-title[data-v-ea3c6b44]{font-size:16px;font-weight:600;color:var(--ink)}.form-group[data-v-ea3c6b44]{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}.form-label[data-v-ea3c6b44]{font-size:12px;font-weight:600;color:var(--ink2)}.form-row[data-v-ea3c6b44]{display:grid;grid-template-columns:1fr 1fr;gap:12px}.form-input[data-v-ea3c6b44]{padding:9px 12px;border:1.5px solid var(--border2);border-radius:var(--r-sm);font-size:13px;font-family:inherit;color:var(--ink);background:var(--bg);outline:none;transition:border-color .18s}.form-input[data-v-ea3c6b44]:focus{border-color:var(--green-l)}.form-input[data-v-ea3c6b44]:disabled{opacity:.5;cursor:not-allowed}.form-actions[data-v-ea3c6b44]{display:flex;justify-content:space-between;margin-top:20px}.btn-primary[data-v-ea3c6b44]{padding:9px 20px;border-radius:var(--r-sm);background:var(--green);color:#fff;font-size:13px;font-weight:600;border:none;cursor:pointer;font-family:inherit;transition:all .18s}.btn-primary[data-v-ea3c6b44]:hover{background:#245c42}.btn-primary[data-v-ea3c6b44]:disabled{opacity:.6;cursor:not-allowed}.btn-danger[data-v-ea3c6b44]{padding:9px 20px;border-radius:var(--r-sm);background:transparent;color:var(--rose);font-size:13px;font-weight:600;border:1px solid var(--rose-l);cursor:pointer;font-family:inherit;transition:all .18s}.btn-danger[data-v-ea3c6b44]:hover{background:var(--rose-bg)}.error-msg[data-v-ea3c6b44]{margin-top:10px;font-size:13px;color:var(--rose)}.success-msg[data-v-ea3c6b44]{margin-top:10px;font-size:13px;color:var(--green)}.avatar-info[data-v-ea3c6b44]{flex:1;min-width:0}
|
|
|
|
|
|
fronted/dist/assets/Profile-D5X-3cOn.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{_ as q,u as D,j as f,q as X,c,a as t,e as r,t as n,g as P,f as g,w as O,d as m,v as _,m as R,F as $,r as G,z as b,l as J,o as p}from"./index-BnN_E-ke.js";import{u as K}from"./ielts-lE7MGpiZ.js";const Q={class:"profile-page"},W={class:"profile-grid"},Y={class:"card profile-card"},Z={class:"avatar-section"},tt={class:"avatar-wrapper"},st=["src","alt"],et={class:"avatar-upload-overlay",title:"Đổi ảnh đại diện"},at=["disabled"],ot={class:"avatar-info"},lt={class:"user-name font-display"},nt={class:"user-email"},it={class:"user-level-badge"},rt={key:0,class:"upload-status"},dt={key:1,class:"error-msg text-[12px]"},ut={key:2,class:"success-msg text-[12px]"},ct={class:"stats-row"},pt={class:"stat-box"},vt={class:"stat-val font-display"},ft={class:"stat-box"},mt={class:"stat-val font-display"},ht={class:"stat-box"},gt={class:"stat-val font-display"},_t={class:"card",style:{padding:"24px"}},bt={class:"form-group"},yt={class:"form-group"},wt=["value"],xt={class:"form-row"},kt={class:"form-group"},Bt=["value"],St={class:"form-group"},Ct={class:"form-actions"},Mt=["disabled"],Vt={key:0,class:"error-msg"},Pt={key:1,class:"success-msg"},Ut={class:"card",style:{padding:"24px","margin-top":"20px"}},Nt={class:"form-row"},Tt={class:"form-group"},jt={class:"form-group"},At={class:"form-group"},zt={__name:"Profile",setup(Ft){var k,B,S;const e=D(),d=K(),U=J(),y=f(!1),N=b(()=>{var a;return((a=e.profile)==null?void 0:a.avatar_url)||"/icon_profile.jpg"}),w=f(!1),h=f(""),x=f(!1);async function T(a){var u;const s=(u=a.target.files)==null?void 0:u[0];if(!s)return;if(s.size>2*1024*1024){h.value="Ảnh không được vượt quá 2MB";return}h.value="",w.value=!0;const l=await e.uploadAvatar(s);w.value=!1,l?(x.value=!0,setTimeout(()=>{x.value=!1},3e3)):h.value=e.error||"Upload thất bại"}const j=b(()=>{var s;return(((s=e.profile)==null?void 0:s.full_name)||"U").split(" ").map(l=>l[0]).join("").toUpperCase().slice(0,2)}),A=b(()=>d.history.length||0),z=b(()=>{const a=Object.values(d.bandScores).filter(Boolean);return a.length?(a.reduce((s,l)=>s+l,0)/a.length).toFixed(1):"—"}),F=b(()=>{const a=d.history.reduce((s,l)=>{const u=parseInt(l.duration)||0;return s+u},0);return a<60?"0h":`${Math.round(a/3600)}h`}),H=[5,5.5,6,6.5,7,7.5,8,8.5,9],i=f({full_name:((k=e.profile)==null?void 0:k.full_name)||"",target_band:((B=e.profile)==null?void 0:B.target_band)||7,exam_date:((S=e.profile)==null?void 0:S.exam_date)||""}),v=f({current:"",newPw:"",confirm:""});async function L(){await e.updateProfile(i.value)&&(y.value=!0,setTimeout(()=>y.value=!1,3e3))}function E(){alert("Tính năng đổi mật khẩu sẽ được tích hợp sau!")}function I(){e.logout(),U.push("/login")}return X(async()=>{var a,s,l;e.profile||await e.fetchProfile(),i.value.full_name=((a=e.profile)==null?void 0:a.full_name)||"",i.value.target_band=((s=e.profile)==null?void 0:s.target_band)||7,i.value.exam_date=((l=e.profile)==null?void 0:l.exam_date)||"",d.history.length||await d.fetchHistory(),d.bandScores.reading||await d.fetchStats()}),(a,s)=>{var l,u,C,M,V;return p(),c("div",Q,[t("div",W,[t("div",Y,[t("div",Z,[t("div",tt,[t("img",{src:N.value,alt:j.value,class:"avatar-img"},null,8,st),t("label",et,[s[6]||(s[6]=t("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("path",{d:"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"}),t("circle",{cx:"12",cy:"13",r:"4"})],-1)),t("input",{type:"file",accept:"image/*",class:"hidden",onChange:T,disabled:r(e).loading},null,40,at)])]),t("div",ot,[t("div",lt,n(((l=r(e).profile)==null?void 0:l.full_name)||"Người dùng"),1),t("div",nt,n((u=r(e).profile)==null?void 0:u.email),1),t("div",it,[s[7]||(s[7]=t("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[t("path",{d:"M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"})],-1)),P(" "+n(((C=r(e).profile)==null?void 0:C.streak)??0)+" ngày streak · "+n((((M=r(e).profile)==null?void 0:M.xp)??0).toLocaleString("vi-VN"))+" XP ",1)])])]),w.value?(p(),c("div",rt,[...s[8]||(s[8]=[t("svg",{class:"animate-spin",width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("circle",{cx:"12",cy:"12",r:"10"}),t("path",{d:"M22 12a10 10 0 0 1-10 10"})],-1),P(" Đang tải ảnh... ",-1)])])):g("",!0),h.value?(p(),c("div",dt,n(h.value),1)):g("",!0),x.value?(p(),c("div",ut,"✅ Cập nhật ảnh thành công!")):g("",!0),t("div",ct,[t("div",pt,[t("div",vt,n(A.value),1),s[9]||(s[9]=t("div",{class:"stat-label"},"Bài đã làm",-1))]),t("div",ft,[t("div",mt,n(z.value),1),s[10]||(s[10]=t("div",{class:"stat-label"},"Band trung bình",-1))]),t("div",ht,[t("div",gt,n(F.value),1),s[11]||(s[11]=t("div",{class:"stat-label"},"Tổng thời gian",-1))])])]),t("div",_t,[s[16]||(s[16]=t("div",{class:"section-title font-display",style:{"margin-bottom":"20px"}},"Chỉnh sửa thông tin",-1)),t("form",{onSubmit:O(L,["prevent"])},[t("div",bt,[s[12]||(s[12]=t("label",{class:"form-label"},"Họ và tên",-1)),m(t("input",{"onUpdate:modelValue":s[0]||(s[0]=o=>i.value.full_name=o),class:"form-input",type:"text",placeholder:"Nhập họ và tên"},null,512),[[_,i.value.full_name]])]),t("div",yt,[s[13]||(s[13]=t("label",{class:"form-label"},"Email",-1)),t("input",{value:(V=r(e).profile)==null?void 0:V.email,class:"form-input",type:"email",disabled:""},null,8,wt)]),t("div",xt,[t("div",kt,[s[14]||(s[14]=t("label",{class:"form-label"},"Band mục tiêu",-1)),m(t("select",{"onUpdate:modelValue":s[1]||(s[1]=o=>i.value.target_band=o),class:"form-input"},[(p(),c($,null,G(H,o=>t("option",{key:o,value:o},n(o),9,Bt)),64))],512),[[R,i.value.target_band]])]),t("div",St,[s[15]||(s[15]=t("label",{class:"form-label"},"Ngày thi dự kiến",-1)),m(t("input",{"onUpdate:modelValue":s[2]||(s[2]=o=>i.value.exam_date=o),class:"form-input",type:"date"},null,512),[[_,i.value.exam_date]])])]),t("div",Ct,[t("button",{type:"button",class:"btn-danger",onClick:I},"Đăng xuất"),t("button",{type:"submit",class:"btn-primary",disabled:r(e).loading},n(r(e).loading?"Đang lưu...":"Lưu thay đổi"),9,Mt)]),r(e).error?(p(),c("div",Vt,n(r(e).error),1)):g("",!0),y.value?(p(),c("div",Pt,"✅ Đã lưu thành công!")):g("",!0)],32)])]),t("div",Ut,[s[20]||(s[20]=t("div",{class:"section-title font-display",style:{"margin-bottom":"20px"}},"Đổi mật khẩu",-1)),t("div",Nt,[t("div",Tt,[s[17]||(s[17]=t("label",{class:"form-label"},"Mật khẩu hiện tại",-1)),m(t("input",{"onUpdate:modelValue":s[3]||(s[3]=o=>v.value.current=o),class:"form-input",type:"password",placeholder:"••••••••"},null,512),[[_,v.value.current]])]),t("div",jt,[s[18]||(s[18]=t("label",{class:"form-label"},"Mật khẩu mới",-1)),m(t("input",{"onUpdate:modelValue":s[4]||(s[4]=o=>v.value.newPw=o),class:"form-input",type:"password",placeholder:"••••••••"},null,512),[[_,v.value.newPw]])]),t("div",At,[s[19]||(s[19]=t("label",{class:"form-label"},"Xác nhận mật khẩu",-1)),m(t("input",{"onUpdate:modelValue":s[5]||(s[5]=o=>v.value.confirm=o),class:"form-input",type:"password",placeholder:"••••••••"},null,512),[[_,v.value.confirm]])])]),t("button",{class:"btn-primary",onClick:E},"Đổi mật khẩu")])])}}},Et=q(zt,[["__scopeId","data-v-ea3c6b44"]]);export{Et as default};
|
|
|
|
|
|
fronted/dist/assets/{QuizResult-DS1mc336.js → QuizResult-CHzIRTvk.js}
RENAMED
|
@@ -1 +1 @@
|
|
| 1 |
-
import{c as i,a as t,h as d,i as c,e as g,F as k,t as n,r as
|
|
|
|
| 1 |
+
import{c as i,a as t,h as d,i as c,e as g,F as k,t as n,r as B,k as S,z as x,C as z,o as l,g as u,n as p,f}from"./index-CId0hMaT.js";import{u as L}from"./mockQuiz-DEUgOybP.js";import"./mockTestService-ByZrQuTM.js";const q={class:"min-h-screen bg-[var(--bg)] py-8"},M={class:"mx-auto w-full max-w-2xl px-4"},T={key:0,class:"ct-card p-8 text-center"},A={class:"ct-card mb-5 overflow-hidden"},R={class:"p-6"},V={class:"mb-6 flex items-start justify-between gap-4"},I={class:"mt-1 text-base font-bold text-[var(--ink)]"},N={class:"shrink-0 rounded-xl border border-[var(--border)] bg-[var(--bg)] px-4 py-2 text-center"},Q={class:"text-xl font-extrabold text-[var(--ink)]"},$={class:"flex flex-wrap items-center gap-8"},D={class:"relative shrink-0"},E={viewBox:"0 0 120 120",width:"120",height:"120"},F=["stroke-dasharray"],H={class:"absolute inset-0 flex flex-col items-center justify-center"},K={class:"text-2xl font-extrabold text-[var(--ink)]"},U={class:"flex flex-1 flex-wrap gap-3"},X={class:"flex-1 rounded-xl border border-[var(--border)] bg-[#f0fdf4] px-4 py-3 text-center"},G={class:"mt-1 text-2xl font-extrabold text-[#059669]"},J={class:"flex-1 rounded-xl border border-[var(--border)] bg-[var(--rose-bg)] px-4 py-3 text-center"},O={class:"mt-1 text-2xl font-extrabold text-[var(--rose)]"},P={class:"flex-1 rounded-xl border border-[var(--border)] bg-[var(--bg2)] px-4 py-3 text-center"},W={class:"mt-1 text-2xl font-extrabold text-[var(--ink)]"},Y={class:"mb-3 flex items-center justify-between"},Z={class:"rounded-full px-2.5 py-0.5 text-[11px] font-semibold",style:{background:"#d1fae5",color:"#065f46"}},tt={class:"flex flex-col gap-3"},et={class:"flex"},st={class:"flex-1 p-4"},rt={class:"mb-2 flex items-center justify-between gap-3"},ot={class:"text-[11px] font-bold uppercase tracking-wider text-[var(--ink3)]"},nt={key:0,class:"mb-3 text-[13px] leading-relaxed text-[var(--ink)]"},it={class:"rounded-lg border border-[var(--border)] bg-[var(--bg)] p-3 text-[12px]"},lt={class:"flex items-center justify-between gap-2"},at={key:0,class:"mt-1.5 flex items-center justify-between gap-2 border-t border-[var(--border)] pt-1.5"},dt={class:"font-semibold font-mono",style:{color:"#059669"}},ct={key:1,class:"mt-3"},xt=["innerHTML"],ut={class:"mt-6 flex flex-wrap justify-center gap-3"},mt={__name:"QuizResult",setup(vt){const b=z(),_=L(),r=x(()=>_.result),v=x(()=>r.value?r.value.correct!=null?r.value.correct:(r.value.detailed||[]).filter(s=>s.isCorrect).length:0),y=x(()=>{var s;return(((s=r.value)==null?void 0:s.total)??0)-v.value}),m=x(()=>{var e;const s=((e=r.value)==null?void 0:e.total)??0;return s?Math.round(v.value/s*100):0}),w=x(()=>{var s;return((s=r.value)==null?void 0:s.detailed)??[]});function C(s){return Array.isArray(s)?s.join(", "):s!=null?String(s):"—"}function j(s){return Array.isArray(s.correct_answers)&&s.correct_answers.length?s.correct_answers.join(" / "):s.correct_answer?String(s.correct_answer):"—"}return(s,e)=>{const a=S("RouterLink");return l(),i("div",q,[t("div",M,[d(a,{to:"/dashboard",class:"mb-5 inline-flex items-center gap-1.5 text-[12px] text-[var(--ink3)] hover:text-[var(--ink)] transition-colors"},{default:c(()=>[...e[0]||(e[0]=[t("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[t("polyline",{points:"15 18 9 12 15 6"})],-1),u(" Về trang chủ ",-1)])]),_:1}),r.value?(l(),i(k,{key:1},[t("div",A,[e[10]||(e[10]=t("div",{class:"h-1 w-full",style:{background:"#34d399"}},null,-1)),t("div",R,[t("div",V,[t("div",null,[e[3]||(e[3]=t("div",{class:"text-[11px] font-bold uppercase tracking-wider text-[var(--ink3)]"},"Kết quả bài thi",-1)),t("div",I,n(r.value.title||"IELTS Mock Test"),1)]),t("div",N,[e[4]||(e[4]=t("div",{class:"text-[10px] text-[var(--ink3)]"},"Band ước tính",-1)),t("div",Q,n(r.value.estimatedBand??"—"),1)])]),t("div",$,[t("div",D,[(l(),i("svg",E,[e[5]||(e[5]=t("circle",{cx:"60",cy:"60",r:"50",fill:"none",stroke:"#f3f4f6","stroke-width":"10"},null,-1)),t("circle",{cx:"60",cy:"60",r:"50",fill:"none",stroke:"#34d399","stroke-width":"10","stroke-linecap":"round","stroke-dasharray":`${m.value*3.14} 314`,transform:"rotate(-90 60 60)",style:{transition:"stroke-dasharray 1.2s ease"}},null,8,F)])),t("div",H,[t("span",K,n(m.value)+"%",1),e[6]||(e[6]=t("span",{class:"text-[10px] text-[var(--ink3)]"},"Score",-1))])]),t("div",U,[t("div",X,[e[7]||(e[7]=t("div",{class:"text-[10px] font-semibold uppercase tracking-wide text-[#059669]"},"Đúng",-1)),t("div",G,n(r.value.correct??v.value),1)]),t("div",J,[e[8]||(e[8]=t("div",{class:"text-[10px] font-semibold uppercase tracking-wide text-[var(--rose)]"},"Sai",-1)),t("div",O,n(y.value),1)]),t("div",P,[e[9]||(e[9]=t("div",{class:"text-[10px] font-semibold uppercase tracking-wide text-[var(--ink3)]"},"Tổng",-1)),t("div",W,n(r.value.total),1)])])])])]),t("div",Y,[e[11]||(e[11]=t("span",{class:"text-[13px] font-bold text-[var(--ink)]"},"Review đáp án",-1)),t("span",Z,n(r.value.correct??v.value)+" đúng",1)]),t("div",tt,[(l(!0),i(k,null,B(w.value,(o,h)=>(l(),i("div",{key:o.questionId||h,class:"ct-card overflow-hidden"},[t("div",et,[t("div",{class:"w-1 shrink-0 rounded-l-xl",style:p(o.isCorrect?"background:#34d399":"background:#f43f5e")},null,4),t("div",st,[t("div",rt,[t("span",ot," Câu "+n(o.order||h+1),1),t("span",{class:"rounded-full px-2 py-0.5 text-[10px] font-bold uppercase",style:p(o.isCorrect?"background:#d1fae5; color:#065f46":"background:#ffe4e6; color:#be123c")},n(o.isCorrect?"✓ Đúng":"✗ Sai"),5)]),o.text?(l(),i("p",nt,n(o.text),1)):f("",!0),t("div",it,[t("div",lt,[e[12]||(e[12]=t("span",{class:"text-[var(--ink3)]"},"Đáp án của tôi",-1)),t("span",{class:"font-semibold font-mono",style:p(o.isCorrect?"color:#059669":"color:#e11d48")},n(C(o.userAnswer)),5)]),o.isCorrect?f("",!0):(l(),i("div",at,[e[13]||(e[13]=t("span",{class:"text-[var(--ink3)]"},"Đáp án đúng",-1)),t("span",dt,n(j(o)),1)]))]),o.explain?(l(),i("details",ct,[e[14]||(e[14]=t("summary",{class:"cursor-pointer text-[12px] font-medium",style:{color:"#7c3aed"}},"Xem giải thích",-1)),t("div",{class:"prose mt-2 max-w-none text-[13px]",innerHTML:o.explain},null,8,xt)])):f("",!0)])])]))),128))]),t("div",ut,[d(a,{to:"/dashboard",class:"ct-btn"},{default:c(()=>[...e[15]||(e[15]=[t("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("rect",{x:"3",y:"3",width:"7",height:"7"}),t("rect",{x:"14",y:"3",width:"7",height:"7"}),t("rect",{x:"14",y:"14",width:"7",height:"7"}),t("rect",{x:"3",y:"14",width:"7",height:"7"})],-1),u(" Dashboard ",-1)])]),_:1}),d(a,{to:`/quiz/${g(b).params.quizId}`,class:"ct-btn"},{default:c(()=>[...e[16]||(e[16]=[t("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("polyline",{points:"1 4 1 10 7 10"}),t("path",{d:"M3.51 15a9 9 0 1 0 .49-3.5"})],-1),u(" Làm lại ",-1)])]),_:1},8,["to"]),d(a,{to:"/history",class:"ct-btn"},{default:c(()=>[...e[17]||(e[17]=[t("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[t("circle",{cx:"12",cy:"12",r:"10"}),t("polyline",{points:"12 6 12 12 16 14"})],-1),u(" Lịch sử ",-1)])]),_:1})])],64)):(l(),i("div",T,[e[2]||(e[2]=t("div",{class:"mb-2 text-base font-semibold text-[var(--ink)]"},"Chưa có kết quả",-1)),d(a,{to:`/quiz/${g(b).params.quizId}`,class:"ct-btn mt-4 inline-flex"},{default:c(()=>[...e[1]||(e[1]=[u("Quay lại bài",-1)])]),_:1},8,["to"])]))])])}}};export{mt as default};
|
fronted/dist/assets/QuizRunner-DCdBBWGG.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{o as s,c as n,a as e,t as v,z as S,E as Ge,p as X,j as w,q as pt,e as _,d as oe,m as ft,g as L,f as M,F as V,y as N,H as ut,s as xt,v as Qe,r as D,w as ge,n as Be,A as qe,b as St,Q as Tt,I as It,h as U,i as ae,T as Le,x as J,D as Ue,N as Re,M as Fe,R as Mt,C as At,k as Nt,l as zt}from"./index-CId0hMaT.js";import{_ as Et,u as Lt,a as Bt,b as dt,s as jt,i as Ot,c as Pt}from"./scoring-MUedn-Ow.js";import{_ as Ht,a as Rt,b as Ee,c as Vt,d as Ut,e as Ft,f as Gt}from"./AudioPlayer-RR1aaAJE.js";import{u as Qt,i as Dt,b as Kt,e as Xt}from"./mockQuiz-DEUgOybP.js";import{u as Wt}from"./practice-nWRX1lS1.js";import{m as Jt}from"./vocabularyService-4YeelBKD.js";import"./vocabTopicPreference-CKYmd6x1.js";import"./mockTestService-ByZrQuTM.js";const Yt={class:"sticky top-0 z-20 border-b border-[var(--border2)] bg-[var(--bg)]/90 backdrop-blur"},Zt={class:"exam-container flex items-center justify-between gap-3 py-3 sm:py-4"},es={class:"min-w-0"},ts={class:"text-sm font-semibold truncate"},ss={class:"text-xs text-[var(--ink2)] truncate"},ns={class:"flex items-center gap-3"},rs={class:"rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-3 py-2"},os={class:"font-mono text-sm font-semibold"},is=["disabled"],as={__name:"ExamHeader",props:{title:{type:String,default:""},subtitle:{type:String,default:""},remainingSeconds:{type:Number,default:0},disabled:{type:Boolean,default:!1}},emits:["submit"],setup(a){const p=a,k=S(()=>{const r=Math.max(0,p.remainingSeconds||0),l=Math.floor(r/60).toString().padStart(2,"0"),o=Math.floor(r%60).toString().padStart(2,"0");return`${l}:${o}`});return(r,l)=>(s(),n("div",Yt,[e("div",Zt,[e("div",es,[e("div",ts,v(a.title),1),e("div",ss,v(a.subtitle),1)]),e("div",ns,[e("div",rs,[l[1]||(l[1]=e("div",{class:"text-[11px] text-[var(--ink2)]"},"Time left",-1)),e("div",os,v(k.value),1)]),e("button",{class:"btn btn-primary",onClick:l[0]||(l[0]=o=>r.$emit("submit")),disabled:a.disabled}," Submit ",8,is)])])]))}};function ls(){const a=w(null),p=w(!1),k=w(0),r=w(0),l=w(1);let o=null;function y(){clearTimeout(o),o=setTimeout(()=>{a.value&&(k.value=a.value.currentTime||0)},50)}function x(){p.value=!0}function u(){p.value=!1}function g(){a.value&&(r.value=a.value.duration||0)}const m={play:x,pause:u,timeupdate:y,loadedmetadata:g,durationchange:g};function f(b){b&&(a.value=b,Object.entries(m).forEach(([c,q])=>b.addEventListener(c,q)),b.playbackRate=l.value)}function d(){clearTimeout(o);const b=a.value;b&&Object.entries(m).forEach(([c,q])=>b.removeEventListener(c,q))}Ge(d);function T(){const b=a.value;b&&(b.paused?b.play().catch(()=>{}):b.pause())}function z(b){const c=a.value;c&&(c.currentTime=Math.max(0,Math.min(c.duration||0,(c.currentTime||0)+b)))}function H(b){const c=a.value;c&&(c.currentTime=Math.max(0,Math.min(c.duration||0,Number(b)||0)))}function G(b){var c;H(b),(c=a.value)==null||c.play().catch(()=>{})}function ne(b){const c=Math.max(0,Number(b)||0);return`${Math.floor(c/60).toString().padStart(2,"0")}:${Math.floor(c%60).toString().padStart(2,"0")}`}return X(l,b=>{a.value&&(a.value.playbackRate=b)}),{audioEl:a,playing:p,currentTime:k,duration:r,playbackRate:l,attach:f,detach:d,toggle:T,seekTo:H,seekDelta:z,seekAndPlay:G,fmt:ne}}const us={class:"card p-4"},ds={class:"flex items-start justify-between gap-3"},cs={class:"text-sm font-semibold"},vs={class:"text-xs text-[var(--ink2)]"},ps={class:"text-xs font-mono text-[var(--ink2)]"},fs=["src"],xs={class:"mt-3 flex items-center gap-2"},gs={class:"ml-auto flex items-center gap-2"},ms=["max","value"],bs={key:0,class:"mt-3 text-xs text-[var(--ink2)]"},hs={__name:"ExamAudioPlayer",props:{src:{type:String,default:""},title:{type:String,default:"Audio"},subtitle:{type:String,default:""},seekTo:{type:Number,default:null}},emits:["time"],setup(a,{expose:p,emit:k}){const r=a,l=k,o=ls(),y=w(null);pt(()=>o.attach(y.value)),X(o.currentTime,u=>l("time",u)),X(()=>r.seekTo,u=>{u!=null&&o.seekTo(u)});function x(u){o.seekAndPlay(u)}return p({seekAndPlay:x}),(u,g)=>(s(),n("div",us,[e("div",ds,[e("div",null,[e("div",cs,v(a.title),1),e("div",vs,v(a.subtitle),1)]),e("div",ps,v(_(o).fmt(_(o).currentTime.value))+" / "+v(_(o).fmt(_(o).duration.value)),1)]),e("audio",{ref_key:"nativeAudio",ref:y,src:a.src,preload:"metadata",class:"hidden"},null,8,fs),e("div",xs,[e("button",{class:"btn btn-secondary",onClick:g[0]||(g[0]=m=>_(o).seekDelta(-5))},"-5s"),e("button",{class:"btn btn-primary",onClick:g[1]||(g[1]=m=>_(o).toggle())},v(_(o).playing.value?"Pause":"Play"),1),e("button",{class:"btn btn-secondary",onClick:g[2]||(g[2]=m=>_(o).seekDelta(5))},"+5s"),e("div",gs,[g[6]||(g[6]=e("div",{class:"text-xs text-[var(--ink2)]"},"Speed",-1)),oe(e("select",{class:"rounded-lg border border-[var(--border2)] bg-[var(--surface)] px-2 py-1 text-xs","onUpdate:modelValue":g[3]||(g[3]=m=>_(o).playbackRate.value=m)},[...g[5]||(g[5]=[e("option",{value:.75},"0.75x",-1),e("option",{value:1},"1.0x",-1),e("option",{value:1.25},"1.25x",-1),e("option",{value:1.5},"1.5x",-1)])],512),[[ft,_(o).playbackRate.value,void 0,{number:!0}]])])]),e("input",{class:"mt-3 w-full",type:"range",min:"0",max:Math.max(0,_(o).duration.value),step:"0.01",value:_(o).currentTime.value,onInput:g[4]||(g[4]=m=>_(o).seekTo(Number(m.target.value)))},null,40,ms),a.src?M("",!0):(s(),n("div",bs,[...g[7]||(g[7]=[L(" Chưa cấu hình audio CDN. Hãy set ",-1),e("code",null,"VITE_AUDIO_CDN_BASE",-1),L(" trong ",-1),e("code",null,"fronted/.env",-1),L(". ",-1)])]))]))}},ys={class:"card overflow-hidden"},ks={class:"flex items-center justify-between px-4 py-3 border-b border-[var(--border)]"},ws={class:"rounded-full bg-[var(--bg2)] px-2 py-0.5 text-[10px] text-[var(--ink3)]"},_s={class:"flex items-center gap-2"},$s={class:"font-mono text-[10px] text-[var(--ink3)]"},Cs=["title"],qs={key:0,width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},Ss={key:1,width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},Ts={class:"border-b border-[var(--border)] px-3 py-2"},Is={class:"relative"},Ms={key:0,class:"py-6 text-center text-[12px] text-[var(--ink3)]"},As=["onClick"],Ns={class:"mt-0.5 shrink-0 font-mono text-[10px] text-[var(--ink3)]"},zs=["innerHTML"],Es=["onClick"],Ls={__name:"TranscriptPanel",props:{paragraphs:{type:Array,default:()=>[]},currentTime:{type:Number,default:0},highlightedIds:{type:Object,default:()=>new Set}},emits:["seek","save-word"],setup(a){const p=a,k=w(!0),r=w(!1),l=w(null),o=new Map,y=w(!1),x=w(""),u=w(null);function g(){y.value=!y.value,y.value?qe(()=>{var b;return(b=u.value)==null?void 0:b.focus()}):m()}function m(){x.value="",y.value=!1}function f(b){if(!x.value)return b;const c=x.value.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),q=new RegExp(`(${c})`,"gi");return b.replace(q,'<mark style="background:#fef08a;border-radius:2px;padding:0 1px">$1</mark>')}const d=S(()=>{const b=[];for(const c of p.paragraphs)for(const q of c.children||[])!Number.isFinite(q.from)||!Number.isFinite(q.to)||b.push({id:q.id,from:q.from,to:q.to,speaker:q.speaker,text:q.text});return b}),T=S(()=>{if(!x.value)return d.value;const b=x.value.toLowerCase();return d.value.filter(c=>c.text.toLowerCase().includes(b))}),z=S(()=>{var c;const b=p.currentTime||0;return((c=d.value.find(q=>b>=q.from&&b<=q.to))==null?void 0:c.id)??null});function H(b){return p.highlightedIds.size>0?p.highlightedIds.has(b.id):b.id===z.value}async function G(b){if(!b||!k.value)return;await qe();const c=o.get(b);c&&c.scrollIntoView({behavior:"smooth",block:"nearest"})}X(()=>p.highlightedIds,async b=>{if(b.size>0){const c=[...b][0];await G(c)}}),X(z,b=>{p.highlightedIds.size===0&&G(b)});function ne(b){const c=Math.max(0,Number(b)||0),q=Math.floor(c/60).toString().padStart(2,"0"),ee=Math.floor(c%60).toString().padStart(2,"0");return`${q}:${ee}`}return(b,c)=>(s(),n("div",ys,[e("div",ks,[e("button",{class:"flex items-center gap-2 text-left",onClick:c[0]||(c[0]=q=>k.value=!k.value)},[c[4]||(c[4]=e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round"},[e("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})],-1)),c[5]||(c[5]=e("span",{class:"text-[12px] font-semibold text-[var(--ink)]"},"Transcript",-1)),e("span",ws,[L(v(T.value.length),1),x.value?(s(),n(V,{key:0},[L(" / "+v(d.value.length),1)],64)):M("",!0)])]),e("div",_s,[e("span",$s,v(ne(a.currentTime)),1),e("button",{class:N(["flex h-6 w-6 items-center justify-center rounded transition-colors",y.value?"bg-[#34d399] text-white":"text-[var(--ink3)] hover:bg-[var(--bg2)]"]),title:"Tìm kiếm trong transcript",onClick:g},[...c[6]||(c[6]=[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("circle",{cx:"11",cy:"11",r:"8"}),e("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})],-1)])],2),e("button",{class:"flex h-6 w-6 items-center justify-center rounded text-[var(--ink3)] hover:bg-[var(--bg2)] transition-colors",title:r.value?"Thu nhỏ transcript":"Mở rộng transcript",onClick:c[1]||(c[1]=q=>r.value=!r.value)},[r.value?(s(),n("svg",Ss,[...c[8]||(c[8]=[e("path",{d:"M4 14h6m0 0v6m0-6l-7 7m17-11h-6m0 0V4m0 6l7-7"},null,-1)])])):(s(),n("svg",qs,[...c[7]||(c[7]=[e("path",{d:"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"},null,-1)])]))],8,Cs),e("button",{onClick:c[2]||(c[2]=q=>k.value=!k.value),class:"flex h-6 w-6 items-center justify-center rounded text-[var(--ink3)] hover:bg-[var(--bg2)]"},[(s(),n("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2",class:N(["transition-transform",k.value?"rotate-180":""])},[...c[9]||(c[9]=[e("polyline",{points:"6 9 12 15 18 9"},null,-1)])],2))])])]),oe(e("div",Ts,[e("div",Is,[c[11]||(c[11]=e("svg",{class:"absolute left-2.5 top-1/2 -translate-y-1/2 text-[var(--ink3)]",width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("circle",{cx:"11",cy:"11",r:"8"}),e("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})],-1)),oe(e("input",{ref_key:"searchInputRef",ref:u,"onUpdate:modelValue":c[3]||(c[3]=q=>x.value=q),class:"w-full rounded-lg border border-[var(--border2)] bg-[var(--bg)] py-1.5 pl-7 pr-3 text-[12px] outline-none focus:border-[#34d399]",placeholder:"Tìm từ trong transcript...",onKeydown:xt(m,["escape"])},null,544),[[Qe,x.value]]),x.value?(s(),n("button",{key:0,onClick:m,class:"absolute right-2.5 top-1/2 -translate-y-1/2 text-[var(--ink3)] hover:text-[var(--ink)]"},[...c[10]||(c[10]=[e("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"})],-1)])])):M("",!0)])],512),[[ut,y.value&&k.value]]),oe(e("div",{class:"overflow-y-auto border-t border-[var(--border)] px-3 py-2 transition-all",style:Be({height:r.value?"calc(100vh - 380px)":"13rem",maxHeight:r.value?"none":"13rem"}),ref_key:"listEl",ref:l},[!T.value.length&&x.value?(s(),n("div",Ms,' Không tìm thấy "'+v(x.value)+'" trong transcript. ',1)):M("",!0),(s(!0),n(V,null,D(T.value,q=>(s(),n("div",{key:q.id,ref_for:!0,ref:ee=>{ee&&_(o).set(q.id,ee)},class:N(["group mb-1 flex cursor-pointer gap-2 rounded-lg border px-2 py-1.5 transition-all",H(q)?"border-[#34d399] bg-[#f0fdf4]":"border-transparent hover:bg-[var(--bg)]"]),onClick:ee=>b.$emit("seek",q.from)},[e("span",Ns,v(ne(q.from)),1),e("span",{class:N(["text-[12px] leading-relaxed transition-all",H(q)?"font-semibold text-[var(--ink)]":"text-[var(--ink2)]"]),innerHTML:f(q.text)},null,10,zs),b.$attrs.onSaveWord?(s(),n("button",{key:0,class:"ml-auto shrink-0 opacity-0 group-hover:opacity-100 flex h-5 w-5 items-center justify-center rounded text-[var(--ink3)] hover:bg-[#34d399] hover:text-white transition-all",title:"Lưu từ vựng",onClick:ge(ee=>b.$emit("save-word",q.text),["stop"])},[...c[12]||(c[12]=[e("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e("line",{x1:"5",y1:"12",x2:"19",y2:"12"})],-1)])],8,Es)):M("",!0)],10,As))),128))],4),[[ut,k.value]])]))}},Bs={class:"overflow-hidden rounded-xl border border-[var(--border)] bg-white"},js={class:"flex items-center overflow-x-auto px-3 py-2.5 gap-1 scrollbar-hide"},Os=["onClick"],Ps={class:"text-[10px] font-normal opacity-70"},Hs=["onClick"],Rs={class:"flex items-center gap-4 border-t border-[var(--border)] px-3 py-1.5 text-[10px] text-[var(--ink3)]"},Vs={class:"ml-auto font-medium text-[var(--ink2)]"},ct={__name:"QuestionNavGrid",props:{questions:{type:Array,default:()=>[]},navParts:{type:Array,default:()=>[]},currentOrder:{type:Number,default:0},answeredMap:{type:Object,default:()=>({})}},emits:["go"],setup(a){const p=a,k=S(()=>{var y;return(y=p.navParts)!=null&&y.length?p.navParts.map(x=>({partId:x.id,label:x.label,questions:x.questions,hasCurrent:x.questions.some(u=>u.order===p.currentOrder),answered:x.questions.filter(u=>{var g;return((g=p.answeredMap)==null?void 0:g[u.id])!==void 0}).length})):[{partId:0,label:"Câu hỏi",questions:p.questions,hasCurrent:p.questions.some(x=>x.order===p.currentOrder),answered:p.questions.filter(x=>{var u;return((u=p.answeredMap)==null?void 0:u[x.id])!==void 0}).length}]}),r=S(()=>p.questions.length||k.value.reduce((y,x)=>y+x.questions.length,0)),l=S(()=>Object.keys(p.answeredMap||{}).length);function o(y){var g;const x=y.order===p.currentOrder,u=((g=p.answeredMap)==null?void 0:g[y.id])!==void 0;return x?"bg-[#111] border-[#111] text-white":u?"bg-[#f0fdf4] border-[#34d399] text-[#065f46]":"bg-white border-[var(--border2)] text-[var(--ink3)] hover:border-[var(--ink2)]"}return(y,x)=>(s(),n("div",Bs,[e("div",js,[(s(!0),n(V,null,D(k.value,u=>(s(),n(V,{key:u.partId},[e("button",{class:N(["shrink-0 flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-[12px] font-semibold transition-colors whitespace-nowrap",u.hasCurrent?"border-[#111] bg-[#111] text-white":"border-[var(--border2)] bg-white text-[var(--ink)]"]),onClick:g=>{var m;return y.$emit("go",(m=u.questions[0])==null?void 0:m.order)}},[L(v(u.label)+" ",1),e("span",Ps,v(u.answered)+"/"+v(u.questions.length),1)],10,Os),(s(!0),n(V,null,D(u.questions,g=>(s(),n("button",{key:g.order,class:N(["shrink-0 flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-semibold transition-colors border",o(g)]),onClick:m=>y.$emit("go",g.order)},v(g.order),11,Hs))),128))],64))),128))]),e("div",Rs,[x[0]||(x[0]=St('<div class="flex items-center gap-1"><span class="inline-block h-3 w-3 rounded-full border border-[var(--border2)] bg-white"></span> Chưa làm </div><div class="flex items-center gap-1"><span class="inline-block h-3 w-3 rounded-full border border-[#34d399] bg-[#f0fdf4]"></span> Đã làm </div><div class="flex items-center gap-1"><span class="inline-block h-3 w-3 rounded-full bg-[#111]"></span> Đang làm </div>',3)),e("div",Vs,v(l.value)+"/"+v(r.value)+" câu",1)])]))}},Us={class:"flex items-start gap-2"},Fs={class:"flex shrink-0 items-center gap-1.5"},Gs=["title"],Qs={class:"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--bg2)] text-[11px] font-bold text-[var(--ink2)]"},Ds={key:0,class:"text-sm font-semibold"},Ks=["innerHTML"],Xs={key:0,class:"mt-3"},Ws={class:"grid gap-2"},Js=["name","value"],Ys={class:"text-sm"},Zs={class:"font-semibold mr-2"},en={key:1,class:"mt-3"},tn={class:"text-xs text-[var(--ink2)] mb-2"},sn={class:"grid gap-2"},nn=["value","disabled"],rn={class:"text-sm"},on={class:"font-semibold mr-2"},an={key:2,class:"mt-3"},ln=["value"],un={key:3,class:"mt-5"},dn={class:"mt-1 text-[13px] text-[var(--ink2)]"},cn={class:"flex flex-col items-center gap-1.5"},vn=["width","height"],pn={key:1,width:"14",height:"14",viewBox:"0 0 24 24",fill:"currentColor"},fn={key:0,class:"text-[10px] font-medium text-[#059669]"},xn=["src"],gn={key:1,class:"mt-2 rounded-lg border border-[#f59e0b33] bg-[#fef9ee] px-3 py-2 text-[11px] text-[#b45309]"},mn=["disabled"],bn={key:4,class:"mt-3"},hn={class:"text-xs text-[var(--ink2)]"},xe=2,Ve={__name:"QuestionRenderer",props:{item:{type:Object,required:!0},answer:{type:[String,Array],default:void 0},isCurrent:{type:Boolean,default:!1},speakingCompact:{type:Boolean,default:!1}},emits:["update:answer","jump-audio","evaluate-speaking"],setup(a,{emit:p}){const k=a,r=p,l=S(()=>k.item.question||{}),o=S(()=>k.item.questionSetType||""),y=S(()=>k.item.questionSetOptions||[]),x=S(()=>k.item.questionSetMaxSelections||0),u=S(()=>{const I=String(o.value||l.value.question_type||"").toUpperCase();return I==="SPEAKING"||I==="speaking"||String(l.value.type||"").toLowerCase()==="speaking"?"speaking":I==="MULTIPLE_CHOICE_MANY"?"multi":I==="TABLE_SELECTION"||I==="MATCHING"||I==="MATCHING_FEATURES"||I==="MATCHING_INFO"||I==="MATCHING_HEADING"||I==="MATCHING_HEADINGS"||I==="MATCHING_ENDINGS"?"select":I==="SINGLE_SELECTION"||I==="SINGLE_CHOICE"||I==="MULTIPLE_CHOICE_ONE"?"single":"unknown"}),g=w(!1),m=w(null),f=w(null),d=w(0),T=w(0);let z=null,H=[],G=null;async function ne(){if(g.value){T.value=d.value,z==null||z.stop(),g.value=!1,clearInterval(G);return}try{const I=await navigator.mediaDevices.getUserMedia({audio:!0});H=[],d.value=0,T.value=0,z=new MediaRecorder(I),z.ondataavailable=A=>H.push(A.data),z.onstop=()=>{const A=new Blob(H,{type:"audio/webm"});f.value=A,m.value=URL.createObjectURL(A),I.getTracks().forEach(R=>R.stop()),r("update:answer","recorded")},z.start(),g.value=!0,G=setInterval(()=>d.value++,1e3)}catch(I){console.warn("Microphone access denied:",I)}}Ge(()=>{g.value&&(z==null||z.stop()),clearInterval(G)});const b=S(()=>T.value>=xe);function c(){b.value&&r("evaluate-speaking",{blob:f.value,questionText:l.value.text||l.value.title||"",questionId:l.value.id})}const q=S(()=>Number.isFinite(l.value.listen_from)),ee=S(()=>{const I=Array.isArray(l.value.options)?l.value.options:[],A=Array.isArray(y.value)?y.value:[];return(I.length?I:A).map(K=>({option:K.option,text:K.text}))}),Se=S(()=>(Array.isArray(l.value.options)?l.value.options:[]).map(A=>({option:A.option,text:A.text}))),ve=S(()=>{const I=Array.isArray(l.value.options)?l.value.options:[],A=Array.isArray(y.value)?y.value:[];return(I.length?I:A).map(K=>({option:K.option,text:K.text}))}),W=w(""),Y=w([]);X(()=>k.answer,I=>{Array.isArray(I)?(Y.value=I,W.value=""):typeof I=="string"?(W.value=I,Y.value=[]):(W.value="",Y.value=[])},{immediate:!0}),X(W,I=>{(u.value==="single"||u.value==="select")&&r("update:answer",I)}),X(Y,I=>{u.value==="multi"&&r("update:answer",I)},{deep:!0});function Te(I){return!x.value||Y.value.includes(I)?!1:Y.value.length>=x.value}return(I,A)=>(s(),n("div",{class:N(["p-4",[a.speakingCompact?"rounded-none border-0 bg-transparent p-4 shadow-none":"card",a.isCurrent&&!a.speakingCompact?"ring-2 ring-[rgba(124,106,247,0.35)]":""]])},[e("div",Us,[e("div",Fs,[q.value?(s(),n("button",{key:0,class:"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white transition-colors hover:bg-[#059669]",title:`Play from ${Math.floor(l.value.listen_from)}s`,onClick:A[0]||(A[0]=ge(R=>I.$emit("jump-audio",{time:l.value.listen_from,locateInfo:l.value.locate_info}),["stop"]))},[...A[4]||(A[4]=[e("svg",{width:"8",height:"8",viewBox:"0 0 24 24",fill:"currentColor"},[e("polygon",{points:"5 3 19 12 5 21 5 3"})],-1)])],8,Gs)):M("",!0),e("div",Qs,v(l.value.order),1)]),e("div",{class:N(["flex-1",a.speakingCompact?"[&_.text-sm.font-semibold]:text-lg [&_.text-sm.font-semibold]:font-semibold [&_.text-sm.font-semibold]:leading-normal [&_.text-sm.font-semibold]:text-[var(--ink)]":""])},[l.value.text||l.value.title?(s(),n("div",Ds,v(l.value.text||l.value.title),1)):l.value.content?(s(),n("div",{key:1,class:"text-sm",innerHTML:l.value.content},null,8,Ks)):M("",!0)],2)]),u.value==="single"?(s(),n("div",Xs,[e("div",Ws,[(s(!0),n(V,null,D(ee.value,R=>(s(),n("label",{key:R.option,class:"flex items-center gap-2 rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-3 py-2 cursor-pointer hover:border-[var(--ink3)]"},[oe(e("input",{type:"radio",name:`q_${l.value.id}`,value:R.option,"onUpdate:modelValue":A[1]||(A[1]=K=>W.value=K)},null,8,Js),[[Tt,W.value]]),e("div",Ys,[e("span",Zs,v(R.option),1),e("span",null,v(R.text),1)])]))),128))])])):u.value==="multi"?(s(),n("div",en,[e("div",tn,"Choose "+v(x.value)+" answers",1),e("div",sn,[(s(!0),n(V,null,D(Se.value,R=>(s(),n("label",{key:R.option,class:"flex items-center gap-2 rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-3 py-2 cursor-pointer hover:border-[var(--ink3)]"},[oe(e("input",{type:"checkbox",value:R.option,"onUpdate:modelValue":A[2]||(A[2]=K=>Y.value=K),disabled:Te(R.option)},null,8,nn),[[It,Y.value]]),e("div",rn,[e("span",on,v(R.option),1),e("span",null,v(R.text),1)])]))),128))])])):u.value==="select"?(s(),n("div",an,[A[6]||(A[6]=e("div",{class:"text-xs text-[var(--ink2)] mb-2"},"Select answer",-1)),oe(e("select",{class:"w-full rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-3 py-2","onUpdate:modelValue":A[3]||(A[3]=R=>W.value=R)},[A[5]||(A[5]=e("option",{value:""},"—",-1)),(s(!0),n(V,null,D(ve.value,R=>(s(),n("option",{key:R.option,value:R.option},v(R.option),9,ln))),128))],512),[[ft,W.value]])])):u.value==="speaking"?(s(),n("div",un,[e("div",{class:N(["rounded-xl border border-[var(--border)] bg-[var(--bg)] p-5",a.speakingCompact?"flex flex-col items-center gap-4 text-center":"flex items-center justify-between gap-4"])},[e("div",{class:N(a.speakingCompact?"w-full":"")},[A[7]||(A[7]=e("div",{class:"text-[11px] font-semibold uppercase tracking-wider text-[var(--ink3)]"}," No time limit ",-1)),e("div",dn,v(g.value?`Đang ghi âm... ${d.value}s`:f.value?"Đã ghi xong":"Nhấn micro để bắt đầu ghi âm"),1)],2),e("div",cn,[e("button",{type:"button",onClick:ne,class:N(["flex items-center justify-center rounded-full border-2 transition-all",[a.speakingCompact?"h-16 w-16":"h-12 w-12",g.value?"border-[#e11d48] bg-[#e11d48] text-white animate-pulse":"border-[#34d399] bg-[#f0fdf4] text-[#34d399] hover:bg-[#34d399] hover:text-white"]])},[g.value?(s(),n("svg",pn,[...A[9]||(A[9]=[e("rect",{x:"6",y:"6",width:"12",height:"12",rx:"2"},null,-1)])])):(s(),n("svg",{key:0,width:a.speakingCompact?22:18,height:a.speakingCompact?22:18,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round"},[...A[8]||(A[8]=[e("path",{d:"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"},null,-1),e("path",{d:"M19 10v2a7 7 0 0 1-14 0v-2"},null,-1),e("line",{x1:"12",y1:"19",x2:"12",y2:"23"},null,-1),e("line",{x1:"8",y1:"23",x2:"16",y2:"23"},null,-1)])],8,vn))],2),f.value?(s(),n("div",fn,"✓ Đã ghi")):M("",!0)])],2),m.value?(s(),n("audio",{key:0,src:m.value,controls:"",class:"mt-2 w-full",style:{height:"32px"}},null,8,xn)):M("",!0),f.value&&!g.value&&T.value<xe?(s(),n("div",gn," Bài ghi quá ngắn ("+v(T.value)+"s). Ghi lại ít nhất "+v(xe)+" giây để đánh giá chính xác. ",1)):M("",!0),f.value&&!g.value?(s(),n("button",{key:2,disabled:T.value<xe,class:N(["mt-2 flex w-full items-center justify-center gap-2 rounded-xl border-none py-2.5 text-sm font-semibold text-white transition-all",b.value?"cursor-pointer bg-emerald-400 hover:bg-emerald-600":"cursor-not-allowed bg-emerald-200 opacity-60"]),onClick:c},[A[10]||(A[10]=e("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"}),e("path",{d:"M19 10v2a7 7 0 0 1-14 0v-2"})],-1)),L(" "+v(T.value<xe?`Ghi thêm ${xe-T.value}s...`:"Đánh giá bài nói"),1)],10,mn)):M("",!0)])):(s(),n("div",bn,[e("div",hn,"Unsupported question type: "+v(l.value.question_type),1)]))],2))}},yn={class:"flex items-start justify-between gap-3"},kn={class:"text-xs font-semibold text-[var(--ink2)]"},wn={class:"text-xs text-[var(--ink2)]"},_n=["innerHTML"],$n={__name:"GapFillingSet",props:{title:{type:String,default:""},description:{type:String,default:""},html:{type:String,default:""},questions:{type:Array,default:()=>[]},answers:{type:Object,default:()=>({})},isCurrent:{type:Boolean,default:!1}},emits:["answer"],setup(a,{emit:p}){const k=a,r=p,l=w(null),o=S(()=>[...k.questions].sort((u,g)=>(u.sort??0)-(g.sort??0))),y=S(()=>{const u={};return o.value.forEach((g,m)=>{var f;u[`gf_${m+1}`]={questionId:g.id,value:((f=k.answers)==null?void 0:f[g.id])??""}}),u});function x({gapKey:u,value:g}){const m=String(u||"").match(/^gf_(\d+)$/);if(!m)return;const f=Number(m[1])-1,d=o.value[f];d!=null&&d.id&&r("answer",{questionId:d.id,value:g})}return(u,g)=>(s(),n("div",{class:N(["card p-4",a.isCurrent?"ring-2 ring-[rgba(124,106,247,0.35)]":""])},[e("div",yn,[e("div",kn,v(a.title),1),e("div",wn,v(a.questions.length)+" gaps",1)]),a.description?(s(),n("div",{key:0,class:"mt-2 text-sm text-[var(--ink2)]",innerHTML:a.description},null,8,_n)):M("",!0),e("div",{class:"mt-4",ref_key:"rootEl",ref:l},[U(Et,{html:a.html,gaps:y.value,onAnswer:x},null,8,["html","gaps"])],512)],2))}},Cn={class:"fixed left-4 top-1/2 z-[150] flex -translate-y-1/2 flex-col gap-2"},qn={class:"relative"},Sn={key:0,class:"absolute left-14 top-0 flex gap-1.5 rounded-xl border border-[var(--border)] bg-white p-1.5 shadow-lg"},Tn=["title","onClick"],In={key:0,class:"fixed right-0 top-0 z-[160] flex h-full w-80 flex-col border-l border-[var(--border)] bg-white shadow-[-4px_0_24px_rgba(0,0,0,0.1)]"},Mn={class:"flex shrink-0 items-center justify-between border-b border-[var(--border)] px-4 py-3"},An={class:"shrink-0 border-t border-[var(--border)] px-4 py-2 text-right"},Nn={class:"text-[10px] text-[var(--ink3)]"},zn={__name:"PracticeToolbar",props:{practiceMode:{type:Boolean,default:!1},modelNote:{type:String,default:""}},emits:["update:modelNote","highlight-applied","tool-changed"],setup(a,{expose:p,emit:k}){const r=a,l=k,{activeTool:o,setTool:y}=Lt(),x=w("yellow"),u=w(r.modelNote),g=[{value:"yellow",label:"Vàng",bg:"#fef08a"},{value:"green",label:"Xanh lá",bg:"#bbf7d0"},{value:"rose",label:"Hồng",bg:"#fecdd3"},{value:"blue",label:"Xanh dương",bg:"#bfdbfe"}];return X(u,m=>l("update:modelNote",m)),X(o,m=>l("tool-changed",{tool:m,color:x.value})),X(x,m=>l("tool-changed",{tool:o.value,color:m})),p({activeTool:o,highlightColor:x,noteText:u}),(m,f)=>a.practiceMode?(s(),n(V,{key:0},[e("div",Cn,[e("div",qn,[e("button",{type:"button",class:N(["relative flex h-11 w-11 cursor-pointer items-center justify-center rounded-xl border shadow-sm transition-all hover:-translate-x-0.5 hover:shadow-md",_(o)==="highlight"?"border-yellow-300 bg-yellow-100 text-yellow-800":"border-[var(--border)] bg-white text-[var(--ink2)] hover:border-yellow-300 hover:bg-yellow-50"]),title:"Tô màu (T)",onClick:f[0]||(f[0]=d=>_(y)("highlight"))},[...f[5]||(f[5]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("path",{d:"m9 11-6 6v3h9l3-3"}),e("path",{d:"m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"})],-1),e("span",{class:"absolute bottom-0.5 right-1 text-[8px] font-bold leading-none opacity-50"},"T",-1)])],2),U(Le,{name:"slide-x"},{default:ae(()=>[_(o)==="highlight"?(s(),n("div",Sn,[(s(),n(V,null,D(g,d=>e("button",{key:d.value,type:"button",title:d.label,class:N(["h-6 w-6 rounded-lg border-2 transition-transform hover:scale-110",x.value===d.value?"border-[var(--ink)]":"border-transparent"]),style:Be({background:d.bg}),onClick:ge(T=>x.value=d.value,["stop"])},null,14,Tn)),64))])):M("",!0)]),_:1})]),e("button",{type:"button",class:N(["relative flex h-11 w-11 cursor-pointer items-center justify-center rounded-xl border shadow-sm transition-all hover:-translate-x-0.5 hover:shadow-md",_(o)==="note"?"border-[var(--blue-l)] bg-[var(--blue-bg)] text-[var(--blue)]":"border-[var(--border)] bg-white text-[var(--ink2)] hover:border-[var(--blue-l)] hover:bg-[var(--blue-bg)]"]),title:"Ghi chú (N)",onClick:f[1]||(f[1]=d=>_(y)("note"))},[...f[6]||(f[6]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})],-1),e("span",{class:"absolute bottom-0.5 right-1 text-[8px] font-bold leading-none opacity-50"},"N",-1)])],2),e("button",{type:"button",class:N(["relative flex h-11 w-11 cursor-pointer items-center justify-center rounded-xl border shadow-sm transition-all hover:-translate-x-0.5 hover:shadow-md",_(o)==="vocab"?"border-green-700 bg-green-100 text-green-700":"border-[var(--border)] bg-white text-[var(--ink2)] hover:border-green-700 hover:bg-green-50"]),title:"Tra từ vựng (S)",onClick:f[2]||(f[2]=d=>_(y)("vocab"))},[...f[7]||(f[7]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("circle",{cx:"11",cy:"11",r:"8"}),e("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})],-1),e("span",{class:"absolute bottom-0.5 right-1 text-[8px] font-bold leading-none opacity-50"},"S",-1)])],2),f[8]||(f[8]=e("div",{class:"mx-auto h-px w-8 bg-[var(--border)]"},null,-1)),f[9]||(f[9]=e("div",{class:"flex h-11 w-11 items-center justify-center text-center text-[9px] leading-tight text-[var(--ink3)]"},[L(" Công"),e("br"),L("cụ ")],-1))]),(s(),J(Ue,{to:"body"},[U(Le,{name:"slide-right"},{default:ae(()=>[_(o)==="note"?(s(),n("div",In,[e("div",Mn,[f[11]||(f[11]=e("div",{class:"flex items-center gap-2 text-sm font-semibold text-[var(--ink)]"},[e("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]),L(" Ghi chú ")],-1)),e("button",{type:"button",class:"cursor-pointer rounded-md p-1 text-[var(--ink3)] hover:bg-[var(--bg2)] hover:text-[var(--ink)]",onClick:f[3]||(f[3]=d=>_(y)("note"))},[...f[10]||(f[10]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"})],-1)])])]),oe(e("textarea",{"onUpdate:modelValue":f[4]||(f[4]=d=>u.value=d),class:"flex-1 resize-none p-4 text-[13px] leading-relaxed text-[var(--ink)] outline-none",placeholder:"Ghi chú của bạn tại đây..."},null,512),[[Qe,u.value]]),e("div",An,[e("span",Nn,v(u.value.length)+" ký tự",1)])])):M("",!0)]),_:1})]))],64)):M("",!0)}},En={class:"mb-5 flex flex-wrap items-center gap-3"},Ln={class:"flex min-w-0 flex-1 flex-wrap items-center gap-2 sm:gap-3"},Bn={class:"flex items-center gap-1.5 rounded-full border border-[var(--border)] bg-white px-4 py-1.5 text-[12px]"},jn={class:"font-bold text-[var(--ink)]"},On={class:"text-[var(--ink3)]"},Pn={class:"hidden gap-1 sm:flex"},Hn={class:"h-1.5 flex-1 min-w-[80px] overflow-hidden rounded-full bg-[var(--border2)] sm:max-w-[200px]"},Rn={class:"p-6 sm:p-8"},Vn={key:0,class:"mb-4 text-[11px] font-bold uppercase tracking-wider text-[var(--ink3)]"},Un={class:"flex items-center justify-between gap-3 border-t border-[var(--border)] bg-[var(--bg)] px-6 py-4 sm:px-8"},Fn=["disabled"],Gn={class:"text-[11px] text-[var(--ink3)]"},Qn=["disabled"],Dn={key:0,class:"mt-6"},Kn={__name:"SpeakingPracticePanel",props:{currentIndex:{type:Number,default:0},total:{type:Number,default:1},partTitle:{type:String,default:""},chatOpen:{type:Boolean,default:!1},canPrev:{type:Boolean,default:!1},canNext:{type:Boolean,default:!1}},emits:["exit","prev","next","toggle-chat"],setup(a){const p=a,k=S(()=>p.total?Math.round((p.currentIndex+1)/p.total*100):0);function r(l){return l<p.currentIndex?"bg-[#34d399]":l===p.currentIndex?"bg-[#111]":"bg-[var(--border2)]"}return(l,o)=>(s(),n("div",{class:N(["speaking-practice mx-auto w-full",a.chatOpen?"max-w-6xl":"max-w-3xl"])},[e("div",En,[e("button",{type:"button",class:"ct-btn flex shrink-0 items-center gap-1.5 px-3 py-1.5 text-[12px]",onClick:o[0]||(o[0]=y=>l.$emit("exit"))},[...o[4]||(o[4]=[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"15 18 9 12 15 6"})],-1),L(" Thoát ",-1)])]),e("div",Ln,[e("div",Bn,[e("span",jn,"Question "+v(a.currentIndex+1),1),e("span",On,"/ "+v(a.total),1)]),e("div",Pn,[(s(!0),n(V,null,D(a.total,(y,x)=>(s(),n("div",{key:x,class:N(["h-2 w-2 rounded-full transition-colors",r(x)])},null,2))),128))]),e("div",Hn,[e("div",{class:"h-full rounded-full bg-[#34d399] transition-all duration-300",style:Be({width:`${k.value}%`})},null,4)])]),e("button",{type:"button",class:N(["flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-[11px] font-semibold transition-colors",a.chatOpen?"border-[#34d399] bg-[#34d39911] text-[#34d399]":"border-[var(--border2)] bg-white text-[var(--ink2)] hover:border-[#34d399] hover:text-[#34d399]"]),onClick:o[1]||(o[1]=y=>l.$emit("toggle-chat"))},[...o[5]||(o[5]=[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"currentColor"},[e("path",{d:"M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"})],-1),L(" Need help? ",-1)])],2)]),e("div",{class:N(["flex overflow-hidden rounded-2xl border border-[var(--border)] bg-white shadow-sm",a.chatOpen?"flex-col lg:flex-row":"flex-col"])},[e("div",{class:N(["flex min-w-0 flex-1 flex-col",a.chatOpen?"lg:border-r lg:border-[var(--border)]":""])},[e("div",Rn,[a.partTitle?(s(),n("div",Vn,v(a.partTitle),1)):M("",!0),Re(l.$slots,"default")]),e("div",Un,[e("button",{type:"button",class:N(["ct-btn flex items-center gap-1.5 px-4 py-2 text-[13px]",a.canPrev?"":"pointer-events-none opacity-30"]),disabled:!a.canPrev,onClick:o[2]||(o[2]=y=>l.$emit("prev"))},[...o[6]||(o[6]=[e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"15 18 9 12 15 6"})],-1),L(" Previous ",-1)])],10,Fn),e("span",Gn,v(a.currentIndex+1)+" / "+v(a.total),1),e("button",{type:"button",class:N(["ct-btn flex items-center gap-1.5 px-4 py-2 text-[13px]",a.canNext?"":"pointer-events-none opacity-30"]),disabled:!a.canNext,onClick:o[3]||(o[3]=y=>l.$emit("next"))},[...o[7]||(o[7]=[L(" Next ",-1),e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"9 18 15 12 9 6"})],-1)])],10,Qn)])],2),Re(l.$slots,"chat")],2),l.$slots.feedback?(s(),n("div",Dn,[Re(l.$slots,"feedback")])):M("",!0)],2))}},Xn={class:"flex h-full w-80 shrink-0 flex-col overflow-hidden border-l border-[var(--border)] bg-white"},Wn={class:"flex shrink-0 items-center justify-between border-b border-[var(--border)] px-4 py-3"},Jn={class:"flex items-center gap-2"},Yn={key:0,class:"rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-[11px] text-[var(--ink2)]"},Zn={key:0,class:"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[var(--bg2)] text-[10px] font-bold text-[var(--ink2)]"},er={key:0,class:"flex items-center gap-1"},tr={key:1,style:{"white-space":"pre-wrap"}},sr={class:"shrink-0 border-t border-[var(--border)] p-3"},nr={class:"mb-2 flex flex-wrap gap-1.5"},rr=["disabled","onClick"],or={class:"flex gap-2"},ir=["disabled","onKeydown"],ar=["disabled"],vt={__name:"SpeakingChatbot",props:{questionText:{type:String,default:""}},emits:["close"],setup(a){const p=a,k=w(""),r=w(!1),l=w([]),o=w(null);let y=0;const x=["Start quickly","3 key vocab","1 band tip"];async function u(){await qe(),o.value&&(o.value.scrollTop=o.value.scrollHeight)}X(()=>p.questionText,(f,d)=>{f&&f!==d&&l.value.length>0&&(l.value.push({id:y++,role:"assistant",text:`📌 New question: "${f}"`,loading:!1}),u())});async function g(f){var T;if(!f.trim()||r.value)return;l.value.push({id:y++,role:"user",text:f,loading:!1});const d={id:y++,role:"assistant",text:"",loading:!0};l.value.push(d),r.value=!0,await u();try{const z=l.value.filter(G=>!G.loading&&G.id<d.id&&!G.text.startsWith("📌")).map(G=>({role:G.role==="user"?"user":"assistant",content:G.text})),{data:H}=await Fe.post("/speaking/chat",{question_text:p.questionText,user_message:f,history:z},{timeout:9e4});d.loading=!1,d.text=(H==null?void 0:H.reply)||(H==null?void 0:H.error)||"Không nhận được phản hồi."}catch(z){d.loading=!1;const H=(T=z.response)==null?void 0:T.data;d.text=(H==null?void 0:H.error)||(H==null?void 0:H.detail)||z.message||"Lỗi kết nối AI. Thử lại sau."}finally{r.value=!1,await u()}}function m(){const f=k.value.trim();f&&(k.value="",g(f))}return(f,d)=>(s(),n("div",Xn,[e("div",Wn,[e("div",Jn,[e("button",{class:"flex h-6 w-6 items-center justify-center rounded text-[var(--ink3)] transition hover:bg-[var(--bg2)]",onClick:d[0]||(d[0]=T=>f.$emit("close"))},[...d[2]||(d[2]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"})],-1)])]),d[3]||(d[3]=e("span",{class:"text-[12px] font-bold text-[var(--ink)]"},"Catbot – Personal Tutor",-1))]),d[4]||(d[4]=e("div",{class:"flex items-center gap-1 text-[11px] text-[var(--ink3)]"},[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("circle",{cx:"12",cy:"12",r:"10"}),e("polyline",{points:"12 6 12 12 16 14"})]),L(" History ")],-1))]),e("div",{ref_key:"scrollEl",ref:o,class:"flex-1 min-h-0 space-y-3 overflow-y-auto scroll-smooth p-4"},[d[7]||(d[7]=e("div",{class:"flex items-start gap-2"},[e("div",{class:"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[var(--bg2)] text-[10px] font-bold text-[var(--ink2)]"},"AI"),e("div",{class:"rounded-xl rounded-tl-none bg-[var(--bg)] p-3 text-[12px] leading-relaxed text-[var(--ink)]"}," Hey! I am your personal tutor. Need help with the speaking task? Go ahead and ask. ")],-1)),a.questionText?(s(),n("div",Yn,[d[5]||(d[5]=e("span",{class:"font-semibold text-[#34d399]"},"Current question: ",-1)),L(v(a.questionText),1)])):M("",!0),(s(!0),n(V,null,D(l.value,T=>(s(),n("div",{key:T.id,class:N(["flex items-start gap-2",T.role==="user"?"flex-row-reverse":""])},[T.role==="assistant"?(s(),n("div",Zn,"AI")):M("",!0),e("div",{class:N(["max-w-[85%] rounded-xl p-3 text-[12px] leading-relaxed",T.role==="user"?"rounded-tr-none bg-[#111] text-white":"rounded-tl-none bg-[var(--bg)] text-[var(--ink)]"])},[T.loading?(s(),n("span",er,[...d[6]||(d[6]=[e("span",{class:"animate-bounce text-[var(--ink3)]"},"●",-1),e("span",{class:"animate-bounce text-[var(--ink3)]",style:{"animation-delay":"0.15s"}},"●",-1),e("span",{class:"animate-bounce text-[var(--ink3)]",style:{"animation-delay":"0.3s"}},"●",-1)])])):(s(),n("span",tr,v(T.text),1))],2)],2))),128))],512),e("div",sr,[e("div",nr,[(s(),n(V,null,D(x,T=>e("button",{key:T,disabled:r.value,class:"rounded-full border border-[var(--border2)] bg-white px-2.5 py-1 text-[10px] font-medium text-[var(--ink2)] transition-colors hover:border-[#34d399] hover:text-[#34d399] disabled:opacity-40",onClick:z=>g(T)},v(T),9,rr)),64))]),e("div",or,[oe(e("input",{"onUpdate:modelValue":d[1]||(d[1]=T=>k.value=T),disabled:r.value,class:"ct-input flex-1 py-1.5 text-[12px]",placeholder:"Ask anything in your language",onKeydown:xt(ge(m,["prevent"]),["enter"])},null,40,ir),[[Qe,k.value]]),e("button",{disabled:r.value||!k.value.trim(),class:"ct-btn px-3 py-1.5 text-[12px] disabled:opacity-40",onClick:m},[...d[8]||(d[8]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"currentColor"},[e("path",{d:"M22 2L11 13"}),e("path",{d:"M22 2L15 22 11 13 2 9l20-7z"})],-1)])],8,ar)])])]))}};function lr(a){return a?Array.isArray(a.paragraph_ranges)?a.paragraph_ranges:Object.values(a).flatMap(p=>Array.isArray(p==null?void 0:p.paragraph_ranges)?p.paragraph_ranges:[]):[]}function ur(a,p){const{start:k,end:r}=p||{};if(!k||!r)return[];const l=k.paragraph,o=r.paragraph,y=k.sentence??1,x=r.sentence??1/0,u=[];for(const g of a){const m=g.paragraph;if(m<l||m>o)continue;(g.children||[]).forEach((d,T)=>{const z=T+1;m===l&&m===o?z>=y&&z<=x&&u.push(d.id):m===l?z>=y&&u.push(d.id):m===o?z<=x&&u.push(d.id):u.push(d.id)})}return u}function dr(a,p){const k=S(()=>{const m=[];for(const f of a.value||[])for(const d of f.children||[])!Number.isFinite(d.from)||!Number.isFinite(d.to)||m.push({id:d.id,from:d.from,to:d.to,speaker:d.speaker,text:d.text});return m}),r=S(()=>{var f;const m=p.value||0;return((f=k.value.find(d=>m>=d.from&&m<=d.to))==null?void 0:f.id)??null}),l=w(new Set);let o=null;function y(m){const f=a.value||[],T=lr(m).flatMap(z=>ur(f,z));l.value=new Set(T),clearTimeout(o),T.length&&(o=setTimeout(()=>{l.value=new Set},5e3))}function x(){clearTimeout(o),l.value=new Set}const u=S(()=>l.value.size>0?l.value:r.value?new Set([r.value]):new Set),g=S(()=>l.value.size>0?[...l.value][0]:r.value);return{segments:k,activeSegId:r,highlightedIds:u,scrollToSegId:g,activateLocateInfo:y,clearForced:x}}const cr={class:"quiz-runner min-h-screen bg-[var(--bg)]"},vr={key:0,class:"fixed inset-0 z-[500] flex items-center justify-center p-4"},pr={class:"relative z-10 w-full max-w-sm rounded-xl bg-white p-6 shadow-xl"},fr={class:"flex justify-end gap-2"},xr={class:"exam-container py-5 sm:py-6"},gr={key:0,class:"card p-6 text-center text-[var(--ink2)]"},mr={key:1,class:"card p-6 text-center"},br={key:0,class:"fixed inset-0 z-[600] flex items-center justify-center bg-black/60"},hr={key:0},yr={class:"space-y-5"},kr={class:"flex flex-wrap items-center justify-between gap-2"},wr={class:"flex items-center gap-2 text-[14px] font-semibold text-[var(--ink)]"},_r={class:"flex gap-2"},$r={class:"grid grid-cols-1 gap-5 xl:grid-cols-2"},Cr={class:"card p-6"},qr={class:"flex items-center gap-6"},Sr={class:"flex-1 space-y-2 text-sm text-[var(--ink2)]"},Tr={class:"flex justify-between"},Ir={class:"font-semibold text-[var(--ink)]"},Mr={class:"flex justify-between"},Ar={class:"font-semibold text-[var(--ink)]"},Nr={class:"flex justify-between"},zr={class:"font-semibold text-[var(--ink)]"},Er={class:"mt-5"},Lr={class:"card p-6"},Br={class:"grid grid-cols-2 gap-y-5 gap-x-4"},jr={class:"grid grid-cols-1 gap-5 xl:grid-cols-2"},Or={class:"grid grid-cols-1 gap-5 xl:grid-cols-2"},Pr={class:"card p-5"},Hr={class:"space-y-1.5"},Rr={key:0,class:"text-sm text-[var(--ink3)]"},Vr={class:"card p-5"},Ur={class:"space-y-1.5"},Fr={key:0,class:"text-sm text-[var(--ink3)]"},Gr={key:0,class:"card border-l-4 border-l-[#34d399] p-5"},Qr={class:"text-sm leading-relaxed text-[var(--ink)]"},Dr={key:1,class:"mx-auto mt-4 max-w-3xl rounded-lg border border-[#f43f5e44] bg-[#f43f5e11] px-4 py-2 text-xs text-[#f43f5e]"},Kr={class:"mb-4 flex items-center justify-between gap-3"},Xr={class:"mx-auto flex max-w-7xl gap-0 overflow-hidden rounded-2xl border border-[var(--border)] bg-white"},Wr={class:"mb-2 text-xs font-semibold text-[var(--ink2)]"},Jr=["innerHTML"],Yr={class:"grid gap-3"},Zr=["onClick"],eo={key:0,class:"mt-3 overflow-hidden rounded-xl border border-[#d1fae5] bg-[#f0fdf4]"},to={class:"flex items-center justify-between border-b border-[#d1fae5] px-4 py-2.5"},so={class:"flex items-center gap-2 text-[12px] font-semibold text-[#059669]"},no=["onClick"],ro={class:"grid grid-cols-2 divide-x divide-[#d1fae5] sm:grid-cols-4"},oo={class:"px-4 py-3 text-center"},io={class:"text-lg font-bold text-[#059669]"},ao={class:"px-4 py-3 text-center"},lo={class:"text-base font-semibold text-[var(--ink)]"},uo={class:"px-4 py-3 text-center"},co={class:"text-base font-semibold text-[var(--ink)]"},vo={class:"px-4 py-3 text-center"},po={class:"text-base font-semibold text-[var(--ink)]"},fo={key:0,class:"border-t border-[#d1fae5] px-4 py-2.5 text-[12px] text-[#374151]"},xo={class:"mt-6 flex items-center justify-between gap-2"},go={key:0,class:"mt-3 rounded-lg border border-[#f43f5e44] bg-[#f43f5e11] px-4 py-2 text-xs text-[#f43f5e]"},mo={key:0,class:"card overflow-hidden"},bo={class:"border-b border-[var(--border)] px-4 py-2.5 text-xs font-semibold text-[var(--ink2)]"},ho={class:"overflow-y-auto p-4",style:{"max-height":"calc(100vh - 280px)"}},yo={key:1,class:"card overflow-hidden"},ko={class:"overflow-y-auto p-4",style:{"max-height":"calc(100vh - 220px)"}},wo={key:1,class:"reading-passage"},_o={class:"para-tag"},$o={class:"text-xs font-semibold text-[var(--ink2)] mb-2"},Co=["innerHTML"],qo={key:2,class:"grid gap-3"},So=["onClick"],To={key:0,width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},Io={key:1,width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},Mo={key:2,class:"ml-1 font-normal text-[var(--ink2)]"},Ao={class:"text-[var(--ink)]"},No={class:"mt-6 flex items-center justify-between gap-2"},Ro={__name:"QuizRunner",setup(a){const p=At(),k=zt(),r=Qt(),l=Wt(),o=S(()=>p.query.mode==="practice"),y=w(!1),x=w(null),u=w(!1),g=w(null),m=w({}),f=w(`sp_${Date.now()}_${Math.random().toString(36).slice(2,8)}`),d=w(null);async function T({blob:i,questionText:t,questionId:$}){var O,F;const E=String($??((F=(O=te.value)==null?void 0:O.question)==null?void 0:F.id)??"");d.value=E||null,y.value=!0,x.value=null;try{const j=new FormData;j.append("file",i,"recording.webm"),j.append("question_text",t),j.append("persist_result","true"),j.append("quiz_id",String(p.params.quizId||"speaking")),j.append("question_id",E),j.append("attempt_id",f.value),Gt();const{data:Q}=await Fe.post("/speaking/evaluate",j,{timeout:12e4,transformRequest:[(C,P)=>(delete P["Content-Type"],delete P["content-type"],C)]}),le=URL.createObjectURL(i),h={result:Q,audioUrl:le,question:t,questionId:E};g.value=h,E&&(m.value[E]={...h})}catch(j){x.value=j.message||"Evaluation failed. Please try again."}finally{y.value=!1}}const z=new Map,H=w(null),G=w(null),ne=w(null),b=w(0),c=w(null),q=w(!1);function ee(){q.value=!1,k.push("/dashboard")}const Se=i=>{i.preventDefault(),i.returnValue=""},ve=w(580);let W=!1,Y=0,Te=0;function I(i){W=!0,Y=i.clientX,Te=ve.value,document.body.style.userSelect="none",document.body.style.cursor="col-resize"}function A(i){var E;if(!W)return;const t=i.clientX-Y,$=((E=G.value)==null?void 0:E.clientWidth)||1200;ve.value=Math.max(280,Math.min($-340,Te+t))}function R(){W&&(W=!1,document.body.style.userSelect="",document.body.style.cursor="")}const K=S(()=>Dt(r.quiz)),me=S(()=>r.flat.some(i=>String(i.questionSetType||"").toLowerCase()==="speaking")),Z=S(()=>r.flat.filter(i=>{const t=i.question||{},$=String(i.questionSetType||"").toLowerCase(),E=String(t.question_type||"").toLowerCase(),O=String(t.type||"").toLowerCase();return $==="speaking"||E==="speaking"||O==="speaking"})),Ie=w(0),re=S(()=>{const i=Math.max(0,Z.value.length-1);return Math.min(Math.max(0,Ie.value),i)}),te=S(()=>Z.value[re.value]??null),B=S(()=>{var t,$;const i=String((($=(t=te.value)==null?void 0:t.question)==null?void 0:$.id)??"");return i?m.value[i]:null});function gt(){var $;const i=re.value-1;if(i<0)return;Ie.value=i;const t=Z.value[i];(($=t==null?void 0:t.question)==null?void 0:$.order)!=null&&Ae(t.question.order)}function De(){var $;const i=re.value+1;if(i>=Z.value.length)return;Ie.value=i;const t=Z.value[i];(($=t==null?void 0:t.question)==null?void 0:$.order)!=null&&Ae(t.question.order)}const Me=S(()=>{const i=o.value?te.value:r.currentItem;if(!i)return"";const t=i.question||{};return t.text||t.title||t.content||""}),mt=S(()=>{var i;return((i=r.quiz)==null?void 0:i.title)||`Quiz #${p.params.quizId}`}),bt=S(()=>`${K.value?"Listening":me.value?"Speaking":"Reading"} · ${r.totalQuestions} câu`),Ke=S(()=>r.flat.map(i=>({order:i.question.order,id:i.question.id}))),Xe=S(()=>{var t;return(((t=r.quiz)==null?void 0:t.parts)||[]).map(($,E)=>{const O=r.flat.filter(j=>j.partId===$.id).map(j=>({order:j.question.order,id:j.question.id})),F=`Part ${$.passage||E+1}`;return{id:$.id,label:F,questions:O}})}),pe=S(()=>{var t,$,E,O,F;const i=(t=r.currentItem)==null?void 0:t.partId;return((E=($=r.quiz)==null?void 0:$.parts)==null?void 0:E.find(j=>j.id===i))||((F=(O=r.quiz)==null?void 0:O.parts)==null?void 0:F[0])||null}),be=S(()=>{var i;return Kt(((i=pe.value)==null?void 0:i.vocabs)||[])}),ht=S(()=>{var i;return Pt((i=pe.value)==null?void 0:i.file_id)}),je=dr(be,b),yt=S(()=>{var i,t;return(t=(i=r.currentItem)==null?void 0:i.question)==null?void 0:t.locate_info}),We=S(()=>Xt(yt.value));function kt(i){return We.value.length?We.value.some(t=>i>=t.startParagraph&&i<=t.endParagraph):!1}function Je(i,t){if(!i||!t)return;const $=(t==null?void 0:t.$el)??t;z.set(i,$)}function Ye(i){const t=z.get(i);t&&typeof t.scrollIntoView=="function"&&t.scrollIntoView({behavior:"smooth",block:"start"})}function Ze(i){r.gotoOrder(i)}async function Ae(i){r.gotoOrder(i),await qe(),Ye(i)}function wt(i){return new Set((i||[]).map($=>$.order)).has(r.currentOrder)}function _t({time:i,locateInfo:t}={}){var $;Number.isFinite(i)&&(($=ne.value)==null||$.seekAndPlay(i)),t&&je.activateLocateInfo(t)}const et=S(()=>{var $;const i=(($=r.quiz)==null?void 0:$.parts)||[],t=[];for(const E of i)for(const O of E.question_sets||[]){const F=`${E.id}_${O.id}`;if(O.question_type==="GAP_FILLING"){t.push({key:F,kind:"gap",title:O.title||`Part ${E.passage}`,description:O.description||"",content:O.content||"",questions:(O.questions||[]).map(Q=>({id:Q.id,sort:Q.sort,order:Q.order}))});continue}const j=r.flat.filter(Q=>Q.partId===E.id&&Q.questionSetId===O.id);t.push({key:F,kind:"items",title:O.title||`Part ${E.passage}`,description:O.description||"",items:j})}return t}),Oe=w(new Set);function $t(i){if(!o.value)return;const t=new Set(Oe.value);t.add(String(i)),Oe.value=t}function Ct(i,t){r.setAnswer(i,t),$t(i)}function se(i){var j,Q;const t=String(((j=i.question)==null?void 0:j.id)??"");if(!Oe.value.has(t))return null;const $=i.question||{},E=r.answers[$.id],O=Ot({question:$,userAnswer:E}),F=(Q=$.correct_answers)!=null&&Q.length?$.correct_answers.join(" / "):$.correct_answer??"—";return{ok:O,correctAnswer:F,explain:$.explain||"",userAnswer:E}}const tt=w(null),Pe=w(null),He=w("yellow"),he=w(""),Ne=w(`session_${Date.now()}_${Math.random().toString(36).slice(2,7)}`),st=w([]);function nt(i){st.value=i}function rt({tool:i,color:t}){Pe.value=i,He.value=t||"yellow"}async function qt(){if(o.value)try{await Jt(Ne.value,{session_id:Ne.value,quiz_id:String(p.params.quizId||""),highlights:st.value,note:he.value})}catch{}}async function ze(i){var j,Q,le,h;if(await qt(),me.value){if(!(Object.keys(m.value||{}).length>0)){x.value="Bạn cần đánh giá ít nhất 1 câu speaking trước khi nộp.";return}try{const{data:P}=await Fe.get("/speaking/attempt-summary",{params:{quiz_id:String(p.params.quizId||"speaking"),attempt_id:f.value}});k.push({path:"/speaking/result",state:{summary:P,question:`Speaking quiz #${p.params.quizId}`,mode:"attempt-summary"}});return}catch(P){x.value=((Q=(j=P==null?void 0:P.response)==null?void 0:j.data)==null?void 0:Q.detail)||(P==null?void 0:P.message)||"Không tải được tổng kết speaking.";return}}r.submit({auto:i});const t=l.currentSession,$=(le=t==null?void 0:t.quiz)==null?void 0:le.id,E=Number(p.params.quizId),O=K.value?"listening":"reading";if(t!=null&&t.session_id&&Number($)===E){const C=Object.entries(r.answers||{}).reduce((ie,[ue,de])=>(ie[String(ue)]=de,ie),{});if(await l.submitSession(O,t.session_id,C)){k.push({path:`/results/${t.session_id}`,query:{annotationSession:Ne.value}});return}}const F=jt({quiz:r.quiz,flat:r.flat,answers:r.answers});r.result={quizId:E,title:(h=r.quiz)==null?void 0:h.title,correct:F.correct,total:F.total,estimatedBand:F.estimatedBand,detailed:F.detailed,answers:r.answers,annotationSession:Ne.value},k.push(`/quiz/${p.params.quizId}/result`)}return X(()=>r.remainingSeconds,i=>{i===0&&r.quiz&&!r.result&&ze(!0)}),X([()=>Z.value,()=>r.currentOrder,()=>o.value],()=>{if(!o.value||!Z.value.length)return;const i=Z.value.findIndex(t=>{var $;return String(($=t.question)==null?void 0:$.order)===String(r.currentOrder)});i>=0&&(Ie.value=i)},{immediate:!0,deep:!1}),pt(async()=>{await r.loadQuiz(p.params.quizId),await qe(),Ye(r.currentOrder),window.addEventListener("beforeunload",Se),window.addEventListener("mousemove",A),window.addEventListener("mouseup",R)}),Ge(()=>{r.stopTimer(),window.removeEventListener("beforeunload",Se),window.removeEventListener("mousemove",A),window.removeEventListener("mouseup",R)}),(i,t)=>{var E,O,F,j,Q,le;const $=Nt("RouterLink");return s(),n("div",cr,[(s(),J(Ue,{to:"body"},[q.value?(s(),n("div",vr,[e("div",{class:"absolute inset-0 bg-black/40",onClick:t[0]||(t[0]=h=>q.value=!1)}),e("div",pr,[t[19]||(t[19]=e("div",{class:"mb-1 text-base font-bold text-[var(--ink)]"},"Thoát bài thi?",-1)),t[20]||(t[20]=e("p",{class:"mb-5 text-[13px] text-[var(--ink3)]"},"Tiến trình làm bài sẽ không được lưu. Bạn có chắc muốn thoát?",-1)),e("div",fr,[e("button",{class:"ct-btn",onClick:t[1]||(t[1]=h=>q.value=!1)},"Tiếp tục làm"),e("button",{class:"ct-btn",style:{"border-color":"#e11d48",color:"#e11d48"},onClick:ee},"Thoát")])])])):M("",!0)])),o.value&&me.value?(s(),J(zn,{key:0,"practice-mode":o.value,"model-note":he.value,"onUpdate:modelNote":t[2]||(t[2]=h=>he.value=h),onToolChanged:rt},null,8,["practice-mode","model-note"])):M("",!0),o.value&&!me.value?(s(),J(Bt,{key:1,floating:"","model-note":he.value,"onUpdate:modelNote":t[3]||(t[3]=h=>he.value=h),onToolChanged:rt},null,8,["model-note"])):M("",!0),U(as,{title:mt.value,subtitle:bt.value,"remaining-seconds":_(r).remainingSeconds,onSubmit:t[4]||(t[4]=h=>ze(!1))},null,8,["title","subtitle","remaining-seconds"]),e("div",xr,[_(r).loading?(s(),n("div",gr,"Loading…")):_(r).quiz?(s(),n(V,{key:2},[(s(),J(Ue,{to:"body"},[y.value?(s(),n("div",br,[...t[23]||(t[23]=[e("div",{class:"flex flex-col items-center gap-4 rounded-2xl bg-[#0f0f1a] p-8 text-white shadow-2xl"},[e("div",{class:"h-10 w-10 animate-spin rounded-full border-4 border-[#6c63ff] border-t-transparent"}),e("p",{class:"text-sm font-semibold"},"Đang phân tích bài nói…"),e("p",{class:"text-[11px] text-[#a0a0c0]"},"Pronunciation · Transcription · AI Feedback")],-1)])])):M("",!0)])),me.value?(s(),n("div",hr,[o.value?(s(),J(Kn,{key:0,"current-index":re.value,total:Z.value.length,"part-title":((E=te.value)==null?void 0:E.partTitle)||(te.value?`Part ${re.value+1}`:""),"chat-open":u.value,"can-prev":re.value>0,"can-next":re.value<Z.value.length-1,onExit:t[8]||(t[8]=h=>q.value=!0),onPrev:gt,onNext:De,onToggleChat:t[9]||(t[9]=h=>u.value=!u.value)},Mt({chat:ae(()=>[U(Le,{name:"slide"},{default:ae(()=>[u.value?(s(),J(vt,{key:0,class:"w-full shrink-0 lg:w-[340px]","question-text":Me.value,onClose:t[6]||(t[6]=h=>u.value=!1)},null,8,["question-text"])):M("",!0)]),_:1})]),default:ae(()=>[te.value?(s(),J(Ve,{key:te.value.question.id,item:te.value,answer:_(r).answers[te.value.question.id],"is-current":!0,"speaking-compact":"","onUpdate:answer":t[5]||(t[5]=h=>_(r).setAnswer(te.value.question.id,h)),onEvaluateSpeaking:T},null,8,["item","answer"])):M("",!0)]),_:2},[(O=B.value)!=null&&O.result?{name:"feedback",fn:ae(()=>{var h,C,P,ie,ue,de,ye,ke,we,_e,$e;return[e("div",yr,[e("div",kr,[e("div",wr,[t[24]||(t[24]=e("svg",{width:"15",height:"15",viewBox:"0 0 24 24",fill:"none",stroke:"#34d399","stroke-width":"2"},[e("path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14"}),e("polyline",{points:"22 4 12 14.01 9 11.01"})],-1)),L(" Kết quả câu "+v(re.value+1),1)]),e("div",_r,[e("button",{class:"ct-btn px-3 py-1.5 text-[12px]",onClick:t[7]||(t[7]=ce=>_(k).push({path:"/speaking/result",state:B.value}))}," Xem chi tiết "),re.value<Z.value.length-1?(s(),n("button",{key:0,class:"flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[12px] font-semibold text-white",style:{background:"#34d399"},onClick:De},[...t[25]||(t[25]=[L(" Câu tiếp theo ",-1),e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"9 18 15 12 9 6"})],-1)])])):M("",!0)])]),e("div",$r,[e("div",Cr,[t[29]||(t[29]=e("div",{class:"mb-4 flex items-center gap-2 text-[var(--ink2)]"},[e("span",{class:"text-xs font-bold uppercase tracking-wider"},"Band Score")],-1)),e("div",qr,[U(Ht,{band:B.value.result.band_estimate||0},null,8,["band"]),e("div",Sr,[e("div",Tr,[t[26]||(t[26]=e("span",null,"Grammar",-1)),e("span",Ir,v(Number(((h=B.value.result.grammar)==null?void 0:h.score)||0).toFixed(1))+"/9",1)]),e("div",Mr,[t[27]||(t[27]=e("span",null,"Vocabulary",-1)),e("span",Ar,v(Number(((C=B.value.result.vocabulary)==null?void 0:C.score)||0).toFixed(1))+"/9",1)]),e("div",Nr,[t[28]||(t[28]=e("span",null,"Pronunciation",-1)),e("span",zr,v(Number(((P=B.value.result.pronunciation)==null?void 0:P.total)||0).toFixed(1))+"/10",1)])])]),e("div",Er,[U(Rt,{"audio-url":B.value.audioUrl},null,8,["audio-url"])])]),e("div",Lr,[t[30]||(t[30]=e("div",{class:"mb-4 flex items-center gap-2 text-[var(--ink2)]"},[e("span",{class:"text-xs font-bold uppercase tracking-wider"},"Pronunciation")],-1)),e("div",Br,[U(Ee,{score:((ie=B.value.result.pronunciation)==null?void 0:ie.accuracy)||0,label:"Accuracy"},null,8,["score"]),U(Ee,{score:((ue=B.value.result.pronunciation)==null?void 0:ue.fluency)||0,label:"Fluency"},null,8,["score"]),U(Ee,{score:((de=B.value.result.pronunciation)==null?void 0:de.prosodic)||0,label:"Prosodic"},null,8,["score"]),U(Ee,{score:((ye=B.value.result.pronunciation)==null?void 0:ye.total)||0,label:"Total",size:96},null,8,["score"])])])]),U(Vt,{transcript:B.value.result.transcript||"","word-timestamps":B.value.result.word_timestamps||[]},null,8,["transcript","word-timestamps"]),e("div",jr,[U(Ut,{transcript:B.value.result.transcript||"","question-text":Me.value,score:Number(((ke=B.value.result.grammar)==null?void 0:ke.score)||0),errors:((we=B.value.result.grammar)==null?void 0:we.errors)||[],"evaluate-result":B.value.result},null,8,["transcript","question-text","score","errors","evaluate-result"]),U(Ft,{transcript:B.value.result.transcript||"","question-text":Me.value,score:Number(((_e=B.value.result.vocabulary)==null?void 0:_e.score)||0),feedback:(($e=B.value.result.vocabulary)==null?void 0:$e.feedback)||[],"evaluate-result":B.value.result},null,8,["transcript","question-text","score","feedback","evaluate-result"])]),e("div",Or,[e("div",Pr,[t[32]||(t[32]=e("div",{class:"mb-3 text-xs font-bold uppercase tracking-wider text-[#34d399]"},"Strengths",-1)),e("ul",Hr,[(s(!0),n(V,null,D(B.value.result.strengths||[],(ce,fe)=>(s(),n("li",{key:`st_${fe}`,class:"flex items-start gap-2 text-sm text-[var(--ink)]"},[t[31]||(t[31]=e("span",{class:"mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-[#34d399]"},null,-1)),L(" "+v(ce),1)]))),128)),(B.value.result.strengths||[]).length?M("",!0):(s(),n("li",Rr,"—"))])]),e("div",Vr,[t[34]||(t[34]=e("div",{class:"mb-3 text-xs font-bold uppercase tracking-wider text-[#f59e0b]"},"Improvements",-1)),e("ul",Ur,[(s(!0),n(V,null,D(B.value.result.improvements||[],(ce,fe)=>(s(),n("li",{key:`im_${fe}`,class:"flex items-start gap-2 text-sm text-[var(--ink)]"},[t[33]||(t[33]=e("span",{class:"mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-[#f59e0b]"},null,-1)),L(" "+v(ce),1)]))),128)),(B.value.result.improvements||[]).length?M("",!0):(s(),n("li",Fr,"—"))])])]),B.value.result.overall_comment?(s(),n("div",Gr,[t[35]||(t[35]=e("div",{class:"mb-2 text-xs font-bold uppercase tracking-wider text-[#34d399]"},"Overall comment",-1)),e("p",Qr,v(B.value.result.overall_comment),1)])):M("",!0)])]}),key:"0"}:void 0]),1032,["current-index","total","part-title","chat-open","can-prev","can-next"])):M("",!0),o.value&&x.value?(s(),n("div",Dr,v(x.value),1)):o.value?M("",!0):(s(),n(V,{key:2},[e("div",Kr,[U(ct,{questions:Ke.value,"nav-parts":Xe.value,"current-order":_(r).currentOrder,"answered-map":_(r).answers,onGo:Ae,class:"min-w-0 flex-1"},null,8,["questions","nav-parts","current-order","answered-map"]),e("button",{type:"button",onClick:t[10]||(t[10]=h=>u.value=!u.value),class:N(["flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-[11px] font-semibold transition-colors",u.value?"border-[#34d399] bg-[#34d39911] text-[#34d399]":"border-[var(--border2)] bg-white text-[var(--ink2)] hover:border-[#34d399] hover:text-[#34d399]"])},[...t[36]||(t[36]=[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"currentColor"},[e("path",{d:"M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"})],-1),L(" Need help? Click here. ",-1)])],2)]),e("div",Xr,[e("div",{class:N(["min-w-0 flex-1 p-5 sm:p-6",u.value?"border-r border-[var(--border)]":""])},[(s(!0),n(V,null,D(et.value,h=>(s(),n("div",{key:h.key,class:"mb-6"},[e("div",Wr,v(h.title),1),h.description?(s(),n("div",{key:0,class:"mb-3 text-sm text-[var(--ink2)]",innerHTML:h.description},null,8,Jr)):M("",!0),e("div",Yr,[(s(!0),n(V,null,D(h.items,C=>{var P,ie,ue,de,ye,ke,we,_e,$e,ce,fe,ot,it,at,lt;return s(),n("div",{key:C.question.id,ref_for:!0,ref:Ce=>Je(C.question.order,Ce),onClick:Ce=>Ze(C.question.order)},[U(Ve,{item:C,answer:_(r).answers[C.question.id],"is-current":_(r).currentOrder===C.question.order,"onUpdate:answer":Ce=>_(r).setAnswer(C.question.id,Ce),onEvaluateSpeaking:T},null,8,["item","answer","is-current","onUpdate:answer"]),m.value[String(C.question.id)]?(s(),n("div",eo,[e("div",to,[e("div",so,[t[37]||(t[37]=e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14"}),e("polyline",{points:"22 4 12 14.01 9 11.01"})],-1)),L(" Kết quả câu "+v(C.question.order),1)]),e("button",{class:"text-[11px] text-[#059669] underline hover:no-underline",onClick:ge(Ce=>_(k).push({path:"/speaking/result",state:m.value[String(C.question.id)]}),["stop"])}," Xem chi tiết ",8,no)]),e("div",ro,[e("div",oo,[t[38]||(t[38]=e("div",{class:"text-[11px] text-[#6b7280]"},"Band",-1)),e("div",io,v(Number(((ie=(P=m.value[String(C.question.id)])==null?void 0:P.result)==null?void 0:ie.band_estimate)||0).toFixed(1)),1)]),e("div",ao,[t[40]||(t[40]=e("div",{class:"text-[11px] text-[#6b7280]"},"Grammar",-1)),e("div",lo,[L(v(Number(((ye=(de=(ue=m.value[String(C.question.id)])==null?void 0:ue.result)==null?void 0:de.grammar)==null?void 0:ye.score)||0).toFixed(1)),1),t[39]||(t[39]=e("span",{class:"text-[10px] text-[#9ca3af]"},"/9",-1))])]),e("div",uo,[t[42]||(t[42]=e("div",{class:"text-[11px] text-[#6b7280]"},"Vocab",-1)),e("div",co,[L(v(Number(((_e=(we=(ke=m.value[String(C.question.id)])==null?void 0:ke.result)==null?void 0:we.vocabulary)==null?void 0:_e.score)||0).toFixed(1)),1),t[41]||(t[41]=e("span",{class:"text-[10px] text-[#9ca3af]"},"/9",-1))])]),e("div",vo,[t[44]||(t[44]=e("div",{class:"text-[11px] text-[#6b7280]"},"Pron.",-1)),e("div",po,[L(v(Number(((fe=(ce=($e=m.value[String(C.question.id)])==null?void 0:$e.result)==null?void 0:ce.pronunciation)==null?void 0:fe.total)||0).toFixed(1)),1),t[43]||(t[43]=e("span",{class:"text-[10px] text-[#9ca3af]"},"/10",-1))])])]),(it=(ot=m.value[String(C.question.id)])==null?void 0:ot.result)!=null&&it.transcript?(s(),n("div",fo,[t[45]||(t[45]=e("span",{class:"mr-1 font-semibold text-[#059669]"},"Bài nói:",-1)),L(" "+v((lt=(at=m.value[String(C.question.id)])==null?void 0:at.result)==null?void 0:lt.transcript),1)])):M("",!0)])):M("",!0)],8,Zr)}),128))])]))),128)),e("div",xo,[e("button",{type:"button",class:"btn btn-secondary",onClick:t[11]||(t[11]=h=>q.value=!0)},"Thoát"),e("button",{type:"button",class:"btn btn-primary",onClick:t[12]||(t[12]=h=>ze(!1))},"Nộp bài & Xem kết quả")]),x.value?(s(),n("div",go,v(x.value),1)):M("",!0)],2),U(Le,{name:"slide"},{default:ae(()=>[u.value?(s(),J(vt,{key:0,"question-text":Me.value,onClose:t[13]||(t[13]=h=>u.value=!1)},null,8,["question-text"])):M("",!0)]),_:1})])],64))])):(s(),n("div",{key:1,class:"flex gap-5 lg:gap-6",ref_key:"layoutEl",ref:G},[e("div",{class:"flex flex-col gap-4 min-w-0",style:Be({flex:`0 0 ${ve.value}px`,width:ve.value+"px"})},[K.value?(s(),n(V,{key:0},[U(hs,{ref_key:"playerRef",ref:ne,src:ht.value,title:((F=pe.value)==null?void 0:F.title)||"Listening",subtitle:`File: ${((j=pe.value)==null?void 0:j.file_id)||"—"}`,"seek-to":c.value,onTime:t[14]||(t[14]=h=>b.value.value=h)},null,8,["src","title","subtitle","seek-to"]),o.value?(s(),n("div",mo,[e("div",bo,v((Q=pe.value)==null?void 0:Q.title),1),e("div",ho,[U(dt,{ref_key:"readingPassageRef",ref:tt,paragraphs:be.value,"active-tool":Pe.value,"highlight-color":He.value,"source-type":K.value?"listening":"reading","source-quiz-id":String(_(p).params.quizId||""),onHighlightsChanged:nt},null,8,["paragraphs","active-tool","highlight-color","source-type","source-quiz-id"])])])):(s(),J(Ls,{key:1,paragraphs:be.value,"current-time":b.value,"highlighted-ids":_(je).highlightedIds.value,onSeek:t[15]||(t[15]=h=>{c.value.value=h,_(je).clearForced()})},null,8,["paragraphs","current-time","highlighted-ids"]))],64)):(s(),n("div",yo,[e("div",{class:N(["border-b border-[var(--border)] px-4 py-2.5 text-xs font-semibold text-[var(--ink2)]",o.value?"":"pt-3"])},v((le=pe.value)==null?void 0:le.title),3),e("div",ko,[o.value?(s(),J(dt,{key:0,ref_key:"readingPassageRef",ref:tt,paragraphs:be.value,"active-tool":Pe.value,"highlight-color":He.value,"source-type":K.value?"listening":"reading","source-quiz-id":String(_(p).params.quizId||""),onHighlightsChanged:nt},null,8,["paragraphs","active-tool","highlight-color","source-type","source-quiz-id"])):(s(),n("div",wo,[(s(!0),n(V,null,D(be.value,h=>(s(),n("div",{key:h.paragraph,class:N(["reading-paragraph",kt(h.paragraph)?"is-highlight":""])},[e("span",_o,v(h.paragraph),1),e("span",null,v(h.text),1)],2))),128))]))])])),U(ct,{questions:Ke.value,"nav-parts":Xe.value,"current-order":_(r).currentOrder,"answered-map":_(r).answers,onGo:Ae},null,8,["questions","nav-parts","current-order","answered-map"])],4),e("div",{class:"flex w-2 cursor-col-resize items-center justify-center group",onMousedown:ge(I,["prevent"])},[...t[46]||(t[46]=[e("div",{class:"h-12 w-0.5 rounded-full bg-[var(--border2)] group-hover:bg-[#34d399] transition-colors"},null,-1)])],32),e("div",{class:"card flex-1 overflow-auto p-4",style:{"max-height":"calc(100vh - 140px)"},ref_key:"rightCol",ref:H},[(s(!0),n(V,null,D(et.value,h=>(s(),n("div",{key:h.key,class:"mb-6"},[e("div",$o,v(h.title),1),h.description?(s(),n("div",{key:0,class:"text-sm text-[var(--ink2)] mb-3",innerHTML:h.description},null,8,Co)):M("",!0),h.kind==="gap"?(s(),J($n,{key:1,title:h.title,description:h.description,html:h.content,questions:h.questions,answers:_(r).answers,"is-current":wt(h.questions),onAnswer:t[16]||(t[16]=({questionId:C,value:P})=>_(r).setAnswer(C,P))},null,8,["title","description","html","questions","answers","is-current"])):(s(),n("div",qo,[(s(!0),n(V,null,D(h.items,C=>(s(),n("div",{key:C.question.id,ref_for:!0,ref:P=>Je(C.question.order,P),onClick:P=>Ze(C.question.order)},[U(Ve,{item:C,answer:_(r).answers[C.question.id],"is-current":_(r).currentOrder===C.question.order,"onUpdate:answer":P=>o.value?Ct(C.question.id,P):_(r).setAnswer(C.question.id,P),onJumpAudio:_t},null,8,["item","answer","is-current","onUpdate:answer"]),o.value&&se(C)?(s(),n("div",{key:0,class:N(["mt-1 overflow-hidden rounded-xl border text-[13px]",se(C).ok?"border-[#bbf7d0] bg-[#f0fdf4]":"border-[#fecaca] bg-[#fef2f2]"])},[e("div",{class:N(["flex items-center gap-2 px-4 py-2.5 font-semibold",se(C).ok?"text-[#059669]":"text-[#dc2626]"])},[se(C).ok?(s(),n("svg",To,[...t[47]||(t[47]=[e("polyline",{points:"20 6 9 17 4 12"},null,-1)])])):(s(),n("svg",Io,[...t[48]||(t[48]=[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"},null,-1),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"},null,-1)])])),L(" "+v(se(C).ok?"Đúng!":"Sai")+" ",1),se(C).ok?M("",!0):(s(),n("span",Mo,[t[49]||(t[49]=L("Đáp án đúng: ",-1)),e("strong",Ao,v(se(C).correctAnswer),1)]))],2),se(C).explain?(s(),n("div",{key:0,class:N(["border-t px-4 py-2.5 text-[12px] leading-relaxed text-[var(--ink2)]",se(C).ok?"border-[#bbf7d0]":"border-[#fecaca]"])},[t[50]||(t[50]=e("span",{class:"mr-1 font-semibold text-[var(--ink)]"},"Giải thích:",-1)),L(" "+v(se(C).explain),1)],2)):M("",!0)],2)):M("",!0)],8,So))),128))]))]))),128)),e("div",No,[e("button",{class:"btn btn-secondary",onClick:t[17]||(t[17]=h=>q.value=!0)},"Thoát"),e("button",{class:"btn btn-primary",onClick:t[18]||(t[18]=h=>ze(!1))},"Nộp bài")])],512)],512))],64)):(s(),n("div",mr,[t[22]||(t[22]=e("div",{class:"text-lg font-semibold mb-2"},"Quiz not found",-1)),U($,{to:"/dashboard",class:"btn btn-primary"},{default:ae(()=>[...t[21]||(t[21]=[L("Về trang chủ",-1)])]),_:1})]))])])}}};export{Ro as default};
|
fronted/dist/assets/QuizRunner-RdUrirGe.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{o as n,c as s,a as e,t as v,z as S,D as De,p as X,j as w,q as xt,e as q,d as oe,m as gt,g as B,f as M,F as U,y as L,J as dt,s as mt,v as Ke,r as D,w as me,n as qe,A as Ce,_ as Se,b as It,L as Mt,M as Nt,h as V,i as ie,T as Oe,x as W,C as Fe,E as Ue,G as Qe,N as At,B as Et,k as zt,l as Bt}from"./index-BnN_E-ke.js";import{_ as Lt,u as Ot,R as ct,a as vt,s as Pt,i as jt,b as Rt}from"./scoring-qifwNxYH.js";import{_ as Ht,a as Vt,b as Le,c as Ut,G as Gt,V as Ft,d as Qt}from"./AudioPlayer-COEgM5Q5.js";import{u as Dt,i as Kt,b as Xt,e as Wt}from"./mockQuiz-TyC2_r6X.js";import{u as Jt}from"./practice-BINrC2xL.js";import{h as Yt}from"./vocabularyService-Cpdp_eoK.js";import"./mockTestService-uSLqkhlt.js";const Zt={class:"sticky top-0 z-20 border-b border-[var(--border2)] bg-[var(--bg)]/90 backdrop-blur"},en={class:"exam-container flex items-center justify-between gap-3 py-3 sm:py-4"},tn={class:"min-w-0"},nn={class:"text-sm font-semibold truncate"},sn={class:"text-xs text-[var(--ink2)] truncate"},on={class:"flex items-center gap-3"},rn={class:"rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-3 py-2"},an={class:"font-mono text-sm font-semibold"},ln=["disabled"],un={__name:"ExamHeader",props:{title:{type:String,default:""},subtitle:{type:String,default:""},remainingSeconds:{type:Number,default:0},disabled:{type:Boolean,default:!1}},emits:["submit"],setup(a){const m=a,y=S(()=>{const o=Math.max(0,m.remainingSeconds||0),l=Math.floor(o/60).toString().padStart(2,"0"),r=Math.floor(o%60).toString().padStart(2,"0");return`${l}:${r}`});return(o,l)=>(n(),s("div",Zt,[e("div",en,[e("div",tn,[e("div",nn,v(a.title),1),e("div",sn,v(a.subtitle),1)]),e("div",on,[e("div",rn,[l[1]||(l[1]=e("div",{class:"text-[11px] text-[var(--ink2)]"},"Time left",-1)),e("div",an,v(y.value),1)]),e("button",{class:"btn btn-primary",onClick:l[0]||(l[0]=r=>o.$emit("submit")),disabled:a.disabled}," Submit ",8,ln)])])]))}};function dn(){const a=w(null),m=w(!1),y=w(0),o=w(0),l=w(1);let r=null;function k(){clearTimeout(r),r=setTimeout(()=>{a.value&&(y.value=a.value.currentTime||0)},50)}function f(){m.value=!0}function u(){m.value=!1}function x(){a.value&&(o.value=a.value.duration||0)}const g={play:f,pause:u,timeupdate:k,loadedmetadata:x,durationchange:x};function p(b){b&&(a.value=b,Object.entries(g).forEach(([c,T])=>b.addEventListener(c,T)),b.playbackRate=l.value)}function d(){clearTimeout(r);const b=a.value;b&&Object.entries(g).forEach(([c,T])=>b.removeEventListener(c,T))}De(d);function I(){const b=a.value;b&&(b.paused?b.play().catch(()=>{}):b.pause())}function E(b){const c=a.value;c&&(c.currentTime=Math.max(0,Math.min(c.duration||0,(c.currentTime||0)+b)))}function H(b){const c=a.value;c&&(c.currentTime=Math.max(0,Math.min(c.duration||0,Number(b)||0)))}function F(b){var c;H(b),(c=a.value)==null||c.play().catch(()=>{})}function ne(b){const c=Math.max(0,Number(b)||0);return`${Math.floor(c/60).toString().padStart(2,"0")}:${Math.floor(c%60).toString().padStart(2,"0")}`}return X(l,b=>{a.value&&(a.value.playbackRate=b)}),{audioEl:a,playing:m,currentTime:y,duration:o,playbackRate:l,attach:p,detach:d,toggle:I,seekTo:H,seekDelta:E,seekAndPlay:F,fmt:ne}}const cn={class:"card p-4"},vn={class:"flex items-start justify-between gap-3"},pn={class:"text-sm font-semibold"},fn={class:"text-xs text-[var(--ink2)]"},xn={class:"text-xs font-mono text-[var(--ink2)]"},gn=["src"],mn={class:"mt-3 flex items-center gap-2"},bn={class:"ml-auto flex items-center gap-2"},hn=["max","value"],kn={key:0,class:"mt-3 text-xs text-[var(--ink2)]"},yn={__name:"ExamAudioPlayer",props:{src:{type:String,default:""},title:{type:String,default:"Audio"},subtitle:{type:String,default:""},seekTo:{type:Number,default:null}},emits:["time"],setup(a,{expose:m,emit:y}){const o=a,l=y,r=dn(),k=w(null);xt(()=>r.attach(k.value)),X(r.currentTime,u=>l("time",u)),X(()=>o.seekTo,u=>{u!=null&&r.seekTo(u)});function f(u){r.seekAndPlay(u)}return m({seekAndPlay:f}),(u,x)=>(n(),s("div",cn,[e("div",vn,[e("div",null,[e("div",pn,v(a.title),1),e("div",fn,v(a.subtitle),1)]),e("div",xn,v(q(r).fmt(q(r).currentTime.value))+" / "+v(q(r).fmt(q(r).duration.value)),1)]),e("audio",{ref_key:"nativeAudio",ref:k,src:a.src,preload:"metadata",class:"hidden"},null,8,gn),e("div",mn,[e("button",{class:"btn btn-secondary",onClick:x[0]||(x[0]=g=>q(r).seekDelta(-5))},"-5s"),e("button",{class:"btn btn-primary",onClick:x[1]||(x[1]=g=>q(r).toggle())},v(q(r).playing.value?"Pause":"Play"),1),e("button",{class:"btn btn-secondary",onClick:x[2]||(x[2]=g=>q(r).seekDelta(5))},"+5s"),e("div",bn,[x[6]||(x[6]=e("div",{class:"text-xs text-[var(--ink2)]"},"Speed",-1)),oe(e("select",{class:"rounded-lg border border-[var(--border2)] bg-[var(--surface)] px-2 py-1 text-xs","onUpdate:modelValue":x[3]||(x[3]=g=>q(r).playbackRate.value=g)},[...x[5]||(x[5]=[e("option",{value:.75},"0.75x",-1),e("option",{value:1},"1.0x",-1),e("option",{value:1.25},"1.25x",-1),e("option",{value:1.5},"1.5x",-1)])],512),[[gt,q(r).playbackRate.value,void 0,{number:!0}]])])]),e("input",{class:"mt-3 w-full",type:"range",min:"0",max:Math.max(0,q(r).duration.value),step:"0.01",value:q(r).currentTime.value,onInput:x[4]||(x[4]=g=>q(r).seekTo(Number(g.target.value)))},null,40,hn),a.src?M("",!0):(n(),s("div",kn,[...x[7]||(x[7]=[B(" Chưa cấu hình audio CDN. Hãy set ",-1),e("code",null,"VITE_AUDIO_CDN_BASE",-1),B(" trong ",-1),e("code",null,"fronted/.env",-1),B(". ",-1)])]))]))}},wn={class:"card overflow-hidden"},_n={class:"flex items-center justify-between px-4 py-3 border-b border-[var(--border)]"},$n={class:"rounded-full bg-[var(--bg2)] px-2 py-0.5 text-[10px] text-[var(--ink3)]"},Cn={class:"flex items-center gap-2"},qn={class:"font-mono text-[10px] text-[var(--ink3)]"},Sn=["title"],Tn={key:0,width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},In={key:1,width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},Mn={class:"border-b border-[var(--border)] px-3 py-2"},Nn={class:"relative"},An={key:0,class:"py-6 text-center text-[12px] text-[var(--ink3)]"},En=["onClick"],zn={class:"mt-0.5 shrink-0 font-mono text-[10px] text-[var(--ink3)]"},Bn=["innerHTML"],Ln=["onClick"],On={__name:"TranscriptPanel",props:{paragraphs:{type:Array,default:()=>[]},currentTime:{type:Number,default:0},highlightedIds:{type:Object,default:()=>new Set}},emits:["seek","save-word"],setup(a){const m=a,y=w(!0),o=w(!1),l=w(null),r=new Map,k=w(!1),f=w(""),u=w(null);function x(){k.value=!k.value,k.value?Ce(()=>{var b;return(b=u.value)==null?void 0:b.focus()}):g()}function g(){f.value="",k.value=!1}function p(b){if(!f.value)return b;const c=f.value.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),T=new RegExp(`(${c})`,"gi");return b.replace(T,'<mark style="background:#fef08a;border-radius:2px;padding:0 1px">$1</mark>')}const d=S(()=>{const b=[];for(const c of m.paragraphs)for(const T of c.children||[])!Number.isFinite(T.from)||!Number.isFinite(T.to)||b.push({id:T.id,from:T.from,to:T.to,speaker:T.speaker,text:T.text});return b}),I=S(()=>{if(!f.value)return d.value;const b=f.value.toLowerCase();return d.value.filter(c=>c.text.toLowerCase().includes(b))}),E=S(()=>{var c;const b=m.currentTime||0;return((c=d.value.find(T=>b>=T.from&&b<=T.to))==null?void 0:c.id)??null});function H(b){return m.highlightedIds.size>0?m.highlightedIds.has(b.id):b.id===E.value}async function F(b){if(!b||!y.value)return;await Ce();const c=r.get(b);c&&c.scrollIntoView({behavior:"smooth",block:"nearest"})}X(()=>m.highlightedIds,async b=>{if(b.size>0){const c=[...b][0];await F(c)}}),X(E,b=>{m.highlightedIds.size===0&&F(b)});function ne(b){const c=Math.max(0,Number(b)||0),T=Math.floor(c/60).toString().padStart(2,"0"),Y=Math.floor(c%60).toString().padStart(2,"0");return`${T}:${Y}`}return(b,c)=>(n(),s("div",wn,[e("div",_n,[e("button",{class:"flex items-center gap-2 text-left",onClick:c[0]||(c[0]=T=>y.value=!y.value)},[c[4]||(c[4]=e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round"},[e("path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"})],-1)),c[5]||(c[5]=e("span",{class:"text-[12px] font-semibold text-[var(--ink)]"},"Transcript",-1)),e("span",$n,[B(v(I.value.length),1),f.value?(n(),s(U,{key:0},[B(" / "+v(d.value.length),1)],64)):M("",!0)])]),e("div",Cn,[e("span",qn,v(ne(a.currentTime)),1),e("button",{class:L(["flex h-6 w-6 items-center justify-center rounded transition-colors",k.value?"bg-[#34d399] text-white":"text-[var(--ink3)] hover:bg-[var(--bg2)]"]),title:"Tìm kiếm trong transcript",onClick:x},[...c[6]||(c[6]=[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("circle",{cx:"11",cy:"11",r:"8"}),e("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})],-1)])],2),e("button",{class:"flex h-6 w-6 items-center justify-center rounded text-[var(--ink3)] hover:bg-[var(--bg2)] transition-colors",title:o.value?"Thu nhỏ transcript":"Mở rộng transcript",onClick:c[1]||(c[1]=T=>o.value=!o.value)},[o.value?(n(),s("svg",In,[...c[8]||(c[8]=[e("path",{d:"M4 14h6m0 0v6m0-6l-7 7m17-11h-6m0 0V4m0 6l7-7"},null,-1)])])):(n(),s("svg",Tn,[...c[7]||(c[7]=[e("path",{d:"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"},null,-1)])]))],8,Sn),e("button",{onClick:c[2]||(c[2]=T=>y.value=!y.value),class:"flex h-6 w-6 items-center justify-center rounded text-[var(--ink3)] hover:bg-[var(--bg2)]"},[(n(),s("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2",class:L(["transition-transform",y.value?"rotate-180":""])},[...c[9]||(c[9]=[e("polyline",{points:"6 9 12 15 18 9"},null,-1)])],2))])])]),oe(e("div",Mn,[e("div",Nn,[c[11]||(c[11]=e("svg",{class:"absolute left-2.5 top-1/2 -translate-y-1/2 text-[var(--ink3)]",width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("circle",{cx:"11",cy:"11",r:"8"}),e("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})],-1)),oe(e("input",{ref_key:"searchInputRef",ref:u,"onUpdate:modelValue":c[3]||(c[3]=T=>f.value=T),class:"w-full rounded-lg border border-[var(--border2)] bg-[var(--bg)] py-1.5 pl-7 pr-3 text-[12px] outline-none focus:border-[#34d399]",placeholder:"Tìm từ trong transcript...",onKeydown:mt(g,["escape"])},null,544),[[Ke,f.value]]),f.value?(n(),s("button",{key:0,onClick:g,class:"absolute right-2.5 top-1/2 -translate-y-1/2 text-[var(--ink3)] hover:text-[var(--ink)]"},[...c[10]||(c[10]=[e("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"})],-1)])])):M("",!0)])],512),[[dt,k.value&&y.value]]),oe(e("div",{class:"overflow-y-auto border-t border-[var(--border)] px-3 py-2 transition-all",style:qe({height:o.value?"calc(100vh - 380px)":"13rem",maxHeight:o.value?"none":"13rem"}),ref_key:"listEl",ref:l},[!I.value.length&&f.value?(n(),s("div",An,' Không tìm thấy "'+v(f.value)+'" trong transcript. ',1)):M("",!0),(n(!0),s(U,null,D(I.value,T=>(n(),s("div",{key:T.id,ref_for:!0,ref:Y=>{Y&&q(r).set(T.id,Y)},class:L(["group mb-1 flex cursor-pointer gap-2 rounded-lg border px-2 py-1.5 transition-all",H(T)?"border-[#34d399] bg-[#f0fdf4]":"border-transparent hover:bg-[var(--bg)]"]),onClick:Y=>b.$emit("seek",T.from)},[e("span",zn,v(ne(T.from)),1),e("span",{class:L(["text-[12px] leading-relaxed transition-all",H(T)?"font-semibold text-[var(--ink)]":"text-[var(--ink2)]"]),innerHTML:p(T.text)},null,10,Bn),b.$attrs.onSaveWord?(n(),s("button",{key:0,class:"ml-auto shrink-0 opacity-0 group-hover:opacity-100 flex h-5 w-5 items-center justify-center rounded text-[var(--ink3)] hover:bg-[#34d399] hover:text-white transition-all",title:"Lưu từ vựng",onClick:me(Y=>b.$emit("save-word",T.text),["stop"])},[...c[12]||(c[12]=[e("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},[e("line",{x1:"12",y1:"5",x2:"12",y2:"19"}),e("line",{x1:"5",y1:"12",x2:"19",y2:"12"})],-1)])],8,Ln)):M("",!0)],10,En))),128))],4),[[dt,y.value]])]))}},Pn={class:"overflow-hidden rounded-xl border border-[var(--border)] bg-white"},jn={class:"flex items-center overflow-x-auto px-3 py-2.5 gap-1 scrollbar-hide"},Rn=["onClick"],Hn={class:"text-[10px] font-normal opacity-70"},Vn=["onClick"],Un={class:"flex items-center gap-4 border-t border-[var(--border)] px-3 py-1.5 text-[10px] text-[var(--ink3)]"},Gn={class:"ml-auto font-medium text-[var(--ink2)]"},Fn={__name:"QuestionNavGrid",props:{questions:{type:Array,default:()=>[]},navParts:{type:Array,default:()=>[]},currentOrder:{type:Number,default:0},answeredMap:{type:Object,default:()=>({})}},emits:["go"],setup(a){const m=a,y=S(()=>{var k;return(k=m.navParts)!=null&&k.length?m.navParts.map(f=>({partId:f.id,label:f.label,questions:f.questions,hasCurrent:f.questions.some(u=>u.order===m.currentOrder),answered:f.questions.filter(u=>{var x;return((x=m.answeredMap)==null?void 0:x[u.id])!==void 0}).length})):[{partId:0,label:"Câu hỏi",questions:m.questions,hasCurrent:m.questions.some(f=>f.order===m.currentOrder),answered:m.questions.filter(f=>{var u;return((u=m.answeredMap)==null?void 0:u[f.id])!==void 0}).length}]}),o=S(()=>m.questions.length||y.value.reduce((k,f)=>k+f.questions.length,0)),l=S(()=>Object.keys(m.answeredMap||{}).length);function r(k){var x;const f=k.order===m.currentOrder,u=((x=m.answeredMap)==null?void 0:x[k.id])!==void 0;return f?"bg-[#111] border-[#111] text-white":u?"bg-[#f0fdf4] border-[#34d399] text-[#065f46]":"bg-white border-[var(--border2)] text-[var(--ink3)] hover:border-[var(--ink2)]"}return(k,f)=>(n(),s("div",Pn,[e("div",jn,[(n(!0),s(U,null,D(y.value,u=>(n(),s(U,{key:u.partId},[e("button",{class:L(["shrink-0 flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-[12px] font-semibold transition-colors whitespace-nowrap",u.hasCurrent?"border-[#111] bg-[#111] text-white":"border-[var(--border2)] bg-white text-[var(--ink)]"]),onClick:x=>{var g;return k.$emit("go",(g=u.questions[0])==null?void 0:g.order)}},[B(v(u.label)+" ",1),e("span",Hn,v(u.answered)+"/"+v(u.questions.length),1)],10,Rn),(n(!0),s(U,null,D(u.questions,x=>(n(),s("button",{key:x.order,class:L(["shrink-0 flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-semibold transition-colors border",r(x)]),onClick:g=>k.$emit("go",x.order)},v(x.order),11,Vn))),128))],64))),128))]),e("div",Un,[f[0]||(f[0]=It('<div class="flex items-center gap-1" data-v-edab0552><span class="inline-block h-3 w-3 rounded-full border border-[var(--border2)] bg-white" data-v-edab0552></span> Chưa làm </div><div class="flex items-center gap-1" data-v-edab0552><span class="inline-block h-3 w-3 rounded-full border border-[#34d399] bg-[#f0fdf4]" data-v-edab0552></span> Đã làm </div><div class="flex items-center gap-1" data-v-edab0552><span class="inline-block h-3 w-3 rounded-full bg-[#111]" data-v-edab0552></span> Đang làm </div>',3)),e("div",Gn,v(l.value)+"/"+v(o.value)+" câu",1)])]))}},pt=Se(Fn,[["__scopeId","data-v-edab0552"]]),Qn={class:"flex items-start gap-2"},Dn={class:"flex shrink-0 items-center gap-1.5"},Kn=["title"],Xn={class:"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--bg2)] text-[11px] font-bold text-[var(--ink2)]"},Wn={class:"flex-1"},Jn={key:0,class:"text-sm font-semibold"},Yn=["innerHTML"],Zn={key:0,class:"mt-3"},es={class:"grid gap-2"},ts=["name","value"],ns={class:"text-sm"},ss={class:"font-semibold mr-2"},os={key:1,class:"mt-3"},rs={class:"text-xs text-[var(--ink2)] mb-2"},is={class:"grid gap-2"},as=["value","disabled"],ls={class:"text-sm"},us={class:"font-semibold mr-2"},ds={key:2,class:"mt-3"},cs=["value"],vs={key:3,class:"mt-5"},ps={class:"mt-1 text-[13px] text-[var(--ink2)]"},fs={class:"flex flex-col items-center gap-1.5"},xs=["width","height"],gs={key:1,width:"14",height:"14",viewBox:"0 0 24 24",fill:"currentColor"},ms={key:0,class:"text-[10px] font-medium text-[#059669]"},bs=["src"],hs={key:1,class:"mt-2 rounded-lg border border-[#f59e0b33] bg-[#fef9ee] px-3 py-2 text-[11px] text-[#b45309]"},ks=["disabled"],ys={key:4,class:"mt-3"},ws={class:"text-xs text-[var(--ink2)]"},ge=2,_s={__name:"QuestionRenderer",props:{item:{type:Object,required:!0},answer:{type:[String,Array],default:void 0},isCurrent:{type:Boolean,default:!1},speakingCompact:{type:Boolean,default:!1}},emits:["update:answer","jump-audio","evaluate-speaking"],setup(a,{emit:m}){const y=a,o=m,l=S(()=>y.item.question||{}),r=S(()=>y.item.questionSetType||""),k=S(()=>y.item.questionSetOptions||[]),f=S(()=>y.item.questionSetMaxSelections||0),u=S(()=>{const _=String(r.value||l.value.question_type||"").toUpperCase();return _==="SPEAKING"||_==="speaking"||String(l.value.type||"").toLowerCase()==="speaking"?"speaking":_==="MULTIPLE_CHOICE_MANY"?"multi":_==="TABLE_SELECTION"||_==="MATCHING"||_==="MATCHING_FEATURES"||_==="MATCHING_INFO"||_==="MATCHING_HEADING"||_==="MATCHING_HEADINGS"||_==="MATCHING_ENDINGS"?"select":_==="SINGLE_SELECTION"||_==="SINGLE_CHOICE"||_==="MULTIPLE_CHOICE_ONE"?"single":"unknown"}),x=w(!1),g=w(null),p=w(null),d=w(0),I=w(0);let E=null,H=[],F=null;async function ne(){if(x.value){I.value=d.value,E==null||E.stop(),x.value=!1,clearInterval(F);return}try{const _=await navigator.mediaDevices.getUserMedia({audio:!0});H=[],d.value=0,I.value=0,E=new MediaRecorder(_),E.ondataavailable=N=>H.push(N.data),E.onstop=()=>{const N=new Blob(H,{type:"audio/webm"});p.value=N,g.value=URL.createObjectURL(N),_.getTracks().forEach(A=>A.stop()),o("update:answer","recorded")},E.start(),x.value=!0,F=setInterval(()=>d.value++,1e3)}catch(_){console.warn("Microphone access denied:",_)}}De(()=>{x.value&&(E==null||E.stop()),clearInterval(F)});const b=S(()=>I.value>=ge),c=S(()=>b.value?"background:#34d399; cursor:pointer;":"background:#a7f3d0; cursor:not-allowed; opacity:0.6;");function T(_){b.value&&(_.currentTarget.style.background="#059669")}function Y(_){b.value&&(_.currentTarget.style.background="#34d399")}function Te(){b.value&&o("evaluate-speaking",{blob:p.value,questionText:l.value.text||l.value.title||"",questionId:l.value.id})}const ve=S(()=>Number.isFinite(l.value.listen_from)),pe=S(()=>{const _=Array.isArray(l.value.options)?l.value.options:[],N=Array.isArray(k.value)?k.value:[];return(_.length?_:N).map(K=>({option:K.option,text:K.text}))}),Ie=S(()=>(Array.isArray(l.value.options)?l.value.options:[]).map(N=>({option:N.option,text:N.text}))),Me=S(()=>{const _=Array.isArray(l.value.options)?l.value.options:[],N=Array.isArray(k.value)?k.value:[];return(_.length?_:N).map(K=>({option:K.option,text:K.text}))}),Z=w(""),J=w([]);X(()=>y.answer,_=>{Array.isArray(_)?(J.value=_,Z.value=""):typeof _=="string"?(Z.value=_,J.value=[]):(Z.value="",J.value=[])},{immediate:!0}),X(Z,_=>{(u.value==="single"||u.value==="select")&&o("update:answer",_)}),X(J,_=>{u.value==="multi"&&o("update:answer",_)},{deep:!0});function Ne(_){return!f.value||J.value.includes(_)?!1:J.value.length>=f.value}return(_,N)=>(n(),s("div",{class:L(["p-4",[a.speakingCompact?"speaking-question":"card",a.isCurrent&&!a.speakingCompact?"ring-2 ring-[rgba(124,106,247,0.35)]":"",a.isCurrent&&a.speakingCompact?"speaking-question--current":""]])},[e("div",Qn,[e("div",Dn,[ve.value?(n(),s("button",{key:0,class:"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[#34d399] text-white transition-colors hover:bg-[#059669]",title:`Play from ${Math.floor(l.value.listen_from)}s`,onClick:N[0]||(N[0]=me(A=>_.$emit("jump-audio",{time:l.value.listen_from,locateInfo:l.value.locate_info}),["stop"]))},[...N[4]||(N[4]=[e("svg",{width:"8",height:"8",viewBox:"0 0 24 24",fill:"currentColor"},[e("polygon",{points:"5 3 19 12 5 21 5 3"})],-1)])],8,Kn)):M("",!0),e("div",Xn,v(l.value.order),1)]),e("div",Wn,[l.value.text||l.value.title?(n(),s("div",Jn,v(l.value.text||l.value.title),1)):l.value.content?(n(),s("div",{key:1,class:"text-sm",innerHTML:l.value.content},null,8,Yn)):M("",!0)])]),u.value==="single"?(n(),s("div",Zn,[e("div",es,[(n(!0),s(U,null,D(pe.value,A=>(n(),s("label",{key:A.option,class:"flex items-center gap-2 rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-3 py-2 cursor-pointer hover:border-[var(--ink3)]"},[oe(e("input",{type:"radio",name:`q_${l.value.id}`,value:A.option,"onUpdate:modelValue":N[1]||(N[1]=K=>Z.value=K)},null,8,ts),[[Mt,Z.value]]),e("div",ns,[e("span",ss,v(A.option),1),e("span",null,v(A.text),1)])]))),128))])])):u.value==="multi"?(n(),s("div",os,[e("div",rs,"Choose "+v(f.value)+" answers",1),e("div",is,[(n(!0),s(U,null,D(Ie.value,A=>(n(),s("label",{key:A.option,class:"flex items-center gap-2 rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-3 py-2 cursor-pointer hover:border-[var(--ink3)]"},[oe(e("input",{type:"checkbox",value:A.option,"onUpdate:modelValue":N[2]||(N[2]=K=>J.value=K),disabled:Ne(A.option)},null,8,as),[[Nt,J.value]]),e("div",ls,[e("span",us,v(A.option),1),e("span",null,v(A.text),1)])]))),128))])])):u.value==="select"?(n(),s("div",ds,[N[6]||(N[6]=e("div",{class:"text-xs text-[var(--ink2)] mb-2"},"Select answer",-1)),oe(e("select",{class:"w-full rounded-xl border border-[var(--border2)] bg-[var(--surface)] px-3 py-2","onUpdate:modelValue":N[3]||(N[3]=A=>Z.value=A)},[N[5]||(N[5]=e("option",{value:""},"—",-1)),(n(!0),s(U,null,D(Me.value,A=>(n(),s("option",{key:A.option,value:A.option},v(A.option),9,cs))),128))],512),[[gt,Z.value]])])):u.value==="speaking"?(n(),s("div",vs,[e("div",{class:L(["rounded-xl border border-[var(--border)] bg-[var(--bg)] p-5",a.speakingCompact?"flex flex-col items-center gap-4 text-center":"flex items-center justify-between gap-4"])},[e("div",{class:L(a.speakingCompact?"w-full":"")},[N[7]||(N[7]=e("div",{class:"text-[11px] font-semibold uppercase tracking-wider text-[var(--ink3)]"}," No time limit ",-1)),e("div",ps,v(x.value?`Đang ghi âm... ${d.value}s`:p.value?"Đã ghi xong":"Nhấn micro để bắt đầu ghi âm"),1)],2),e("div",fs,[e("button",{type:"button",onClick:ne,class:L(["flex items-center justify-center rounded-full border-2 transition-all",[a.speakingCompact?"h-16 w-16":"h-12 w-12",x.value?"border-[#e11d48] bg-[#e11d48] text-white animate-pulse":"border-[#34d399] bg-[#f0fdf4] text-[#34d399] hover:bg-[#34d399] hover:text-white"]])},[x.value?(n(),s("svg",gs,[...N[9]||(N[9]=[e("rect",{x:"6",y:"6",width:"12",height:"12",rx:"2"},null,-1)])])):(n(),s("svg",{key:0,width:a.speakingCompact?22:18,height:a.speakingCompact?22:18,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round"},[...N[8]||(N[8]=[e("path",{d:"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"},null,-1),e("path",{d:"M19 10v2a7 7 0 0 1-14 0v-2"},null,-1),e("line",{x1:"12",y1:"19",x2:"12",y2:"23"},null,-1),e("line",{x1:"8",y1:"23",x2:"16",y2:"23"},null,-1)])],8,xs))],2),p.value?(n(),s("div",ms,"✓ Đã ghi")):M("",!0)])],2),g.value?(n(),s("audio",{key:0,src:g.value,controls:"",class:"mt-2 w-full",style:{height:"32px"}},null,8,bs)):M("",!0),p.value&&!x.value&&I.value<ge?(n(),s("div",hs," Bài ghi quá ngắn ("+v(I.value)+"s). Ghi lại ít nhất "+v(ge)+" giây để đánh giá chính xác. ",1)):M("",!0),p.value&&!x.value?(n(),s("button",{key:2,disabled:I.value<ge,class:"mt-2 flex w-full items-center justify-center gap-2 rounded-xl border-none py-2.5 text-sm font-semibold text-white transition-all",style:qe(c.value),onMouseenter:T,onMouseleave:Y,onClick:Te},[N[10]||(N[10]=e("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"}),e("path",{d:"M19 10v2a7 7 0 0 1-14 0v-2"})],-1)),B(" "+v(I.value<ge?`Ghi thêm ${ge-I.value}s...`:"Đánh giá bài nói"),1)],44,ks)):M("",!0)])):(n(),s("div",ys,[e("div",ws,"Unsupported question type: "+v(l.value.question_type),1)]))],2))}},Ge=Se(_s,[["__scopeId","data-v-1392778c"]]),$s={class:"flex items-start justify-between gap-3"},Cs={class:"text-xs font-semibold text-[var(--ink2)]"},qs={class:"text-xs text-[var(--ink2)]"},Ss=["innerHTML"],Ts={__name:"GapFillingSet",props:{title:{type:String,default:""},description:{type:String,default:""},html:{type:String,default:""},questions:{type:Array,default:()=>[]},answers:{type:Object,default:()=>({})},isCurrent:{type:Boolean,default:!1}},emits:["answer"],setup(a,{emit:m}){const y=a,o=m,l=w(null),r=S(()=>[...y.questions].sort((u,x)=>(u.sort??0)-(x.sort??0))),k=S(()=>{const u={};return r.value.forEach((x,g)=>{var p;u[`gf_${g+1}`]={questionId:x.id,value:((p=y.answers)==null?void 0:p[x.id])??""}}),u});function f({gapKey:u,value:x}){const g=String(u||"").match(/^gf_(\d+)$/);if(!g)return;const p=Number(g[1])-1,d=r.value[p];d!=null&&d.id&&o("answer",{questionId:d.id,value:x})}return(u,x)=>(n(),s("div",{class:L(["card p-4",a.isCurrent?"ring-2 ring-[rgba(124,106,247,0.35)]":""])},[e("div",$s,[e("div",Cs,v(a.title),1),e("div",qs,v(a.questions.length)+" gaps",1)]),a.description?(n(),s("div",{key:0,class:"mt-2 text-sm text-[var(--ink2)]",innerHTML:a.description},null,8,Ss)):M("",!0),e("div",{class:"mt-4",ref_key:"rootEl",ref:l},[V(Lt,{html:a.html,gaps:k.value,onAnswer:f},null,8,["html","gaps"])],512)],2))}},Is={class:"fixed left-4 top-1/2 z-[150] -translate-y-1/2 flex flex-col gap-2"},Ms={class:"relative"},Ns={key:0,class:"absolute left-14 top-0 flex gap-1.5 rounded-xl border border-[var(--border)] bg-white p-1.5 shadow-lg"},As=["title","onClick"],Es={key:0,class:"note-panel"},zs={class:"note-panel__header"},Bs={class:"note-panel__footer"},Ls={class:"text-[10px] text-[var(--ink3)]"},Os={__name:"PracticeToolbar",props:{practiceMode:{type:Boolean,default:!1},modelNote:{type:String,default:""}},emits:["update:modelNote","highlight-applied","tool-changed"],setup(a,{expose:m,emit:y}){const o=a,l=y,{activeTool:r,setTool:k}=Ot(),f=w("yellow"),u=w(o.modelNote),x=[{value:"yellow",label:"Vàng",bg:"#fef08a"},{value:"green",label:"Xanh lá",bg:"#bbf7d0"},{value:"rose",label:"Hồng",bg:"#fecdd3"},{value:"blue",label:"Xanh dương",bg:"#bfdbfe"}];return X(u,g=>l("update:modelNote",g)),X(r,g=>l("tool-changed",{tool:g,color:f.value})),X(f,g=>l("tool-changed",{tool:r.value,color:g})),m({activeTool:r,highlightColor:f,noteText:u}),(g,p)=>a.practiceMode?(n(),s(U,{key:0},[e("div",Is,[e("div",Ms,[e("button",{class:L(["tool-btn",q(r)==="highlight"?"tool-btn--highlight-active":"tool-btn--default hover:tool-btn--highlight-hover"]),title:"Tô màu (T)",onClick:p[0]||(p[0]=d=>q(k)("highlight"))},[...p[5]||(p[5]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("path",{d:"m9 11-6 6v3h9l3-3"}),e("path",{d:"m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"})],-1),e("span",{class:"tool-key"},"T",-1)])],2),V(Oe,{name:"slide-x"},{default:ie(()=>[q(r)==="highlight"?(n(),s("div",Ns,[(n(),s(U,null,D(x,d=>e("button",{key:d.value,title:d.label,class:L(["h-6 w-6 rounded-lg border-2 transition-transform hover:scale-110",f.value===d.value?"border-[var(--ink)]":"border-transparent"]),style:qe({background:d.bg}),onClick:me(I=>f.value=d.value,["stop"])},null,14,As)),64))])):M("",!0)]),_:1})]),e("button",{class:L(["tool-btn",q(r)==="note"?"tool-btn--note-active":"tool-btn--default hover:tool-btn--note-hover"]),title:"Ghi chú (N)",onClick:p[1]||(p[1]=d=>q(k)("note"))},[...p[6]||(p[6]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})],-1),e("span",{class:"tool-key"},"N",-1)])],2),e("button",{class:L(["tool-btn",q(r)==="vocab"?"tool-btn--vocab-active":"tool-btn--default hover:tool-btn--vocab-hover"]),title:"Tra từ vựng (S)",onClick:p[2]||(p[2]=d=>q(k)("vocab"))},[...p[7]||(p[7]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("circle",{cx:"11",cy:"11",r:"8"}),e("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})],-1),e("span",{class:"tool-key"},"S",-1)])],2),p[8]||(p[8]=e("div",{class:"mx-auto h-px w-8 bg-[var(--border)]"},null,-1)),p[9]||(p[9]=e("div",{class:"flex h-11 w-11 items-center justify-center text-[9px] text-[var(--ink3)] leading-tight text-center"},[B(" Công"),e("br"),B("cụ ")],-1))]),(n(),W(Fe,{to:"body"},[V(Oe,{name:"slide-right"},{default:ie(()=>[q(r)==="note"?(n(),s("div",Es,[e("div",zs,[p[11]||(p[11]=e("div",{class:"flex items-center gap-2 text-sm font-semibold text-[var(--ink)]"},[e("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}),e("path",{d:"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"})]),B(" Ghi chú ")],-1)),e("button",{class:"icon-close",onClick:p[3]||(p[3]=d=>q(k)("note"))},[...p[10]||(p[10]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"})],-1)])])]),oe(e("textarea",{"onUpdate:modelValue":p[4]||(p[4]=d=>u.value=d),class:"flex-1 resize-none p-4 text-[13px] text-[var(--ink)] outline-none leading-relaxed",placeholder:"Ghi chú của bạn tại đây..."},null,512),[[Ke,u.value]]),e("div",Bs,[e("span",Ls,v(u.value.length)+" ký tự",1)])])):M("",!0)]),_:1})]))],64)):M("",!0)}},Ps=Se(Os,[["__scopeId","data-v-daae9535"]]),js={class:"mb-5 flex flex-wrap items-center gap-3"},Rs={class:"flex min-w-0 flex-1 flex-wrap items-center gap-2 sm:gap-3"},Hs={class:"flex items-center gap-1.5 rounded-full border border-[var(--border)] bg-white px-4 py-1.5 text-[12px]"},Vs={class:"font-bold text-[var(--ink)]"},Us={class:"text-[var(--ink3)]"},Gs={class:"hidden gap-1 sm:flex"},Fs={class:"h-1.5 flex-1 min-w-[80px] overflow-hidden rounded-full bg-[var(--border2)] sm:max-w-[200px]"},Qs={class:"p-6 sm:p-8"},Ds={key:0,class:"mb-4 text-[11px] font-bold uppercase tracking-wider text-[var(--ink3)]"},Ks={class:"flex items-center justify-between gap-3 border-t border-[var(--border)] bg-[var(--bg)] px-6 py-4 sm:px-8"},Xs=["disabled"],Ws={class:"text-[11px] text-[var(--ink3)]"},Js=["disabled"],Ys={key:0,class:"mt-6"},Zs={__name:"SpeakingPracticePanel",props:{currentIndex:{type:Number,default:0},total:{type:Number,default:1},partTitle:{type:String,default:""},chatOpen:{type:Boolean,default:!1},canPrev:{type:Boolean,default:!1},canNext:{type:Boolean,default:!1}},emits:["exit","prev","next","toggle-chat"],setup(a){const m=a,y=S(()=>m.total?Math.round((m.currentIndex+1)/m.total*100):0);function o(l){return l<m.currentIndex?"bg-[#34d399]":l===m.currentIndex?"bg-[#111]":"bg-[var(--border2)]"}return(l,r)=>(n(),s("div",{class:L(["speaking-practice mx-auto w-full",a.chatOpen?"max-w-6xl":"max-w-3xl"])},[e("div",js,[e("button",{type:"button",class:"ct-btn flex shrink-0 items-center gap-1.5 px-3 py-1.5 text-[12px]",onClick:r[0]||(r[0]=k=>l.$emit("exit"))},[...r[4]||(r[4]=[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"15 18 9 12 15 6"})],-1),B(" Thoát ",-1)])]),e("div",Rs,[e("div",Hs,[e("span",Vs,"Question "+v(a.currentIndex+1),1),e("span",Us,"/ "+v(a.total),1)]),e("div",Gs,[(n(!0),s(U,null,D(a.total,(k,f)=>(n(),s("div",{key:f,class:L(["h-2 w-2 rounded-full transition-colors",o(f)])},null,2))),128))]),e("div",Fs,[e("div",{class:"h-full rounded-full bg-[#34d399] transition-all duration-300",style:qe({width:`${y.value}%`})},null,4)])]),e("button",{type:"button",class:L(["flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-[11px] font-semibold transition-colors",a.chatOpen?"border-[#34d399] bg-[#34d39911] text-[#34d399]":"border-[var(--border2)] bg-white text-[var(--ink2)] hover:border-[#34d399] hover:text-[#34d399]"]),onClick:r[1]||(r[1]=k=>l.$emit("toggle-chat"))},[...r[5]||(r[5]=[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"currentColor"},[e("path",{d:"M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"})],-1),B(" Need help? ",-1)])],2)]),e("div",{class:L(["flex overflow-hidden rounded-2xl border border-[var(--border)] bg-white shadow-sm",a.chatOpen?"flex-col lg:flex-row":"flex-col"])},[e("div",{class:L(["flex min-w-0 flex-1 flex-col",a.chatOpen?"lg:border-r lg:border-[var(--border)]":""])},[e("div",Qs,[a.partTitle?(n(),s("div",Ds,v(a.partTitle),1)):M("",!0),Ue(l.$slots,"default")]),e("div",Ks,[e("button",{type:"button",class:L(["ct-btn flex items-center gap-1.5 px-4 py-2 text-[13px]",a.canPrev?"":"pointer-events-none opacity-30"]),disabled:!a.canPrev,onClick:r[2]||(r[2]=k=>l.$emit("prev"))},[...r[6]||(r[6]=[e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"15 18 9 12 15 6"})],-1),B(" Previous ",-1)])],10,Xs),e("span",Ws,v(a.currentIndex+1)+" / "+v(a.total),1),e("button",{type:"button",class:L(["ct-btn flex items-center gap-1.5 px-4 py-2 text-[13px]",a.canNext?"":"pointer-events-none opacity-30"]),disabled:!a.canNext,onClick:r[3]||(r[3]=k=>l.$emit("next"))},[...r[7]||(r[7]=[B(" Next ",-1),e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"9 18 15 12 9 6"})],-1)])],10,Js)])],2),Ue(l.$slots,"chat")],2),l.$slots.feedback?(n(),s("div",Ys,[Ue(l.$slots,"feedback")])):M("",!0)],2))}},eo={class:"flex h-full w-80 shrink-0 flex-col overflow-hidden border-l border-[var(--border)] bg-white"},to={class:"flex shrink-0 items-center justify-between border-b border-[var(--border)] px-4 py-3"},no={class:"flex items-center gap-2"},so={key:0,class:"rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-[11px] text-[var(--ink2)]"},oo={key:0,class:"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[var(--bg2)] text-[10px] font-bold text-[var(--ink2)]"},ro={key:0,class:"flex items-center gap-1"},io={key:1,style:{"white-space":"pre-wrap"}},ao={class:"shrink-0 border-t border-[var(--border)] p-3"},lo={class:"mb-2 flex flex-wrap gap-1.5"},uo=["disabled","onClick"],co={class:"flex gap-2"},vo=["disabled","onKeydown"],po=["disabled"],fo={__name:"SpeakingChatbot",props:{questionText:{type:String,default:""}},emits:["close"],setup(a){const m=a,y=w(""),o=w(!1),l=w([]),r=w(null);let k=0;const f=["Start quickly","3 key vocab","1 band tip"];async function u(){await Ce(),r.value&&(r.value.scrollTop=r.value.scrollHeight)}X(()=>m.questionText,(p,d)=>{p&&p!==d&&l.value.length>0&&(l.value.push({id:k++,role:"assistant",text:`📌 New question: "${p}"`,loading:!1}),u())});async function x(p){var I;if(!p.trim()||o.value)return;l.value.push({id:k++,role:"user",text:p,loading:!1});const d={id:k++,role:"assistant",text:"",loading:!0};l.value.push(d),o.value=!0,await u();try{const E=l.value.filter(F=>!F.loading&&F.id<d.id&&!F.text.startsWith("📌")).map(F=>({role:F.role==="user"?"user":"assistant",content:F.text})),{data:H}=await Qe.post("/speaking/chat",{question_text:m.questionText,user_message:p,history:E},{timeout:9e4});d.loading=!1,d.text=(H==null?void 0:H.reply)||(H==null?void 0:H.error)||"Không nhận được phản hồi."}catch(E){d.loading=!1;const H=(I=E.response)==null?void 0:I.data;d.text=(H==null?void 0:H.error)||(H==null?void 0:H.detail)||E.message||"Lỗi kết nối AI. Thử lại sau."}finally{o.value=!1,await u()}}function g(){const p=y.value.trim();p&&(y.value="",x(p))}return(p,d)=>(n(),s("div",eo,[e("div",to,[e("div",no,[e("button",{class:"flex h-6 w-6 items-center justify-center rounded text-[var(--ink3)] transition hover:bg-[var(--bg2)]",onClick:d[0]||(d[0]=I=>p.$emit("close"))},[...d[2]||(d[2]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"})],-1)])]),d[3]||(d[3]=e("span",{class:"text-[12px] font-bold text-[var(--ink)]"},"Catbot – Personal Tutor",-1))]),d[4]||(d[4]=e("div",{class:"flex items-center gap-1 text-[11px] text-[var(--ink3)]"},[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("circle",{cx:"12",cy:"12",r:"10"}),e("polyline",{points:"12 6 12 12 16 14"})]),B(" History ")],-1))]),e("div",{ref_key:"scrollEl",ref:r,class:"chat-scroll flex-1 min-h-0 space-y-3 overflow-y-auto p-4"},[d[7]||(d[7]=e("div",{class:"flex items-start gap-2"},[e("div",{class:"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[var(--bg2)] text-[10px] font-bold text-[var(--ink2)]"},"AI"),e("div",{class:"rounded-xl rounded-tl-none bg-[var(--bg)] p-3 text-[12px] leading-relaxed text-[var(--ink)]"}," Hey! I am your personal tutor. Need help with the speaking task? Go ahead and ask. ")],-1)),a.questionText?(n(),s("div",so,[d[5]||(d[5]=e("span",{class:"font-semibold text-[#34d399]"},"Current question: ",-1)),B(v(a.questionText),1)])):M("",!0),(n(!0),s(U,null,D(l.value,I=>(n(),s("div",{key:I.id,class:L(["flex items-start gap-2",I.role==="user"?"flex-row-reverse":""])},[I.role==="assistant"?(n(),s("div",oo,"AI")):M("",!0),e("div",{class:L(["max-w-[85%] rounded-xl p-3 text-[12px] leading-relaxed",I.role==="user"?"rounded-tr-none bg-[#111] text-white":"rounded-tl-none bg-[var(--bg)] text-[var(--ink)]"])},[I.loading?(n(),s("span",ro,[...d[6]||(d[6]=[e("span",{class:"animate-bounce text-[var(--ink3)]"},"●",-1),e("span",{class:"animate-bounce text-[var(--ink3)]",style:{"animation-delay":"0.15s"}},"●",-1),e("span",{class:"animate-bounce text-[var(--ink3)]",style:{"animation-delay":"0.3s"}},"●",-1)])])):(n(),s("span",io,v(I.text),1))],2)],2))),128))],512),e("div",ao,[e("div",lo,[(n(),s(U,null,D(f,I=>e("button",{key:I,disabled:o.value,class:"rounded-full border border-[var(--border2)] bg-white px-2.5 py-1 text-[10px] font-medium text-[var(--ink2)] transition-colors hover:border-[#34d399] hover:text-[#34d399] disabled:opacity-40",onClick:E=>x(I)},v(I),9,uo)),64))]),e("div",co,[oe(e("input",{"onUpdate:modelValue":d[1]||(d[1]=I=>y.value=I),disabled:o.value,class:"ct-input flex-1 py-1.5 text-[12px]",placeholder:"Ask anything in your language",onKeydown:mt(me(g,["prevent"]),["enter"])},null,40,vo),[[Ke,y.value]]),e("button",{disabled:o.value||!y.value.trim(),class:"ct-btn px-3 py-1.5 text-[12px] disabled:opacity-40",onClick:g},[...d[8]||(d[8]=[e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"currentColor"},[e("path",{d:"M22 2L11 13"}),e("path",{d:"M22 2L15 22 11 13 2 9l20-7z"})],-1)])],8,po)])])]))}},ft=Se(fo,[["__scopeId","data-v-8a6603dd"]]);function xo(a){return a?Array.isArray(a.paragraph_ranges)?a.paragraph_ranges:Object.values(a).flatMap(m=>Array.isArray(m==null?void 0:m.paragraph_ranges)?m.paragraph_ranges:[]):[]}function go(a,m){const{start:y,end:o}=m||{};if(!y||!o)return[];const l=y.paragraph,r=o.paragraph,k=y.sentence??1,f=o.sentence??1/0,u=[];for(const x of a){const g=x.paragraph;if(g<l||g>r)continue;(x.children||[]).forEach((d,I)=>{const E=I+1;g===l&&g===r?E>=k&&E<=f&&u.push(d.id):g===l?E>=k&&u.push(d.id):g===r?E<=f&&u.push(d.id):u.push(d.id)})}return u}function mo(a,m){const y=S(()=>{const g=[];for(const p of a.value||[])for(const d of p.children||[])!Number.isFinite(d.from)||!Number.isFinite(d.to)||g.push({id:d.id,from:d.from,to:d.to,speaker:d.speaker,text:d.text});return g}),o=S(()=>{var p;const g=m.value||0;return((p=y.value.find(d=>g>=d.from&&g<=d.to))==null?void 0:p.id)??null}),l=w(new Set);let r=null;function k(g){const p=a.value||[],I=xo(g).flatMap(E=>go(p,E));l.value=new Set(I),clearTimeout(r),I.length&&(r=setTimeout(()=>{l.value=new Set},5e3))}function f(){clearTimeout(r),l.value=new Set}const u=S(()=>l.value.size>0?l.value:o.value?new Set([o.value]):new Set),x=S(()=>l.value.size>0?[...l.value][0]:o.value);return{segments:y,activeSegId:o,highlightedIds:u,scrollToSegId:x,activateLocateInfo:k,clearForced:f}}const bo={class:"min-h-screen bg-[var(--bg)]"},ho={key:0,class:"fixed inset-0 z-[500] flex items-center justify-center p-4"},ko={class:"relative z-10 w-full max-w-sm rounded-xl bg-white p-6 shadow-xl"},yo={class:"flex justify-end gap-2"},wo={class:"exam-container py-5 sm:py-6"},_o={key:0,class:"card p-6 text-center text-[var(--ink2)]"},$o={key:1,class:"card p-6 text-center"},Co={key:0,class:"fixed inset-0 z-[600] flex items-center justify-center bg-black/60"},qo={key:0},So={class:"space-y-5"},To={class:"flex flex-wrap items-center justify-between gap-2"},Io={class:"flex items-center gap-2 text-[14px] font-semibold text-[var(--ink)]"},Mo={class:"flex gap-2"},No={class:"grid grid-cols-1 gap-5 xl:grid-cols-2"},Ao={class:"card p-6"},Eo={class:"flex items-center gap-6"},zo={class:"flex-1 space-y-2 text-sm text-[var(--ink2)]"},Bo={class:"flex justify-between"},Lo={class:"font-semibold text-[var(--ink)]"},Oo={class:"flex justify-between"},Po={class:"font-semibold text-[var(--ink)]"},jo={class:"flex justify-between"},Ro={class:"font-semibold text-[var(--ink)]"},Ho={class:"mt-5"},Vo={class:"card p-6"},Uo={class:"grid grid-cols-2 gap-y-5 gap-x-4"},Go={class:"grid grid-cols-1 gap-5 xl:grid-cols-2"},Fo={class:"grid grid-cols-1 gap-5 xl:grid-cols-2"},Qo={class:"card p-5"},Do={class:"space-y-1.5"},Ko={key:0,class:"text-sm text-[var(--ink3)]"},Xo={class:"card p-5"},Wo={class:"space-y-1.5"},Jo={key:0,class:"text-sm text-[var(--ink3)]"},Yo={key:0,class:"card border-l-4 border-l-[#34d399] p-5"},Zo={class:"text-sm leading-relaxed text-[var(--ink)]"},er={key:1,class:"mx-auto mt-4 max-w-3xl rounded-lg border border-[#f43f5e44] bg-[#f43f5e11] px-4 py-2 text-xs text-[#f43f5e]"},tr={class:"mb-4 flex items-center justify-between gap-3"},nr={class:"mx-auto flex max-w-7xl gap-0 overflow-hidden rounded-2xl border border-[var(--border)] bg-white"},sr={class:"mb-2 text-xs font-semibold text-[var(--ink2)]"},or=["innerHTML"],rr={class:"grid gap-3"},ir=["onClick"],ar={key:0,class:"mt-3 overflow-hidden rounded-xl border border-[#d1fae5] bg-[#f0fdf4]"},lr={class:"flex items-center justify-between border-b border-[#d1fae5] px-4 py-2.5"},ur={class:"flex items-center gap-2 text-[12px] font-semibold text-[#059669]"},dr=["onClick"],cr={class:"grid grid-cols-2 divide-x divide-[#d1fae5] sm:grid-cols-4"},vr={class:"px-4 py-3 text-center"},pr={class:"text-lg font-bold text-[#059669]"},fr={class:"px-4 py-3 text-center"},xr={class:"text-base font-semibold text-[var(--ink)]"},gr={class:"px-4 py-3 text-center"},mr={class:"text-base font-semibold text-[var(--ink)]"},br={class:"px-4 py-3 text-center"},hr={class:"text-base font-semibold text-[var(--ink)]"},kr={key:0,class:"border-t border-[#d1fae5] px-4 py-2.5 text-[12px] text-[#374151]"},yr={class:"mt-6 flex items-center justify-between gap-2"},wr={key:0,class:"mt-3 rounded-lg border border-[#f43f5e44] bg-[#f43f5e11] px-4 py-2 text-xs text-[#f43f5e]"},_r={key:0,class:"card overflow-hidden"},$r={class:"px-4 pt-3 pb-1 text-xs font-semibold text-[var(--ink2)]"},Cr={class:"overflow-y-auto p-4",style:{"max-height":"calc(100vh - 280px)"}},qr={key:1,class:"card overflow-hidden"},Sr={class:"px-4 pt-3 pb-1 text-xs font-semibold text-[var(--ink2)]"},Tr={class:"overflow-y-auto p-4",style:{"max-height":"calc(100vh - 220px)"}},Ir={key:1,class:"reading-passage"},Mr={class:"para-tag"},Nr={class:"text-xs font-semibold text-[var(--ink2)] mb-2"},Ar=["innerHTML"],Er={key:2,class:"grid gap-3"},zr=["onClick"],Br={key:0,width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},Lr={key:1,width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5"},Or={key:2,class:"ml-1 font-normal text-[var(--ink2)]"},Pr={class:"text-[var(--ink)]"},jr={class:"mt-6 flex items-center justify-between gap-2"},Rr={__name:"QuizRunner",setup(a){const m=Et(),y=Bt(),o=Dt(),l=Jt(),r=S(()=>m.query.mode==="practice"),k=w(!1),f=w(null),u=w(!1),x=w(null),g=w({}),p=w(`sp_${Date.now()}_${Math.random().toString(36).slice(2,8)}`),d=w(null);async function I({blob:i,questionText:t,questionId:$}){var j,G;const z=String($??((G=(j=ee.value)==null?void 0:j.question)==null?void 0:G.id)??"");d.value=z||null,k.value=!0,f.value=null;try{const P=new FormData;P.append("file",i,"recording.webm"),P.append("question_text",t),P.append("persist_result","true"),P.append("quiz_id",String(m.params.quizId||"speaking")),P.append("question_id",z),P.append("attempt_id",p.value),Qt();const{data:Q}=await Qe.post("/speaking/evaluate",P,{timeout:12e4,transformRequest:[(C,R)=>(delete R["Content-Type"],delete R["content-type"],C)]}),le=URL.createObjectURL(i),h={result:Q,audioUrl:le,question:t,questionId:z};x.value=h,z&&(g.value[z]={...h})}catch(P){f.value=P.message||"Evaluation failed. Please try again."}finally{k.value=!1}}const E=new Map,H=w(null),F=w(null),ne=w(null),b=w(0),c=w(null),T=w(!1);function Y(){T.value=!1,y.push("/dashboard")}const Te=i=>{i.preventDefault(),i.returnValue=""},ve=w(580);let pe=!1,Ie=0,Me=0;function Z(i){pe=!0,Ie=i.clientX,Me=ve.value,document.body.style.userSelect="none",document.body.style.cursor="col-resize"}function J(i){var z;if(!pe)return;const t=i.clientX-Ie,$=((z=F.value)==null?void 0:z.clientWidth)||1200;ve.value=Math.max(280,Math.min($-340,Me+t))}function Ne(){pe&&(pe=!1,document.body.style.userSelect="",document.body.style.cursor="")}const _=S(()=>Kt(o.quiz)),N=S(()=>o.flat.some(i=>String(i.questionSetType||"").toLowerCase()==="speaking")),A=S(()=>o.flat.filter(i=>{const t=i.question||{},$=String(i.questionSetType||"").toLowerCase(),z=String(t.question_type||"").toLowerCase(),j=String(t.type||"").toLowerCase();return $==="speaking"||z==="speaking"||j==="speaking"})),K=w(0),se=S(()=>{const i=Math.max(0,A.value.length-1);return Math.min(Math.max(0,K.value),i)}),ee=S(()=>A.value[se.value]??null),O=S(()=>{var t,$;const i=String((($=(t=ee.value)==null?void 0:t.question)==null?void 0:$.id)??"");return i?g.value[i]:null});function bt(){var $;const i=se.value-1;if(i<0)return;K.value=i;const t=A.value[i];(($=t==null?void 0:t.question)==null?void 0:$.order)!=null&&Ee(t.question.order)}function Xe(){var $;const i=se.value+1;if(i>=A.value.length)return;K.value=i;const t=A.value[i];(($=t==null?void 0:t.question)==null?void 0:$.order)!=null&&Ee(t.question.order)}const Ae=S(()=>{const i=r.value?ee.value:o.currentItem;if(!i)return"";const t=i.question||{};return t.text||t.title||t.content||""}),ht=S(()=>{var i;return((i=o.quiz)==null?void 0:i.title)||`Quiz #${m.params.quizId}`}),kt=S(()=>`${_.value?"Listening":N.value?"Speaking":"Reading"} · ${o.totalQuestions} câu`),We=S(()=>o.flat.map(i=>({order:i.question.order,id:i.question.id}))),Je=S(()=>{var t;return(((t=o.quiz)==null?void 0:t.parts)||[]).map(($,z)=>{const j=o.flat.filter(P=>P.partId===$.id).map(P=>({order:P.question.order,id:P.question.id})),G=`Part ${$.passage||z+1}`;return{id:$.id,label:G,questions:j}})}),fe=S(()=>{var t,$,z,j,G;const i=(t=o.currentItem)==null?void 0:t.partId;return((z=($=o.quiz)==null?void 0:$.parts)==null?void 0:z.find(P=>P.id===i))||((G=(j=o.quiz)==null?void 0:j.parts)==null?void 0:G[0])||null}),be=S(()=>{var i;return Xt(((i=fe.value)==null?void 0:i.vocabs)||[])}),yt=S(()=>{var i;return Rt((i=fe.value)==null?void 0:i.file_id)}),Pe=mo(be,b),wt=S(()=>{var i,t;return(t=(i=o.currentItem)==null?void 0:i.question)==null?void 0:t.locate_info}),Ye=S(()=>Wt(wt.value));function _t(i){return Ye.value.length?Ye.value.some(t=>i>=t.startParagraph&&i<=t.endParagraph):!1}function Ze(i,t){if(!i||!t)return;const $=(t==null?void 0:t.$el)??t;E.set(i,$)}function et(i){const t=E.get(i);t&&typeof t.scrollIntoView=="function"&&t.scrollIntoView({behavior:"smooth",block:"start"})}function tt(i){o.gotoOrder(i)}async function Ee(i){o.gotoOrder(i),await Ce(),et(i)}function $t(i){return new Set((i||[]).map($=>$.order)).has(o.currentOrder)}function Ct({time:i,locateInfo:t}={}){var $;Number.isFinite(i)&&(($=ne.value)==null||$.seekAndPlay(i)),t&&Pe.activateLocateInfo(t)}const nt=S(()=>{var $;const i=(($=o.quiz)==null?void 0:$.parts)||[],t=[];for(const z of i)for(const j of z.question_sets||[]){const G=`${z.id}_${j.id}`;if(j.question_type==="GAP_FILLING"){t.push({key:G,kind:"gap",title:j.title||`Part ${z.passage}`,description:j.description||"",content:j.content||"",questions:(j.questions||[]).map(Q=>({id:Q.id,sort:Q.sort,order:Q.order}))});continue}const P=o.flat.filter(Q=>Q.partId===z.id&&Q.questionSetId===j.id);t.push({key:G,kind:"items",title:j.title||`Part ${z.passage}`,description:j.description||"",items:P})}return t}),je=w(new Set);function qt(i){if(!r.value)return;const t=new Set(je.value);t.add(String(i)),je.value=t}function St(i,t){o.setAnswer(i,t),qt(i)}function te(i){var P,Q;const t=String(((P=i.question)==null?void 0:P.id)??"");if(!je.value.has(t))return null;const $=i.question||{},z=o.answers[$.id],j=jt({question:$,userAnswer:z}),G=(Q=$.correct_answers)!=null&&Q.length?$.correct_answers.join(" / "):$.correct_answer??"—";return{ok:j,correctAnswer:G,explain:$.explain||"",userAnswer:z}}const st=w(null),Re=w(null),He=w("yellow"),ae=w(""),ze=w(`session_${Date.now()}_${Math.random().toString(36).slice(2,7)}`),ot=w([]);function rt(i){ot.value=i}function Ve({tool:i,color:t}){Re.value=i,He.value=t||"yellow"}async function Tt(){if(r.value)try{await Yt(ze.value,{session_id:ze.value,quiz_id:String(m.params.quizId||""),highlights:ot.value,note:ae.value})}catch{}}async function Be(i){var P,Q,le,h;if(await Tt(),N.value){if(!(Object.keys(g.value||{}).length>0)){f.value="Bạn cần đánh giá ít nhất 1 câu speaking trước khi nộp.";return}try{const{data:R}=await Qe.get("/speaking/attempt-summary",{params:{quiz_id:String(m.params.quizId||"speaking"),attempt_id:p.value}});y.push({path:"/speaking/result",state:{summary:R,question:`Speaking quiz #${m.params.quizId}`,mode:"attempt-summary"}});return}catch(R){f.value=((Q=(P=R==null?void 0:R.response)==null?void 0:P.data)==null?void 0:Q.detail)||(R==null?void 0:R.message)||"Không tải được tổng kết speaking.";return}}o.submit({auto:i});const t=l.currentSession,$=(le=t==null?void 0:t.quiz)==null?void 0:le.id,z=Number(m.params.quizId),j=_.value?"listening":"reading";if(t!=null&&t.session_id&&Number($)===z){const C=Object.entries(o.answers||{}).reduce((re,[ue,de])=>(re[String(ue)]=de,re),{});if(await l.submitSession(j,t.session_id,C)){y.push({path:`/results/${t.session_id}`,query:{annotationSession:ze.value}});return}}const G=Pt({quiz:o.quiz,flat:o.flat,answers:o.answers});o.result={quizId:z,title:(h=o.quiz)==null?void 0:h.title,correct:G.correct,total:G.total,estimatedBand:G.estimatedBand,detailed:G.detailed,answers:o.answers,annotationSession:ze.value},y.push(`/quiz/${m.params.quizId}/result`)}return X(()=>o.remainingSeconds,i=>{i===0&&o.quiz&&!o.result&&Be(!0)}),X([()=>A.value,()=>o.currentOrder,()=>r.value],()=>{if(!r.value||!A.value.length)return;const i=A.value.findIndex(t=>{var $;return String(($=t.question)==null?void 0:$.order)===String(o.currentOrder)});i>=0&&(K.value=i)},{immediate:!0,deep:!1}),xt(async()=>{await o.loadQuiz(m.params.quizId),await Ce(),et(o.currentOrder),window.addEventListener("beforeunload",Te),window.addEventListener("mousemove",J),window.addEventListener("mouseup",Ne)}),De(()=>{o.stopTimer(),window.removeEventListener("beforeunload",Te),window.removeEventListener("mousemove",J),window.removeEventListener("mouseup",Ne)}),(i,t)=>{var z,j,G,P,Q,le;const $=zt("RouterLink");return n(),s("div",bo,[(n(),W(Fe,{to:"body"},[T.value?(n(),s("div",ho,[e("div",{class:"absolute inset-0 bg-black/40",onClick:t[0]||(t[0]=h=>T.value=!1)}),e("div",ko,[t[20]||(t[20]=e("div",{class:"mb-1 text-base font-bold text-[var(--ink)]"},"Thoát bài thi?",-1)),t[21]||(t[21]=e("p",{class:"mb-5 text-[13px] text-[var(--ink3)]"},"Tiến trình làm bài sẽ không được lưu. Bạn có chắc muốn thoát?",-1)),e("div",yo,[e("button",{class:"ct-btn",onClick:t[1]||(t[1]=h=>T.value=!1)},"Tiếp tục làm"),e("button",{class:"ct-btn",style:{"border-color":"#e11d48",color:"#e11d48"},onClick:Y},"Thoát")])])])):M("",!0)])),r.value&&N.value?(n(),W(Ps,{key:0,"practice-mode":r.value,"model-note":ae.value,"onUpdate:modelNote":t[2]||(t[2]=h=>ae.value=h),onToolChanged:Ve},null,8,["practice-mode","model-note"])):M("",!0),V(un,{title:ht.value,subtitle:kt.value,"remaining-seconds":q(o).remainingSeconds,onSubmit:t[3]||(t[3]=h=>Be(!1))},null,8,["title","subtitle","remaining-seconds"]),e("div",wo,[q(o).loading?(n(),s("div",_o,"Loading…")):q(o).quiz?(n(),s(U,{key:2},[(n(),W(Fe,{to:"body"},[k.value?(n(),s("div",Co,[...t[24]||(t[24]=[e("div",{class:"flex flex-col items-center gap-4 rounded-2xl bg-[#0f0f1a] p-8 text-white shadow-2xl"},[e("div",{class:"h-10 w-10 animate-spin rounded-full border-4 border-[#6c63ff] border-t-transparent"}),e("p",{class:"text-sm font-semibold"},"Đang phân tích bài nói…"),e("p",{class:"text-[11px] text-[#a0a0c0]"},"Pronunciation · Transcription · AI Feedback")],-1)])])):M("",!0)])),N.value?(n(),s("div",qo,[r.value?(n(),W(Zs,{key:0,"current-index":se.value,total:A.value.length,"part-title":((z=ee.value)==null?void 0:z.partTitle)||(ee.value?`Part ${se.value+1}`:""),"chat-open":u.value,"can-prev":se.value>0,"can-next":se.value<A.value.length-1,onExit:t[7]||(t[7]=h=>T.value=!0),onPrev:bt,onNext:Xe,onToggleChat:t[8]||(t[8]=h=>u.value=!u.value)},At({chat:ie(()=>[V(Oe,{name:"slide"},{default:ie(()=>[u.value?(n(),W(ft,{key:0,class:"w-full shrink-0 lg:w-[340px]","question-text":Ae.value,onClose:t[5]||(t[5]=h=>u.value=!1)},null,8,["question-text"])):M("",!0)]),_:1})]),default:ie(()=>[ee.value?(n(),W(Ge,{key:ee.value.question.id,item:ee.value,answer:q(o).answers[ee.value.question.id],"is-current":!0,"speaking-compact":"","onUpdate:answer":t[4]||(t[4]=h=>q(o).setAnswer(ee.value.question.id,h)),onEvaluateSpeaking:I},null,8,["item","answer"])):M("",!0)]),_:2},[(j=O.value)!=null&&j.result?{name:"feedback",fn:ie(()=>{var h,C,R,re,ue,de,he,ke,ye,we,_e;return[e("div",So,[e("div",To,[e("div",Io,[t[25]||(t[25]=e("svg",{width:"15",height:"15",viewBox:"0 0 24 24",fill:"none",stroke:"#34d399","stroke-width":"2"},[e("path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14"}),e("polyline",{points:"22 4 12 14.01 9 11.01"})],-1)),B(" Kết quả câu "+v(se.value+1),1)]),e("div",Mo,[e("button",{class:"ct-btn px-3 py-1.5 text-[12px]",onClick:t[6]||(t[6]=ce=>q(y).push({path:"/speaking/result",state:O.value}))}," Xem chi tiết "),se.value<A.value.length-1?(n(),s("button",{key:0,class:"flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[12px] font-semibold text-white",style:{background:"#34d399"},onClick:Xe},[...t[26]||(t[26]=[B(" Câu tiếp theo ",-1),e("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"9 18 15 12 9 6"})],-1)])])):M("",!0)])]),e("div",No,[e("div",Ao,[t[30]||(t[30]=e("div",{class:"mb-4 flex items-center gap-2 text-[var(--ink2)]"},[e("span",{class:"text-xs font-bold uppercase tracking-wider"},"Band Score")],-1)),e("div",Eo,[V(Ht,{band:O.value.result.band_estimate||0},null,8,["band"]),e("div",zo,[e("div",Bo,[t[27]||(t[27]=e("span",null,"Grammar",-1)),e("span",Lo,v(Number(((h=O.value.result.grammar)==null?void 0:h.score)||0).toFixed(1))+"/9",1)]),e("div",Oo,[t[28]||(t[28]=e("span",null,"Vocabulary",-1)),e("span",Po,v(Number(((C=O.value.result.vocabulary)==null?void 0:C.score)||0).toFixed(1))+"/9",1)]),e("div",jo,[t[29]||(t[29]=e("span",null,"Pronunciation",-1)),e("span",Ro,v(Number(((R=O.value.result.pronunciation)==null?void 0:R.total)||0).toFixed(1))+"/10",1)])])]),e("div",Ho,[V(Vt,{"audio-url":O.value.audioUrl},null,8,["audio-url"])])]),e("div",Vo,[t[31]||(t[31]=e("div",{class:"mb-4 flex items-center gap-2 text-[var(--ink2)]"},[e("span",{class:"text-xs font-bold uppercase tracking-wider"},"Pronunciation")],-1)),e("div",Uo,[V(Le,{score:((re=O.value.result.pronunciation)==null?void 0:re.accuracy)||0,label:"Accuracy"},null,8,["score"]),V(Le,{score:((ue=O.value.result.pronunciation)==null?void 0:ue.fluency)||0,label:"Fluency"},null,8,["score"]),V(Le,{score:((de=O.value.result.pronunciation)==null?void 0:de.prosodic)||0,label:"Prosodic"},null,8,["score"]),V(Le,{score:((he=O.value.result.pronunciation)==null?void 0:he.total)||0,label:"Total",size:96},null,8,["score"])])])]),V(Ut,{transcript:O.value.result.transcript||"","word-timestamps":O.value.result.word_timestamps||[]},null,8,["transcript","word-timestamps"]),e("div",Go,[V(Gt,{transcript:O.value.result.transcript||"","question-text":Ae.value,score:Number(((ke=O.value.result.grammar)==null?void 0:ke.score)||0),errors:((ye=O.value.result.grammar)==null?void 0:ye.errors)||[],"evaluate-result":O.value.result},null,8,["transcript","question-text","score","errors","evaluate-result"]),V(Ft,{transcript:O.value.result.transcript||"","question-text":Ae.value,score:Number(((we=O.value.result.vocabulary)==null?void 0:we.score)||0),feedback:((_e=O.value.result.vocabulary)==null?void 0:_e.feedback)||[],"evaluate-result":O.value.result},null,8,["transcript","question-text","score","feedback","evaluate-result"])]),e("div",Fo,[e("div",Qo,[t[33]||(t[33]=e("div",{class:"mb-3 text-xs font-bold uppercase tracking-wider text-[#34d399]"},"Strengths",-1)),e("ul",Do,[(n(!0),s(U,null,D(O.value.result.strengths||[],(ce,xe)=>(n(),s("li",{key:`st_${xe}`,class:"flex items-start gap-2 text-sm text-[var(--ink)]"},[t[32]||(t[32]=e("span",{class:"mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-[#34d399]"},null,-1)),B(" "+v(ce),1)]))),128)),(O.value.result.strengths||[]).length?M("",!0):(n(),s("li",Ko,"—"))])]),e("div",Xo,[t[35]||(t[35]=e("div",{class:"mb-3 text-xs font-bold uppercase tracking-wider text-[#f59e0b]"},"Improvements",-1)),e("ul",Wo,[(n(!0),s(U,null,D(O.value.result.improvements||[],(ce,xe)=>(n(),s("li",{key:`im_${xe}`,class:"flex items-start gap-2 text-sm text-[var(--ink)]"},[t[34]||(t[34]=e("span",{class:"mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-[#f59e0b]"},null,-1)),B(" "+v(ce),1)]))),128)),(O.value.result.improvements||[]).length?M("",!0):(n(),s("li",Jo,"—"))])])]),O.value.result.overall_comment?(n(),s("div",Yo,[t[36]||(t[36]=e("div",{class:"mb-2 text-xs font-bold uppercase tracking-wider text-[#34d399]"},"Overall comment",-1)),e("p",Zo,v(O.value.result.overall_comment),1)])):M("",!0)])]}),key:"0"}:void 0]),1032,["current-index","total","part-title","chat-open","can-prev","can-next"])):M("",!0),r.value&&f.value?(n(),s("div",er,v(f.value),1)):r.value?M("",!0):(n(),s(U,{key:2},[e("div",tr,[V(pt,{questions:We.value,"nav-parts":Je.value,"current-order":q(o).currentOrder,"answered-map":q(o).answers,onGo:Ee,class:"min-w-0 flex-1"},null,8,["questions","nav-parts","current-order","answered-map"]),e("button",{type:"button",onClick:t[9]||(t[9]=h=>u.value=!u.value),class:L(["flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-[11px] font-semibold transition-colors",u.value?"border-[#34d399] bg-[#34d39911] text-[#34d399]":"border-[var(--border2)] bg-white text-[var(--ink2)] hover:border-[#34d399] hover:text-[#34d399]"])},[...t[37]||(t[37]=[e("svg",{width:"11",height:"11",viewBox:"0 0 24 24",fill:"currentColor"},[e("path",{d:"M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"})],-1),B(" Need help? Click here. ",-1)])],2)]),e("div",nr,[e("div",{class:L(["min-w-0 flex-1 p-5 sm:p-6",u.value?"border-r border-[var(--border)]":""])},[(n(!0),s(U,null,D(nt.value,h=>(n(),s("div",{key:h.key,class:"mb-6"},[e("div",sr,v(h.title),1),h.description?(n(),s("div",{key:0,class:"mb-3 text-sm text-[var(--ink2)]",innerHTML:h.description},null,8,or)):M("",!0),e("div",rr,[(n(!0),s(U,null,D(h.items,C=>{var R,re,ue,de,he,ke,ye,we,_e,ce,xe,it,at,lt,ut;return n(),s("div",{key:C.question.id,ref_for:!0,ref:$e=>Ze(C.question.order,$e),onClick:$e=>tt(C.question.order)},[V(Ge,{item:C,answer:q(o).answers[C.question.id],"is-current":q(o).currentOrder===C.question.order,"onUpdate:answer":$e=>q(o).setAnswer(C.question.id,$e),onEvaluateSpeaking:I},null,8,["item","answer","is-current","onUpdate:answer"]),g.value[String(C.question.id)]?(n(),s("div",ar,[e("div",lr,[e("div",ur,[t[38]||(t[38]=e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14"}),e("polyline",{points:"22 4 12 14.01 9 11.01"})],-1)),B(" Kết quả câu "+v(C.question.order),1)]),e("button",{class:"text-[11px] text-[#059669] underline hover:no-underline",onClick:me($e=>q(y).push({path:"/speaking/result",state:g.value[String(C.question.id)]}),["stop"])}," Xem chi tiết ",8,dr)]),e("div",cr,[e("div",vr,[t[39]||(t[39]=e("div",{class:"text-[11px] text-[#6b7280]"},"Band",-1)),e("div",pr,v(Number(((re=(R=g.value[String(C.question.id)])==null?void 0:R.result)==null?void 0:re.band_estimate)||0).toFixed(1)),1)]),e("div",fr,[t[41]||(t[41]=e("div",{class:"text-[11px] text-[#6b7280]"},"Grammar",-1)),e("div",xr,[B(v(Number(((he=(de=(ue=g.value[String(C.question.id)])==null?void 0:ue.result)==null?void 0:de.grammar)==null?void 0:he.score)||0).toFixed(1)),1),t[40]||(t[40]=e("span",{class:"text-[10px] text-[#9ca3af]"},"/9",-1))])]),e("div",gr,[t[43]||(t[43]=e("div",{class:"text-[11px] text-[#6b7280]"},"Vocab",-1)),e("div",mr,[B(v(Number(((we=(ye=(ke=g.value[String(C.question.id)])==null?void 0:ke.result)==null?void 0:ye.vocabulary)==null?void 0:we.score)||0).toFixed(1)),1),t[42]||(t[42]=e("span",{class:"text-[10px] text-[#9ca3af]"},"/9",-1))])]),e("div",br,[t[45]||(t[45]=e("div",{class:"text-[11px] text-[#6b7280]"},"Pron.",-1)),e("div",hr,[B(v(Number(((xe=(ce=(_e=g.value[String(C.question.id)])==null?void 0:_e.result)==null?void 0:ce.pronunciation)==null?void 0:xe.total)||0).toFixed(1)),1),t[44]||(t[44]=e("span",{class:"text-[10px] text-[#9ca3af]"},"/10",-1))])])]),(at=(it=g.value[String(C.question.id)])==null?void 0:it.result)!=null&&at.transcript?(n(),s("div",kr,[t[46]||(t[46]=e("span",{class:"mr-1 font-semibold text-[#059669]"},"Bài nói:",-1)),B(" "+v((ut=(lt=g.value[String(C.question.id)])==null?void 0:lt.result)==null?void 0:ut.transcript),1)])):M("",!0)])):M("",!0)],8,ir)}),128))])]))),128)),e("div",yr,[e("button",{type:"button",class:"btn btn-secondary",onClick:t[10]||(t[10]=h=>T.value=!0)},"Thoát"),e("button",{type:"button",class:"btn btn-primary",onClick:t[11]||(t[11]=h=>Be(!1))},"Nộp bài & Xem kết quả")]),f.value?(n(),s("div",wr,v(f.value),1)):M("",!0)],2),V(Oe,{name:"slide"},{default:ie(()=>[u.value?(n(),W(ft,{key:0,"question-text":Ae.value,onClose:t[12]||(t[12]=h=>u.value=!1)},null,8,["question-text"])):M("",!0)]),_:1})])],64))])):(n(),s("div",{key:1,class:"flex gap-5 lg:gap-6",ref_key:"layoutEl",ref:F},[e("div",{class:"flex flex-col gap-4 min-w-0",style:qe({flex:`0 0 ${ve.value}px`,width:ve.value+"px"})},[_.value?(n(),s(U,{key:0},[V(yn,{ref_key:"playerRef",ref:ne,src:yt.value,title:((G=fe.value)==null?void 0:G.title)||"Listening",subtitle:`File: ${((P=fe.value)==null?void 0:P.file_id)||"—"}`,"seek-to":c.value,onTime:t[13]||(t[13]=h=>b.value.value=h)},null,8,["src","title","subtitle","seek-to"]),r.value?(n(),s("div",_r,[e("div",$r,v((Q=fe.value)==null?void 0:Q.title),1),V(ct,{"model-note":ae.value,"onUpdate:modelNote":t[14]||(t[14]=h=>ae.value=h),onToolChanged:Ve},null,8,["model-note"]),e("div",Cr,[V(vt,{ref_key:"readingPassageRef",ref:st,paragraphs:be.value,"active-tool":Re.value,"highlight-color":He.value,onHighlightsChanged:rt},null,8,["paragraphs","active-tool","highlight-color"])])])):(n(),W(On,{key:1,paragraphs:be.value,"current-time":b.value,"highlighted-ids":q(Pe).highlightedIds.value,onSeek:t[15]||(t[15]=h=>{c.value.value=h,q(Pe).clearForced()})},null,8,["paragraphs","current-time","highlighted-ids"]))],64)):(n(),s("div",qr,[e("div",Sr,v((le=fe.value)==null?void 0:le.title),1),r.value?(n(),W(ct,{key:0,"model-note":ae.value,"onUpdate:modelNote":t[16]||(t[16]=h=>ae.value=h),onToolChanged:Ve},null,8,["model-note"])):M("",!0),e("div",Tr,[r.value?(n(),W(vt,{key:0,ref_key:"readingPassageRef",ref:st,paragraphs:be.value,"active-tool":Re.value,"highlight-color":He.value,onHighlightsChanged:rt},null,8,["paragraphs","active-tool","highlight-color"])):(n(),s("div",Ir,[(n(!0),s(U,null,D(be.value,h=>(n(),s("div",{key:h.paragraph,class:L(["reading-paragraph",_t(h.paragraph)?"is-highlight":""])},[e("span",Mr,v(h.paragraph),1),e("span",null,v(h.text),1)],2))),128))]))])])),V(pt,{questions:We.value,"nav-parts":Je.value,"current-order":q(o).currentOrder,"answered-map":q(o).answers,onGo:Ee},null,8,["questions","nav-parts","current-order","answered-map"])],4),e("div",{class:"flex w-2 cursor-col-resize items-center justify-center group",onMousedown:me(Z,["prevent"])},[...t[47]||(t[47]=[e("div",{class:"h-12 w-0.5 rounded-full bg-[var(--border2)] group-hover:bg-[#34d399] transition-colors"},null,-1)])],32),e("div",{class:"card flex-1 overflow-auto p-4",style:{"max-height":"calc(100vh - 140px)"},ref_key:"rightCol",ref:H},[(n(!0),s(U,null,D(nt.value,h=>(n(),s("div",{key:h.key,class:"mb-6"},[e("div",Nr,v(h.title),1),h.description?(n(),s("div",{key:0,class:"text-sm text-[var(--ink2)] mb-3",innerHTML:h.description},null,8,Ar)):M("",!0),h.kind==="gap"?(n(),W(Ts,{key:1,title:h.title,description:h.description,html:h.content,questions:h.questions,answers:q(o).answers,"is-current":$t(h.questions),onAnswer:t[17]||(t[17]=({questionId:C,value:R})=>q(o).setAnswer(C,R))},null,8,["title","description","html","questions","answers","is-current"])):(n(),s("div",Er,[(n(!0),s(U,null,D(h.items,C=>(n(),s("div",{key:C.question.id,ref_for:!0,ref:R=>Ze(C.question.order,R),onClick:R=>tt(C.question.order)},[V(Ge,{item:C,answer:q(o).answers[C.question.id],"is-current":q(o).currentOrder===C.question.order,"onUpdate:answer":R=>r.value?St(C.question.id,R):q(o).setAnswer(C.question.id,R),onJumpAudio:Ct},null,8,["item","answer","is-current","onUpdate:answer"]),r.value&&te(C)?(n(),s("div",{key:0,class:L(["mt-1 overflow-hidden rounded-xl border text-[13px]",te(C).ok?"border-[#bbf7d0] bg-[#f0fdf4]":"border-[#fecaca] bg-[#fef2f2]"])},[e("div",{class:L(["flex items-center gap-2 px-4 py-2.5 font-semibold",te(C).ok?"text-[#059669]":"text-[#dc2626]"])},[te(C).ok?(n(),s("svg",Br,[...t[48]||(t[48]=[e("polyline",{points:"20 6 9 17 4 12"},null,-1)])])):(n(),s("svg",Lr,[...t[49]||(t[49]=[e("line",{x1:"18",y1:"6",x2:"6",y2:"18"},null,-1),e("line",{x1:"6",y1:"6",x2:"18",y2:"18"},null,-1)])])),B(" "+v(te(C).ok?"Đúng!":"Sai")+" ",1),te(C).ok?M("",!0):(n(),s("span",Or,[t[50]||(t[50]=B("Đáp án đúng: ",-1)),e("strong",Pr,v(te(C).correctAnswer),1)]))],2),te(C).explain?(n(),s("div",{key:0,class:L(["border-t px-4 py-2.5 text-[12px] leading-relaxed text-[var(--ink2)]",te(C).ok?"border-[#bbf7d0]":"border-[#fecaca]"])},[t[51]||(t[51]=e("span",{class:"mr-1 font-semibold text-[var(--ink)]"},"Giải thích:",-1)),B(" "+v(te(C).explain),1)],2)):M("",!0)],2)):M("",!0)],8,zr))),128))]))]))),128)),e("div",jr,[e("button",{class:"btn btn-secondary",onClick:t[18]||(t[18]=h=>T.value=!0)},"Thoát"),e("button",{class:"btn btn-primary",onClick:t[19]||(t[19]=h=>Be(!1))},"Nộp bài")])],512)],512))],64)):(n(),s("div",$o,[t[23]||(t[23]=e("div",{class:"text-lg font-semibold mb-2"},"Quiz not found",-1)),V($,{to:"/dashboard",class:"btn btn-primary"},{default:ie(()=>[...t[22]||(t[22]=[B("Về trang chủ",-1)])]),_:1})]))])])}}},Kr=Se(Rr,[["__scopeId","data-v-95508725"]]);export{Kr as default};
|
|
|
|
|
|
fronted/dist/assets/QuizRunner-iXYCFrWo.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
.scrollbar-hide[data-v-edab0552]::-webkit-scrollbar{display:none}.scrollbar-hide[data-v-edab0552]{-ms-overflow-style:none;scrollbar-width:none}.speaking-question[data-v-1392778c]{border-radius:0;border:none;background:transparent;box-shadow:none}.speaking-question--current[data-v-1392778c]{outline:none}.speaking-question[data-v-1392778c] .text-sm.font-semibold{font-size:1.125rem;line-height:1.5;font-weight:600;color:var(--ink)}.tool-btn[data-v-daae9535]{position:relative;display:flex;align-items:center;justify-content:center;height:44px;width:44px;border-radius:12px;border:1px solid;cursor:pointer;box-shadow:0 2px 8px #00000014;transition:all .15s}.tool-btn[data-v-daae9535]:hover{transform:translate(-2px);box-shadow:0 4px 12px #0000001f}.tool-key[data-v-daae9535]{position:absolute;bottom:2px;right:4px;font-size:8px;font-weight:700;opacity:.5;line-height:1}.tool-btn--default[data-v-daae9535]{border-color:var(--border);background:#fff;color:var(--ink2)}.tool-btn--highlight-active[data-v-daae9535]{border-color:#fde047;background:#fef9c3;color:#854d0e}.hover\:tool-btn--highlight-hover[data-v-daae9535]:hover{border-color:#fde047;background:#fefce8}.tool-btn--note-active[data-v-daae9535]{border-color:var(--blue-l);background:var(--blue-bg);color:var(--blue)}.hover\:tool-btn--note-hover[data-v-daae9535]:hover{border-color:var(--blue-l);background:var(--blue-bg)}.tool-btn--vocab-active[data-v-daae9535]{border-color:#15803d;background:#dcfce7;color:#15803d}.hover\:tool-btn--vocab-hover[data-v-daae9535]:hover{border-color:#15803d;background:#f0fdf4}.note-panel[data-v-daae9535]{position:fixed;right:0;top:0;z-index:160;height:100%;width:320px;display:flex;flex-direction:column;background:#fff;box-shadow:-4px 0 24px #0000001a;border-left:1px solid var(--border)}.note-panel__header[data-v-daae9535]{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border);flex-shrink:0}.note-panel__footer[data-v-daae9535]{padding:8px 16px;border-top:1px solid var(--border);flex-shrink:0;text-align:right}.icon-close[data-v-daae9535]{background:none;border:none;cursor:pointer;color:var(--ink3);padding:4px;border-radius:6px}.icon-close[data-v-daae9535]:hover{background:var(--bg2);color:var(--ink)}.slide-right-enter-active[data-v-daae9535],.slide-right-leave-active[data-v-daae9535]{transition:transform .25s ease}.slide-right-enter-from[data-v-daae9535],.slide-right-leave-to[data-v-daae9535]{transform:translate(100%)}.slide-x-enter-active[data-v-daae9535],.slide-x-leave-active[data-v-daae9535]{transition:opacity .15s,transform .15s}.slide-x-enter-from[data-v-daae9535],.slide-x-leave-to[data-v-daae9535]{opacity:0;transform:translate(-8px)}.chat-scroll[data-v-8a6603dd]{scroll-behavior:smooth}.slide-enter-active[data-v-95508725],.slide-leave-active[data-v-95508725]{transition:all .2s ease}.slide-enter-from[data-v-95508725],.slide-leave-to[data-v-95508725]{opacity:0;transform:translate(20px)}.reading-passage[data-v-95508725]{max-height:420px;overflow:auto;padding-right:8px}.reading-paragraph[data-v-95508725]{display:flex;gap:10px;padding:10px 12px;border-radius:12px;border:1px solid transparent;margin-bottom:10px;background:var(--surface)}.reading-paragraph.is-highlight[data-v-95508725]{border-color:#7c6af759;background:#7c6af714}.para-tag[data-v-95508725]{min-width:28px;height:22px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--ink2);border:1px solid var(--border2);background:var(--bg)}
|
|
|
|
|
|
fronted/dist/assets/Reading-CN1Pbi4C.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{q as B,c as r,a,h as f,i as I,t as M,d as E,v as F,F as g,r as C,f as N,j as o,k as j,z as T,l as U,o as i,g as A,x as D}from"./index-CId0hMaT.js";import{l as G}from"./mockTestService-ByZrQuTM.js";import{u as Q}from"./practice-nWRX1lS1.js";import{_ as K}from"./SkillTestCard-B586wTHe.js";import{_ as O}from"./ModePickerModal-CUJKgPsz.js";import{_ as W}from"./Paginator-C1T4CVWL.js";const Z={class:"mb-6"},H={class:"flex flex-wrap items-end gap-3"},J={class:"mt-0.5 text-[13px] text-[var(--ink3)]"},X={class:"ml-auto"},Y={key:0,class:"grid gap-4 sm:grid-cols-2 xl:grid-cols-3"},ee={class:"grid gap-4 sm:grid-cols-2 xl:grid-cols-3"},te={key:0,class:"col-span-3 py-16 text-center text-[var(--ink3)]"},le={class:"mt-6"},p=9,de={__name:"Reading",setup(ae){const k=U(),$=Q(),v=o(!1),d=o(""),c=o([]),u=o(1),_=T(()=>d.value?c.value.filter(l=>{var e;return(e=l.title)==null?void 0:e.toLowerCase().includes(d.value.toLowerCase())}):c.value),x=T(()=>_.value.slice((u.value-1)*p,u.value*p));function L(l){return Object.entries((l==null?void 0:l.quizzes)||{}).filter(([e])=>e.startsWith("part_")).sort((e,s)=>e[0].localeCompare(s[0],void 0,{numeric:!0})).map(([e,s])=>({key:e,...s}))}const m=o(!1),h=o(""),P=o(null),z=o(null);function b(l,e){var s;P.value=l,z.value=e||((s=l.quizzes)==null?void 0:s.full),h.value=l.title,m.value=!0}async function R(l){var s,t;const e=(s=z.value)==null?void 0:s.id;if(e)if(l==="exam")k.push(`/quiz/${e}?mode=exam`);else{const n=await $.startSession("reading",e);k.push(`/quiz/${((t=n==null?void 0:n.quiz)==null?void 0:t.id)||e}?mode=practice`)}}return B(async()=>{v.value=!0;try{c.value=await G({skillId:1})}finally{v.value=!1}}),(l,e)=>{const s=j("RouterLink");return i(),r("div",null,[a("div",Z,[f(s,{to:"/dashboard",class:"mb-3 inline-flex items-center gap-1.5 text-[12px] text-[var(--ink3)] hover:text-[var(--ink)] transition-colors"},{default:I(()=>[...e[4]||(e[4]=[a("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[a("polyline",{points:"15 18 9 12 15 6"})],-1),A(" Trang chủ ",-1)])]),_:1}),a("div",H,[a("div",null,[e[5]||(e[5]=a("h1",{class:"text-xl font-bold text-[var(--ink)]"},"Reading",-1)),a("p",J,M(c.value.length)+" bộ đề · IELTS Academic & General",1)]),a("div",X,[E(a("input",{"onUpdate:modelValue":e[0]||(e[0]=t=>d.value=t),class:"ct-input w-64",placeholder:"Tìm đề...",onInput:e[1]||(e[1]=t=>u.value=1)},null,544),[[F,d.value]])])])]),v.value?(i(),r("div",Y,[(i(),r(g,null,C(p,t=>a("div",{key:t,class:"h-44 animate-pulse rounded-xl bg-[var(--bg2)]"})),64))])):(i(),r(g,{key:1},[a("div",ee,[(i(!0),r(g,null,C(x.value,t=>{var n,q,y,w;return i(),D(K,{key:t.id,title:t.title,thumbnail:t.thumbnail,"book-code":t.book_code,"skill-label":"Reading","question-count":(q=(n=t.quizzes)==null?void 0:n.full)==null?void 0:q.question_count,time:(w=(y=t.quizzes)==null?void 0:y.full)==null?void 0:w.time,parts:L(t),onStartFull:V=>{var S;return b(t,(S=t.quizzes)==null?void 0:S.full)},onStartPart:V=>b(t,V)},null,8,["title","thumbnail","book-code","question-count","time","parts","onStartFull","onStartPart"])}),128)),x.value.length?N("",!0):(i(),r("p",te,"Không tìm thấy đề phù hợp."))]),a("div",le,[f(W,{modelValue:u.value,"onUpdate:modelValue":e[2]||(e[2]=t=>u.value=t),total:_.value.length,"page-size":p},null,8,["modelValue","total"])])],64)),f(O,{modelValue:m.value,"onUpdate:modelValue":e[3]||(e[3]=t=>m.value=t),"test-title":h.value,onConfirm:R},null,8,["modelValue","test-title"])])}}};export{de as default};
|
fronted/dist/assets/Reading-Cc5Jk6VP.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{q as $,c as r,a,h as f,i as B,t as I,d as E,v as F,F as g,r as C,f as N,j as s,k as j,z as M,l as U,o as i,g as A,x as D}from"./index-BnN_E-ke.js";import{l as G}from"./mockTestService-uSLqkhlt.js";import{u as Q}from"./practice-BINrC2xL.js";import{_ as K}from"./SkillTestCard-CKhK5GXd.js";import{_ as O,M as W}from"./Paginator-lTh9FHNB.js";const Z={class:"mb-6"},H={class:"flex flex-wrap items-end gap-3"},J={class:"mt-0.5 text-[13px] text-[var(--ink3)]"},X={class:"ml-auto"},Y={key:0,class:"grid gap-4 sm:grid-cols-2 xl:grid-cols-3"},ee={class:"grid gap-4 sm:grid-cols-2 xl:grid-cols-3"},te={key:0,class:"col-span-3 py-16 text-center text-[var(--ink3)]"},le={class:"mt-6"},p=9,ue={__name:"Reading",setup(ae){const k=U(),P=Q(),v=s(!1),d=s(""),c=s([]),u=s(1),x=M(()=>d.value?c.value.filter(l=>{var e;return(e=l.title)==null?void 0:e.toLowerCase().includes(d.value.toLowerCase())}):c.value),h=M(()=>x.value.slice((u.value-1)*p,u.value*p));function T(l){return Object.entries((l==null?void 0:l.quizzes)||{}).filter(([e])=>e.startsWith("part_")).sort((e,o)=>e[0].localeCompare(o[0],void 0,{numeric:!0})).map(([e,o])=>({key:e,...o}))}const m=s(!1),_=s(""),L=s(null),z=s(null);function b(l,e){var o;L.value=l,z.value=e||((o=l.quizzes)==null?void 0:o.full),_.value=l.title,m.value=!0}async function R(l){var o,t;const e=(o=z.value)==null?void 0:o.id;if(e)if(l==="exam")k.push(`/quiz/${e}?mode=exam`);else{const n=await P.startSession("reading",e);k.push(`/quiz/${((t=n==null?void 0:n.quiz)==null?void 0:t.id)||e}?mode=practice`)}}return $(async()=>{v.value=!0;try{c.value=await G({skillId:1})}finally{v.value=!1}}),(l,e)=>{const o=j("RouterLink");return i(),r("div",null,[a("div",Z,[f(o,{to:"/dashboard",class:"mb-3 inline-flex items-center gap-1.5 text-[12px] text-[var(--ink3)] hover:text-[var(--ink)] transition-colors"},{default:B(()=>[...e[4]||(e[4]=[a("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[a("polyline",{points:"15 18 9 12 15 6"})],-1),A(" Trang chủ ",-1)])]),_:1}),a("div",H,[a("div",null,[e[5]||(e[5]=a("h1",{class:"text-xl font-bold text-[var(--ink)]"},"Reading",-1)),a("p",J,I(c.value.length)+" bộ đề · IELTS Academic & General",1)]),a("div",X,[E(a("input",{"onUpdate:modelValue":e[0]||(e[0]=t=>d.value=t),class:"ct-input w-64",placeholder:"Tìm đề...",onInput:e[1]||(e[1]=t=>u.value=1)},null,544),[[F,d.value]])])])]),v.value?(i(),r("div",Y,[(i(),r(g,null,C(p,t=>a("div",{key:t,class:"h-44 animate-pulse rounded-xl bg-[var(--bg2)]"})),64))])):(i(),r(g,{key:1},[a("div",ee,[(i(!0),r(g,null,C(h.value,t=>{var n,q,y,w;return i(),D(K,{key:t.id,title:t.title,thumbnail:t.thumbnail,"book-code":t.book_code,"skill-label":"Reading","question-count":(q=(n=t.quizzes)==null?void 0:n.full)==null?void 0:q.question_count,time:(w=(y=t.quizzes)==null?void 0:y.full)==null?void 0:w.time,parts:T(t),onStartFull:V=>{var S;return b(t,(S=t.quizzes)==null?void 0:S.full)},onStartPart:V=>b(t,V)},null,8,["title","thumbnail","book-code","question-count","time","parts","onStartFull","onStartPart"])}),128)),h.value.length?N("",!0):(i(),r("p",te,"Không tìm thấy đề phù hợp."))]),a("div",le,[f(O,{modelValue:u.value,"onUpdate:modelValue":e[2]||(e[2]=t=>u.value=t),total:x.value.length,"page-size":p},null,8,["modelValue","total"])])],64)),f(W,{modelValue:m.value,"onUpdate:modelValue":e[3]||(e[3]=t=>m.value=t),"test-title":_.value,onConfirm:R},null,8,["modelValue","test-title"])])}}};export{ue as default};
|
|
|
|
|
|
fronted/dist/assets/Register-ChpOvjos.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{u as S,c as o,a as s,b as B,w as M,d as a,v as u,m as N,F as b,r as f,e as p,t as n,f as T,g as i,h as L,i as C,j as r,k as E,l as U,o as d,n as D}from"./index-CId0hMaT.js";const I={class:"auth-page auth-register"},q={class:"auth-card"},A={class:"form-group"},R={class:"form-group"},j={class:"form-group"},z={class:"target-row"},F={class:"form-group",style:{flex:"1"}},P=["value"],H={class:"form-group",style:{flex:"1"}},G={key:0,class:"error-msg"},J=["disabled"],K={class:"auth-footer"},O={class:"auth-deco"},Q={class:"deco-content"},W={class:"deco-badge"},X={width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",style:{display:"inline","margin-right":"5px"}},Y={class:"milestone-list"},Z={class:"milestone-band font-display"},$={class:"milestone-bar"},ss={class:"milestone-label"},ls={__name:"Register",setup(es){const l=S(),y=U(),m=r(""),v=r(""),h=r(""),c=r(7),g=r(""),k=[5,5.5,6,6.5,7,7.5,8,8.5,9],_=[{band:5,label:"Cơ bản"},{band:6,label:"Du học"},{band:6.5,label:"Đại học"},{band:7,label:"Chuyên nghiệp"},{band:7.5,label:"Thạc sĩ"}];async function w(){await l.register(v.value,h.value,m.value)&&y.push("/dashboard")}return(x,e)=>{const V=E("router-link");return d(),o("div",I,[s("div",q,[e[12]||(e[12]=B('<div class="auth-logo"><div class="logo-mark font-display">Lingua<span>IELTS</span></div><div class="logo-sub">AI-Powered Learning Platform</div></div><h1 class="auth-title font-display">Tạo tài khoản miễn phí</h1><p class="auth-sub">Bắt đầu hành trình chinh phục IELTS của bạn ngay hôm nay.</p>',3)),s("form",{onSubmit:M(w,["prevent"])},[s("div",A,[e[5]||(e[5]=s("label",{class:"form-label",for:"reg-name"},"Họ và tên",-1)),a(s("input",{id:"reg-name","onUpdate:modelValue":e[0]||(e[0]=t=>m.value=t),type:"text",class:"form-input",placeholder:"Nguyễn Văn A",required:""},null,512),[[u,m.value]])]),s("div",R,[e[6]||(e[6]=s("label",{class:"form-label",for:"reg-email"},"Email",-1)),a(s("input",{id:"reg-email","onUpdate:modelValue":e[1]||(e[1]=t=>v.value=t),type:"email",class:"form-input",placeholder:"you@example.com",required:"",autocomplete:"email"},null,512),[[u,v.value]])]),s("div",j,[e[7]||(e[7]=s("label",{class:"form-label",for:"reg-password"},"Mật khẩu",-1)),a(s("input",{id:"reg-password","onUpdate:modelValue":e[2]||(e[2]=t=>h.value=t),type:"password",class:"form-input",placeholder:"Tối thiểu 8 ký tự",required:"",minlength:"8",autocomplete:"new-password"},null,512),[[u,h.value]])]),s("div",z,[s("div",F,[e[8]||(e[8]=s("label",{class:"form-label"},"Band mục tiêu",-1)),a(s("select",{"onUpdate:modelValue":e[3]||(e[3]=t=>c.value=t),class:"form-input"},[(d(),o(b,null,f(k,t=>s("option",{key:t,value:t},n(t),9,P)),64))],512),[[N,c.value]])]),s("div",H,[e[9]||(e[9]=s("label",{class:"form-label"},"Ngày thi (không bắt buộc)",-1)),a(s("input",{"onUpdate:modelValue":e[4]||(e[4]=t=>g.value=t),type:"date",class:"form-input"},null,512),[[u,g.value]])])]),p(l).error?(d(),o("div",G,n(p(l).error),1)):T("",!0),s("button",{type:"submit",class:"btn-primary",disabled:p(l).loading,id:"register-btn"},n(p(l).loading?"Đang tạo tài khoản...":"Bắt đầu học miễn phí →"),9,J)],32),s("div",K,[e[11]||(e[11]=i(" Đã có tài khoản? ",-1)),L(V,{to:"/login",class:"auth-link"},{default:C(()=>[...e[10]||(e[10]=[i("Đăng nhập",-1)])]),_:1})])]),s("div",O,[s("div",Q,[s("div",W,[(d(),o("svg",X,[...e[13]||(e[13]=[s("path",{d:"M22 10v6M2 10l10-5 10 5-10 5z"},null,-1),s("path",{d:"M6 12v5c3 3 9 3 12 0v-5"},null,-1)])])),e[14]||(e[14]=i(" Miễn phí hoàn toàn ",-1))]),e[15]||(e[15]=s("h2",{class:"deco-title font-display"},[i("Luyện thi IELTS"),s("br"),i("thông minh hơn")],-1)),s("div",Y,[(d(),o(b,null,f(_,t=>s("div",{key:t.band,class:"milestone"},[s("div",Z,n(t.band),1),s("div",$,[s("div",{class:"milestone-fill",style:D({width:t.band/9*100+"%"})},null,4)]),s("div",ss,n(t.label),1)])),64))])])])])}}};export{ls as default};
|
fronted/dist/assets/Register-DDXKtuDu.js
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
import{_ as S,u as B,c as l,a as s,b as M,w as N,d as a,v as u,m as T,F as b,r as f,e as p,t as n,f as L,g as i,h as C,i as E,j as d,k as I,l as U,o as r,n as D}from"./index-BnN_E-ke.js";const R={class:"auth-page"},q={class:"auth-card"},A={class:"form-group"},j={class:"form-group"},z={class:"form-group"},F={class:"target-row"},P={class:"form-group",style:{flex:"1"}},H=["value"],G={class:"form-group",style:{flex:"1"}},J={key:0,class:"error-msg"},K=["disabled"],O={class:"auth-footer"},Q={class:"auth-deco"},W={class:"deco-content"},X={class:"deco-badge"},Y={width:"11",height:"11",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",style:{display:"inline","margin-right":"5px"}},Z={class:"milestone-list"},$={class:"milestone-band font-display"},ss={class:"milestone-bar"},ts={class:"milestone-label"},es={__name:"Register",setup(os){const o=B(),y=U(),m=d(""),c=d(""),v=d(""),h=d(7),g=d(""),_=[5,5.5,6,6.5,7,7.5,8,8.5,9],k=[{band:5,label:"Cơ bản"},{band:6,label:"Du học"},{band:6.5,label:"Đại học"},{band:7,label:"Chuyên nghiệp"},{band:7.5,label:"Thạc sĩ"}];async function w(){await o.register(c.value,v.value,m.value)&&y.push("/dashboard")}return(x,t)=>{const V=I("router-link");return r(),l("div",R,[s("div",q,[t[12]||(t[12]=M('<div class="auth-logo" data-v-78a140c8><div class="logo-mark font-display" data-v-78a140c8>Lingua<span data-v-78a140c8>IELTS</span></div><div class="logo-sub" data-v-78a140c8>AI-Powered Learning Platform</div></div><h1 class="auth-title font-display" data-v-78a140c8>Tạo tài khoản miễn phí</h1><p class="auth-sub" data-v-78a140c8>Bắt đầu hành trình chinh phục IELTS của bạn ngay hôm nay.</p>',3)),s("form",{onSubmit:N(w,["prevent"])},[s("div",A,[t[5]||(t[5]=s("label",{class:"form-label",for:"reg-name"},"Họ và tên",-1)),a(s("input",{id:"reg-name","onUpdate:modelValue":t[0]||(t[0]=e=>m.value=e),type:"text",class:"form-input",placeholder:"Nguyễn Văn A",required:""},null,512),[[u,m.value]])]),s("div",j,[t[6]||(t[6]=s("label",{class:"form-label",for:"reg-email"},"Email",-1)),a(s("input",{id:"reg-email","onUpdate:modelValue":t[1]||(t[1]=e=>c.value=e),type:"email",class:"form-input",placeholder:"you@example.com",required:"",autocomplete:"email"},null,512),[[u,c.value]])]),s("div",z,[t[7]||(t[7]=s("label",{class:"form-label",for:"reg-password"},"Mật khẩu",-1)),a(s("input",{id:"reg-password","onUpdate:modelValue":t[2]||(t[2]=e=>v.value=e),type:"password",class:"form-input",placeholder:"Tối thiểu 8 ký tự",required:"",minlength:"8",autocomplete:"new-password"},null,512),[[u,v.value]])]),s("div",F,[s("div",P,[t[8]||(t[8]=s("label",{class:"form-label"},"Band mục tiêu",-1)),a(s("select",{"onUpdate:modelValue":t[3]||(t[3]=e=>h.value=e),class:"form-input"},[(r(),l(b,null,f(_,e=>s("option",{key:e,value:e},n(e),9,H)),64))],512),[[T,h.value]])]),s("div",G,[t[9]||(t[9]=s("label",{class:"form-label"},"Ngày thi (không bắt buộc)",-1)),a(s("input",{"onUpdate:modelValue":t[4]||(t[4]=e=>g.value=e),type:"date",class:"form-input"},null,512),[[u,g.value]])])]),p(o).error?(r(),l("div",J,n(p(o).error),1)):L("",!0),s("button",{type:"submit",class:"btn-primary",disabled:p(o).loading,id:"register-btn"},n(p(o).loading?"Đang tạo tài khoản...":"Bắt đầu học miễn phí →"),9,K)],32),s("div",O,[t[11]||(t[11]=i(" Đã có tài khoản? ",-1)),C(V,{to:"/login",class:"auth-link"},{default:E(()=>[...t[10]||(t[10]=[i("Đăng nhập",-1)])]),_:1})])]),s("div",Q,[s("div",W,[s("div",X,[(r(),l("svg",Y,[...t[13]||(t[13]=[s("path",{d:"M22 10v6M2 10l10-5 10 5-10 5z"},null,-1),s("path",{d:"M6 12v5c3 3 9 3 12 0v-5"},null,-1)])])),t[14]||(t[14]=i(" Miễn phí hoàn toàn ",-1))]),t[15]||(t[15]=s("h2",{class:"deco-title font-display"},[i("Luyện thi IELTS"),s("br"),i("thông minh hơn")],-1)),s("div",Z,[(r(),l(b,null,f(k,e=>s("div",{key:e.band,class:"milestone"},[s("div",$,n(e.band),1),s("div",ss,[s("div",{class:"milestone-fill",style:D({width:e.band/9*100+"%"})},null,4)]),s("div",ts,n(e.label),1)])),64))])])])])}}},as=S(es,[["__scopeId","data-v-78a140c8"]]);export{as as default};
|
|
|
|
|
|
fronted/dist/assets/Register-EQfUP2x7.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
.auth-page[data-v-78a140c8]{min-height:100vh;display:grid;grid-template-columns:1fr 1fr;background:var(--bg)}.auth-card[data-v-78a140c8]{display:flex;flex-direction:column;justify-content:center;padding:48px 64px;background:var(--surface)}.auth-logo[data-v-78a140c8]{margin-bottom:28px}.logo-mark[data-v-78a140c8]{font-size:24px;font-weight:700;color:var(--ink);letter-spacing:-.02em}.logo-mark span[data-v-78a140c8]{color:var(--green-l)}.logo-sub[data-v-78a140c8]{font-size:11px;color:var(--ink3);letter-spacing:.1em;text-transform:uppercase;margin-top:3px}.auth-title[data-v-78a140c8]{font-size:26px;font-weight:700;color:var(--ink);margin-bottom:6px}.auth-sub[data-v-78a140c8]{font-size:13px;color:var(--ink3);margin-bottom:28px}.form-group[data-v-78a140c8]{margin-bottom:14px}.form-label[data-v-78a140c8]{display:block;font-size:12px;font-weight:600;color:var(--ink2);margin-bottom:5px}.target-row[data-v-78a140c8]{display:flex;gap:12px}.form-input[data-v-78a140c8]{width:100%;padding:10px 13px;border:1.5px solid var(--border2);border-radius:var(--r-sm);font-size:13px;font-family:inherit;color:var(--ink);background:var(--bg);outline:none;transition:border-color .18s}.form-input[data-v-78a140c8]:focus{border-color:var(--green-l);background:#fff}.error-msg[data-v-78a140c8]{font-size:13px;color:var(--rose);background:var(--rose-bg);padding:10px 14px;border-radius:var(--r-sm);margin-bottom:12px}.btn-primary[data-v-78a140c8]{width:100%;padding:12px;border-radius:var(--r-sm);background:var(--green);color:#fff;font-size:14px;font-weight:600;border:none;cursor:pointer;font-family:inherit;transition:all .18s;margin-top:4px}.btn-primary[data-v-78a140c8]:hover{background:#245c42;transform:translateY(-1px)}.btn-primary[data-v-78a140c8]:disabled{opacity:.6;cursor:not-allowed;transform:none}.auth-footer[data-v-78a140c8]{margin-top:20px;font-size:13px;color:var(--ink3);text-align:center}.auth-link[data-v-78a140c8]{color:var(--green);font-weight:600;text-decoration:none;margin-left:4px}.auth-deco[data-v-78a140c8]{background:var(--ink);display:flex;align-items:center;justify-content:center;padding:60px;position:relative;overflow:hidden}.auth-deco[data-v-78a140c8]:before{content:"";position:absolute;bottom:-100px;right:-80px;width:400px;height:400px;background:radial-gradient(circle,rgba(82,183,136,.2) 0%,transparent 70%)}.deco-content[data-v-78a140c8]{position:relative;z-index:1;width:100%}.deco-badge[data-v-78a140c8]{display:inline-block;padding:5px 14px;border-radius:20px;background:#52b78833;color:var(--green-l);font-size:12px;font-weight:600;margin-bottom:20px;border:1px solid rgba(82,183,136,.3)}.deco-title[data-v-78a140c8]{font-size:32px;font-weight:700;color:#fff;line-height:1.2;margin-bottom:32px}.milestone-list[data-v-78a140c8]{display:flex;flex-direction:column;gap:14px}.milestone[data-v-78a140c8]{display:flex;align-items:center;gap:12px}.milestone-band[data-v-78a140c8]{font-size:16px;font-weight:700;color:var(--green-l);width:36px;flex-shrink:0}.milestone-bar[data-v-78a140c8]{flex:1;height:6px;background:#ffffff1a;border-radius:99px;overflow:hidden}.milestone-fill[data-v-78a140c8]{height:100%;background:linear-gradient(to right,var(--green-l),var(--blue-l));border-radius:99px}.milestone-label[data-v-78a140c8]{font-size:12px;color:#ffffff80;width:80px}@media (max-width: 768px){.auth-page[data-v-78a140c8]{grid-template-columns:1fr}.auth-deco[data-v-78a140c8]{display:none}.auth-card[data-v-78a140c8]{padding:40px 24px}}
|
|
|
|
|
|
fronted/dist/assets/{Result-Czy97pS1.js → Result-mQyw3_em.js}
RENAMED
|
@@ -1 +1 @@
|
|
| 1 |
-
import{H as $,j as g,z as w,G as D,q as Q,c,a as e,h as A,i as _,F as B,t as a,r as z,f as j,e as F,x as U,B as V,k as K,o as d,g as q,n as C}from"./index-BnN_E-ke.js";import{u as E}from"./practice-BINrC2xL.js";const G={Mathematics:[{id:1,question:"What is the derivative of x²?",options:["x","2x","2","x²"],answer:1},{id:2,question:"Solve: 3x + 6 = 21",options:["x=3","x=4","x=5","x=7"],answer:2},{id:3,question:"What is π to 2 decimal places?",options:["3.12","3.14","3.16","3.18"],answer:1},{id:4,question:"Integral of cos(x)?",options:["-sin(x)+C","sin(x)+C","cos(x)+C","tan(x)+C"],answer:1},{id:5,question:"Sum of angles in a triangle?",options:["90°","180°","270°","360°"],answer:1}],Physics:[{id:1,question:"Newton's 2nd law: F = ?",options:["mv","ma","mv²","m/a"],answer:1},{id:2,question:"Speed of light in vacuum?",options:["3×10⁶ m/s","3×10⁸ m/s","3×10¹⁰ m/s","3×10⁴ m/s"],answer:1},{id:3,question:"Conservation of energy is which law?",options:["1st Law of Thermodynamics","2nd Law","Newton 3rd","Ohm's Law"],answer:0},{id:4,question:"SI unit of electric current?",options:["Volt","Watt","Ampere","Ohm"],answer:2},{id:5,question:"Gravitational acceleration on Earth?",options:["8.9 m/s²","9.8 m/s²","10.8 m/s²","11 m/s²"],answer:1}],Chemistry:[{id:1,question:"Atomic number of Carbon?",options:["4","6","8","12"],answer:1},{id:2,question:"Water is composed of?",options:["H₂O₂","HO","H₂O","H₃O"],answer:2},{id:3,question:'Symbol "Au" is for?',options:["Silver","Aluminum","Gold","Argon"],answer:2},{id:4,question:"pH of pure water?",options:["5","6","7","8"],answer:2},{id:5,question:"Periodic table developed by?",options:["Newton","Mendeleev","Einstein","Bohr"],answer:1}],Biology:[{id:1,question:"Powerhouse of the cell?",options:["Nucleus","Ribosome","Mitochondria","Lysosome"],answer:2},{id:2,question:"DNA stands for?",options:["Deoxyribonucleic Acid","Diribonucleic Acid","Deoxyribose Nucleic Acid","Dioxynucleic Acid"],answer:0},{id:3,question:"Chromosomes in human cell?",options:["23","44","46","48"],answer:2},{id:4,question:"Plants make food via?",options:["Respiration","Photosynthesis","Digestion","Fermentation"],answer:1},{id:5,question:"Universal blood donor type?",options:["A","B","AB","O"],answer:3}],"Computer Science":[{id:1,question:"CPU stands for?",options:["Central Processing Unit","Computer Processing Unit","Central Program Unit","Control Processing Unit"],answer:0},{id:2,question:"FIFO data structure?",options:["Stack","Queue","Tree","Graph"],answer:1},{id:3,question:"Binary search complexity?",options:["O(n)","O(n²)","O(log n)","O(1)"],answer:2},{id:4,question:'"Mother of all languages"?',options:["Python","Java","C","Assembly"],answer:2},{id:5,question:"HTTP stands for?",options:["HyperText Transfer Protocol","High Transfer Text Protocol","HyperText Transmission Protocol","Hybrid Text Transfer Protocol"],answer:0}]},W=$("quiz",()=>{const y=g(null),x=g([]),m=g({}),p=g(0),f=g(null),l=g(!1),k=g(null),S=w(()=>x.value[p.value]),n=w(()=>x.value.length),t=w(()=>p.value===n.value-1),i=w(()=>Object.keys(m.value).length),o=w(()=>n.value?Math.round(i.value/n.value*100):0);function r(u){y.value=u,x.value=G[u]||[],m.value={},p.value=0,f.value=null,k.value=null}function s(u,P){m.value[u]=P}function v(){t.value||p.value++}function b(){p.value>0&&p.value--}async function T(){var M,L;l.value=!0,k.value=null;let u=0;const P=x.value.map(h=>{const H=m.value[h.id]??-1,R=h.answer===H;return R&&u++,{questionId:h.id,question:h.question,selected:H,correct:h.answer,isCorrect:R}}),N=x.value.length,O=Math.round(u/N*100);try{const{data:h}=await D.post("/history/save",{quiz_id:`${y.value}-${Date.now()}`,subject:y.value,score:u,total_questions:N,percentage:O,answers:P});return f.value={...h,score:u,total:N,percentage:O,detailedAnswers:P,subject:y.value},f.value}catch(h){return k.value=((L=(M=h.response)==null?void 0:M.data)==null?void 0:L.detail)||"Failed to submit quiz",null}finally{l.value=!1}}function I(){f.value=null}return{currentSubject:y,questions:x,userAnswers:m,currentIndex:p,result:f,loading:l,error:k,currentQuestion:S,totalQuestions:n,isLastQuestion:t,answeredCount:i,progressPercent:o,startQuiz:r,selectAnswer:s,nextQuestion:v,prevQuestion:b,submitQuiz:T,clearResult:I}}),J={class:"min-h-screen bg-[var(--bg)] py-8"},X={class:"mx-auto w-full max-w-2xl px-4"},Y={key:0,class:"ct-card p-8 text-center"},Z={class:"ct-card mb-5 overflow-hidden"},ee={class:"p-6"},te={class:"mb-6 flex items-start justify-between gap-4"},se={class:"mt-1 text-base font-bold text-[var(--ink)]"},oe={class:"shrink-0 rounded-xl border border-[var(--border)] bg-[var(--bg)] px-4 py-2 text-center"},ne={class:"text-xl font-extrabold text-[var(--ink)]"},re={class:"flex flex-wrap items-center gap-8"},ie={class:"relative shrink-0"},le={viewBox:"0 0 120 120",width:"120",height:"120"},ae=["stroke-dasharray"],de={class:"absolute inset-0 flex flex-col items-center justify-center"},ce={class:"text-2xl font-extrabold text-[var(--ink)]"},ue={class:"flex flex-1 flex-wrap gap-3"},pe={class:"flex-1 rounded-xl border border-[var(--border)] bg-[#f0fdf4] px-4 py-3 text-center"},ve={class:"mt-1 text-2xl font-extrabold text-[#059669]"},xe={class:"flex-1 rounded-xl border border-[var(--border)] bg-[var(--rose-bg)] px-4 py-3 text-center"},fe={class:"mt-1 text-2xl font-extrabold text-[var(--rose)]"},be={class:"flex-1 rounded-xl border border-[var(--border)] bg-[var(--bg2)] px-4 py-3 text-center"},he={class:"mt-1 text-2xl font-extrabold text-[var(--ink)]"},we={key:0,class:"ct-card mb-5 overflow-hidden"},me={class:"mb-3 flex items-center justify-between"},ge={class:"text-[12px] font-bold text-[var(--ink2)]"},ye={class:"grid grid-cols-2 gap-x-6 gap-y-2"},ke={class:"truncate font-medium text-[var(--ink)]"},_e={key:0,class:"shrink-0",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"#059669","stroke-width":"2.5"},qe={key:1,class:"shrink-0",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"#e11d48","stroke-width":"2.5"},Ce={key:1},Ae={class:"mb-3 flex items-center justify-between"},Se={class:"rounded-full px-2.5 py-0.5 text-[11px] font-semibold",style:{background:"#d1fae5",color:"#065f46"}},Ie={class:"flex flex-col gap-3"},Pe={class:"flex"},Be={class:"flex-1 p-4"},je={class:"mb-2 flex items-center justify-between gap-3"},Te={class:"text-[11px] font-bold uppercase tracking-wider text-[var(--ink3)]"},Ne={key:0,class:"mb-3 text-[13px] leading-relaxed text-[var(--ink)]"},ze={class:"rounded-lg border border-[var(--border)] bg-[var(--bg)] p-3 text-[12px]"},Oe={class:"flex items-center justify-between gap-2"},Me={key:0,class:"mt-1.5 flex items-center justify-between gap-2 border-t border-[var(--border)] pt-1.5"},Le={class:"font-semibold font-mono",style:{color:"#059669"}},He={key:2,class:"ct-card p-6 text-center text-[var(--ink3)]"},Re={class:"mt-6 flex flex-wrap justify-center gap-3"},Qe={__name:"Result",setup(y){const x=W(),m=E(),p=V(),f=w(()=>m.lastResult||x.result),l=w(()=>{const n=f.value||{},t=Number(n.score??0),i=Number(n.total_questions??n.total??0),o=Number(n.percentage??(i?Math.round(t/i*100):0)),r=Array.isArray(n.details)?n.details.map(s=>({questionId:s.question_id,order:s.order||0,question:s.question,userAnswer:s.user_answer,correctAnswer:s.correct_answer??(Array.isArray(s.correct_answers)?s.correct_answers.join(" / "):null),isCorrect:!!s.is_correct,partIndex:s.part_index??null,partTitle:s.part_title??null})):Array.isArray(n.detailedAnswers)?n.detailedAnswers:[];return{score:t,total:i,percentage:o,subject:n.subject||"Listening",detailedAnswers:r}}),k=w(()=>{const n=l.value.detailedAnswers;if(!n.length)return[];if(n.some(r=>r.partIndex!==null&&r.partIndex!==void 0)){const r=new Map;for(const s of n){const v=s.partIndex??0;r.has(v)||r.set(v,{label:s.partTitle||`Part ${v+1}`,answers:[],correct:0,total:0});const b=r.get(v);b.answers.push(s),b.total++,s.isCorrect&&b.correct++}return Array.from(r.values()).sort((s,v)=>{var I,u;const b=parseInt(((I=s.label.match(/\d+/))==null?void 0:I[0])||0),T=parseInt(((u=v.label.match(/\d+/))==null?void 0:u[0])||0);return b-T})}const i=10,o=[];for(let r=0;r<n.length;r+=i){const s=n.slice(r,r+i),v=Math.floor(r/i)+1;o.push({label:`Part ${v}`,answers:s,correct:s.filter(b=>b.isCorrect).length,total:s.length})}return o}),S=w(()=>{var o;const n=p.params.sessionId,t=p.query.annotationSession;if(n){const r=t?`?annotationSession=${t}`:"";return`/review/${n}${r}`}const i=(o=f.value)==null?void 0:o.quizId;return i?`/review/quiz/${i}`:null});return Q(async()=>{const n=p.params.sessionId;n&&await m.fetchResult(n)}),(n,t)=>{const i=K("RouterLink");return d(),c("div",J,[e("div",X,[A(i,{to:"/dashboard",class:"mb-5 inline-flex items-center gap-1.5 text-[12px] text-[var(--ink3)] hover:text-[var(--ink)] transition-colors"},{default:_(()=>[...t[1]||(t[1]=[e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("polyline",{points:"15 18 9 12 15 6"})],-1),q(" Về trang chủ ",-1)])]),_:1}),f.value?(d(),c(B,{key:1},[e("div",Z,[t[11]||(t[11]=e("div",{class:"h-1 w-full",style:{background:"#34d399"}},null,-1)),e("div",ee,[e("div",te,[e("div",null,[t[4]||(t[4]=e("div",{class:"text-[11px] font-bold uppercase tracking-wider text-[var(--ink3)]"},"Kết quả luyện tập",-1)),e("div",se,a(l.value.subject),1)]),e("div",oe,[t[5]||(t[5]=e("div",{class:"text-[10px] text-[var(--ink3)]"},"Điểm",-1)),e("div",ne,a(l.value.score)+"/"+a(l.value.total),1)])]),e("div",re,[e("div",ie,[(d(),c("svg",le,[t[6]||(t[6]=e("circle",{cx:"60",cy:"60",r:"50",fill:"none",stroke:"#f3f4f6","stroke-width":"10"},null,-1)),e("circle",{cx:"60",cy:"60",r:"50",fill:"none",stroke:"#34d399","stroke-width":"10","stroke-linecap":"round","stroke-dasharray":`${l.value.percentage*3.14} 314`,transform:"rotate(-90 60 60)",style:{transition:"stroke-dasharray 1.2s ease"}},null,8,ae)])),e("div",de,[e("span",ce,a(l.value.percentage)+"%",1),t[7]||(t[7]=e("span",{class:"text-[10px] text-[var(--ink3)]"},"Score",-1))])]),e("div",ue,[e("div",pe,[t[8]||(t[8]=e("div",{class:"text-[10px] font-semibold uppercase tracking-wide text-[#059669]"},"Đúng",-1)),e("div",ve,a(l.value.score),1)]),e("div",xe,[t[9]||(t[9]=e("div",{class:"text-[10px] font-semibold uppercase tracking-wide text-[var(--rose)]"},"Sai",-1)),e("div",fe,a(l.value.total-l.value.score),1)]),e("div",be,[t[10]||(t[10]=e("div",{class:"text-[10px] font-semibold uppercase tracking-wide text-[var(--ink3)]"},"Tổng",-1)),e("div",he,a(l.value.total),1)])])])])]),l.value.detailedAnswers.length?(d(),c("div",we,[t[15]||(t[15]=e("div",{class:"border-b border-[var(--border)] px-5 py-3.5"},[e("span",{class:"text-[14px] font-bold text-[var(--ink)]"},"Answer Key")],-1)),(d(!0),c(B,null,z(k.value,(o,r)=>(d(),c("div",{key:r,class:"border-b border-[var(--border)] last:border-0 px-5 py-4"},[e("div",me,[e("span",ge,a(o.label),1),e("span",{class:"rounded-full px-2.5 py-0.5 text-[11px] font-semibold",style:C(o.correct===o.total?"background:#d1fae5;color:#065f46":"background:#ffe4e6;color:#be123c")},a(o.correct)+"/"+a(o.total)+" đúng ",5)]),e("div",ye,[(d(!0),c(B,null,z(o.answers,s=>(d(),c("div",{key:s.questionId,class:"flex items-center gap-2 text-[12px]"},[e("div",{class:"flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] font-bold",style:C(s.isCorrect?"background:#d1fae5;color:#065f46":"background:#ffe4e6;color:#be123c")},a(s.order),5),e("span",ke,a(s.correctAnswer??"—"),1),t[14]||(t[14]=e("span",{class:"text-[var(--ink3)]"}," : ",-1)),e("span",{class:"font-mono font-semibold",style:C(s.isCorrect?"color:#059669":"color:#e11d48")},a(s.userAnswer??"—"),5),s.isCorrect?(d(),c("svg",_e,[...t[12]||(t[12]=[e("polyline",{points:"20 6 9 17 4 12"},null,-1)])])):(d(),c("svg",qe,[...t[13]||(t[13]=[e("circle",{cx:"12",cy:"12",r:"10"},null,-1),e("line",{x1:"15",y1:"9",x2:"9",y2:"15"},null,-1),e("line",{x1:"9",y1:"9",x2:"15",y2:"15"},null,-1)])]))]))),128))])]))),128))])):j("",!0),l.value.detailedAnswers.length?(d(),c("div",Ce,[e("div",Ae,[t[16]||(t[16]=e("span",{class:"text-[13px] font-bold text-[var(--ink)]"},"Review đáp án chi tiết",-1)),e("span",Se,a(l.value.score)+" đúng",1)]),e("div",Ie,[(d(!0),c(B,null,z(l.value.detailedAnswers,(o,r)=>(d(),c("div",{key:o.questionId||r,class:"ct-card overflow-hidden"},[e("div",Pe,[e("div",{class:"w-1 shrink-0 rounded-l-xl",style:C(o.isCorrect?"background:#34d399":"background:#f43f5e")},null,4),e("div",Be,[e("div",je,[e("span",Te,"Câu "+a(o.order||r+1),1),e("span",{class:"rounded-full px-2 py-0.5 text-[10px] font-bold uppercase",style:C(o.isCorrect?"background:#d1fae5;color:#065f46":"background:#ffe4e6;color:#be123c")},a(o.isCorrect?"✓ Đúng":"✗ Sai"),5)]),o.question?(d(),c("p",Ne,a(o.question),1)):j("",!0),e("div",ze,[e("div",Oe,[t[17]||(t[17]=e("span",{class:"text-[var(--ink3)]"},"Đáp án của tôi",-1)),e("span",{class:"font-semibold font-mono",style:C(o.isCorrect?"color:#059669":"color:#e11d48")},a(o.userAnswer??"—"),5)]),o.isCorrect?j("",!0):(d(),c("div",Me,[t[18]||(t[18]=e("span",{class:"text-[var(--ink3)]"},"Đáp án đúng",-1)),e("span",Le,a(o.correctAnswer??"—"),1)]))])])])]))),128))])])):(d(),c("div",He," Chưa có thông tin chi tiết đáp án. ")),e("div",Re,[A(i,{to:"/dashboard",class:"ct-btn"},{default:_(()=>[...t[19]||(t[19]=[e("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("rect",{x:"3",y:"3",width:"7",height:"7"}),e("rect",{x:"14",y:"3",width:"7",height:"7"}),e("rect",{x:"14",y:"14",width:"7",height:"7"}),e("rect",{x:"3",y:"14",width:"7",height:"7"})],-1),q(" Dashboard ",-1)])]),_:1}),A(i,{to:`/${String(l.value.subject).toLowerCase()}`,class:"ct-btn",onClick:t[0]||(t[0]=o=>{var r,s;return(s=(r=F(x)).clearResult)==null?void 0:s.call(r)})},{default:_(()=>[...t[20]||(t[20]=[e("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"1 4 1 10 7 10"}),e("path",{d:"M3.51 15a9 9 0 1 0 .49-3.5"})],-1),q(" Thử lại ",-1)])]),_:1},8,["to"]),S.value?(d(),U(i,{key:0,to:S.value,class:"ct-btn",style:{"border-color":"#15803d",color:"#15803d",background:"#f0fdf4"}},{default:_(()=>[...t[21]||(t[21]=[e("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"}),e("circle",{cx:"12",cy:"12",r:"3"})],-1),q(" Xem lời giải ",-1)])]),_:1},8,["to"])):j("",!0),A(i,{to:"/history",class:"ct-btn"},{default:_(()=>[...t[22]||(t[22]=[e("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("circle",{cx:"12",cy:"12",r:"10"}),e("polyline",{points:"12 6 12 12 16 14"})],-1),q(" Lịch sử ",-1)])]),_:1})])],64)):(d(),c("div",Y,[t[3]||(t[3]=e("div",{class:"mb-2 text-base font-semibold text-[var(--ink)]"},"Không tìm thấy kết quả",-1)),A(i,{to:"/dashboard",class:"ct-btn mt-4 inline-flex"},{default:_(()=>[...t[2]||(t[2]=[q("Về Dashboard",-1)])]),_:1})]))])])}}};export{Qe as default};
|
|
|
|
| 1 |
+
import{O as D,j as g,z as w,M as H,q as Q,c,a as e,h as A,i as _,F as j,t as a,r as O,f as B,e as F,x as U,C as V,k as K,o as d,g as q,n as C}from"./index-CId0hMaT.js";import{u as E}from"./practice-nWRX1lS1.js";const W={Mathematics:[{id:1,question:"What is the derivative of x²?",options:["x","2x","2","x²"],answer:1},{id:2,question:"Solve: 3x + 6 = 21",options:["x=3","x=4","x=5","x=7"],answer:2},{id:3,question:"What is π to 2 decimal places?",options:["3.12","3.14","3.16","3.18"],answer:1},{id:4,question:"Integral of cos(x)?",options:["-sin(x)+C","sin(x)+C","cos(x)+C","tan(x)+C"],answer:1},{id:5,question:"Sum of angles in a triangle?",options:["90°","180°","270°","360°"],answer:1}],Physics:[{id:1,question:"Newton's 2nd law: F = ?",options:["mv","ma","mv²","m/a"],answer:1},{id:2,question:"Speed of light in vacuum?",options:["3×10⁶ m/s","3×10⁸ m/s","3×10¹⁰ m/s","3×10⁴ m/s"],answer:1},{id:3,question:"Conservation of energy is which law?",options:["1st Law of Thermodynamics","2nd Law","Newton 3rd","Ohm's Law"],answer:0},{id:4,question:"SI unit of electric current?",options:["Volt","Watt","Ampere","Ohm"],answer:2},{id:5,question:"Gravitational acceleration on Earth?",options:["8.9 m/s²","9.8 m/s²","10.8 m/s²","11 m/s²"],answer:1}],Chemistry:[{id:1,question:"Atomic number of Carbon?",options:["4","6","8","12"],answer:1},{id:2,question:"Water is composed of?",options:["H₂O₂","HO","H₂O","H₃O"],answer:2},{id:3,question:'Symbol "Au" is for?',options:["Silver","Aluminum","Gold","Argon"],answer:2},{id:4,question:"pH of pure water?",options:["5","6","7","8"],answer:2},{id:5,question:"Periodic table developed by?",options:["Newton","Mendeleev","Einstein","Bohr"],answer:1}],Biology:[{id:1,question:"Powerhouse of the cell?",options:["Nucleus","Ribosome","Mitochondria","Lysosome"],answer:2},{id:2,question:"DNA stands for?",options:["Deoxyribonucleic Acid","Diribonucleic Acid","Deoxyribose Nucleic Acid","Dioxynucleic Acid"],answer:0},{id:3,question:"Chromosomes in human cell?",options:["23","44","46","48"],answer:2},{id:4,question:"Plants make food via?",options:["Respiration","Photosynthesis","Digestion","Fermentation"],answer:1},{id:5,question:"Universal blood donor type?",options:["A","B","AB","O"],answer:3}],"Computer Science":[{id:1,question:"CPU stands for?",options:["Central Processing Unit","Computer Processing Unit","Central Program Unit","Control Processing Unit"],answer:0},{id:2,question:"FIFO data structure?",options:["Stack","Queue","Tree","Graph"],answer:1},{id:3,question:"Binary search complexity?",options:["O(n)","O(n²)","O(log n)","O(1)"],answer:2},{id:4,question:'"Mother of all languages"?',options:["Python","Java","C","Assembly"],answer:2},{id:5,question:"HTTP stands for?",options:["HyperText Transfer Protocol","High Transfer Text Protocol","HyperText Transmission Protocol","Hybrid Text Transfer Protocol"],answer:0}]},G=D("quiz",()=>{const y=g(null),x=g([]),m=g({}),p=g(0),f=g(null),l=g(!1),k=g(null),S=w(()=>x.value[p.value]),n=w(()=>x.value.length),t=w(()=>p.value===n.value-1),i=w(()=>Object.keys(m.value).length),o=w(()=>n.value?Math.round(i.value/n.value*100):0);function r(u){y.value=u,x.value=W[u]||[],m.value={},p.value=0,f.value=null,k.value=null}function s(u,P){m.value[u]=P}function v(){t.value||p.value++}function b(){p.value>0&&p.value--}async function T(){var M,L;l.value=!0,k.value=null;let u=0;const P=x.value.map(h=>{const R=m.value[h.id]??-1,$=h.answer===R;return $&&u++,{questionId:h.id,question:h.question,selected:R,correct:h.answer,isCorrect:$}}),N=x.value.length,z=Math.round(u/N*100);try{const{data:h}=await H.post("/history/save",{quiz_id:`${y.value}-${Date.now()}`,subject:y.value,score:u,total_questions:N,percentage:z,answers:P});return f.value={...h,score:u,total:N,percentage:z,detailedAnswers:P,subject:y.value},f.value}catch(h){return k.value=((L=(M=h.response)==null?void 0:M.data)==null?void 0:L.detail)||"Failed to submit quiz",null}finally{l.value=!1}}function I(){f.value=null}return{currentSubject:y,questions:x,userAnswers:m,currentIndex:p,result:f,loading:l,error:k,currentQuestion:S,totalQuestions:n,isLastQuestion:t,answeredCount:i,progressPercent:o,startQuiz:r,selectAnswer:s,nextQuestion:v,prevQuestion:b,submitQuiz:T,clearResult:I}}),J={class:"min-h-screen bg-[var(--bg)] py-8"},X={class:"mx-auto w-full max-w-2xl px-4"},Y={key:0,class:"ct-card p-8 text-center"},Z={class:"ct-card mb-5 overflow-hidden"},ee={class:"p-6"},te={class:"mb-6 flex items-start justify-between gap-4"},se={class:"mt-1 text-base font-bold text-[var(--ink)]"},oe={class:"shrink-0 rounded-xl border border-[var(--border)] bg-[var(--bg)] px-4 py-2 text-center"},ne={class:"text-xl font-extrabold text-[var(--ink)]"},re={class:"flex flex-wrap items-center gap-8"},ie={class:"relative shrink-0"},le={viewBox:"0 0 120 120",width:"120",height:"120"},ae=["stroke-dasharray"],de={class:"absolute inset-0 flex flex-col items-center justify-center"},ce={class:"text-2xl font-extrabold text-[var(--ink)]"},ue={class:"flex flex-1 flex-wrap gap-3"},pe={class:"flex-1 rounded-xl border border-[var(--border)] bg-[#f0fdf4] px-4 py-3 text-center"},ve={class:"mt-1 text-2xl font-extrabold text-[#059669]"},xe={class:"flex-1 rounded-xl border border-[var(--border)] bg-[var(--rose-bg)] px-4 py-3 text-center"},fe={class:"mt-1 text-2xl font-extrabold text-[var(--rose)]"},be={class:"flex-1 rounded-xl border border-[var(--border)] bg-[var(--bg2)] px-4 py-3 text-center"},he={class:"mt-1 text-2xl font-extrabold text-[var(--ink)]"},we={key:0,class:"ct-card mb-5 overflow-hidden"},me={class:"mb-3 flex items-center justify-between"},ge={class:"text-[12px] font-bold text-[var(--ink2)]"},ye={class:"grid grid-cols-2 gap-x-6 gap-y-2"},ke={class:"truncate font-medium text-[var(--ink)]"},_e={key:0,class:"shrink-0",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"#059669","stroke-width":"2.5"},qe={key:1,class:"shrink-0",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"#e11d48","stroke-width":"2.5"},Ce={key:1},Ae={class:"mb-3 flex items-center justify-between"},Se={class:"rounded-full px-2.5 py-0.5 text-[11px] font-semibold",style:{background:"#d1fae5",color:"#065f46"}},Ie={class:"flex flex-col gap-3"},Pe={class:"flex"},je={class:"flex-1 p-4"},Be={class:"mb-2 flex items-center justify-between gap-3"},Te={class:"text-[11px] font-bold uppercase tracking-wider text-[var(--ink3)]"},Ne={key:0,class:"mb-3 text-[13px] leading-relaxed text-[var(--ink)]"},Oe={class:"rounded-lg border border-[var(--border)] bg-[var(--bg)] p-3 text-[12px]"},ze={class:"flex items-center justify-between gap-2"},Me={key:0,class:"mt-1.5 flex items-center justify-between gap-2 border-t border-[var(--border)] pt-1.5"},Le={class:"font-semibold font-mono",style:{color:"#059669"}},Re={key:2,class:"ct-card p-6 text-center text-[var(--ink3)]"},$e={class:"mt-6 flex flex-wrap justify-center gap-3"},Qe={__name:"Result",setup(y){const x=G(),m=E(),p=V(),f=w(()=>m.lastResult||x.result),l=w(()=>{const n=f.value||{},t=Number(n.score??0),i=Number(n.total_questions??n.total??0),o=Number(n.percentage??(i?Math.round(t/i*100):0)),r=Array.isArray(n.details)?n.details.map(s=>({questionId:s.question_id,order:s.order||0,question:s.question,userAnswer:s.user_answer,correctAnswer:s.correct_answer??(Array.isArray(s.correct_answers)?s.correct_answers.join(" / "):null),isCorrect:!!s.is_correct,partIndex:s.part_index??null,partTitle:s.part_title??null})):Array.isArray(n.detailedAnswers)?n.detailedAnswers:[];return{score:t,total:i,percentage:o,subject:n.subject||"Listening",detailedAnswers:r}}),k=w(()=>{const n=l.value.detailedAnswers;if(!n.length)return[];if(n.some(r=>r.partIndex!==null&&r.partIndex!==void 0)){const r=new Map;for(const s of n){const v=s.partIndex??0;r.has(v)||r.set(v,{label:s.partTitle||`Part ${v+1}`,answers:[],correct:0,total:0});const b=r.get(v);b.answers.push(s),b.total++,s.isCorrect&&b.correct++}return Array.from(r.values()).sort((s,v)=>{var I,u;const b=parseInt(((I=s.label.match(/\d+/))==null?void 0:I[0])||0),T=parseInt(((u=v.label.match(/\d+/))==null?void 0:u[0])||0);return b-T})}const i=10,o=[];for(let r=0;r<n.length;r+=i){const s=n.slice(r,r+i),v=Math.floor(r/i)+1;o.push({label:`Part ${v}`,answers:s,correct:s.filter(b=>b.isCorrect).length,total:s.length})}return o}),S=w(()=>{var o;const n=p.params.sessionId,t=p.query.annotationSession;if(n){const r=t?`?annotationSession=${t}`:"";return`/review/${n}${r}`}const i=(o=f.value)==null?void 0:o.quizId;return i?`/review/quiz/${i}`:null});return Q(async()=>{const n=p.params.sessionId;n&&await m.fetchResult(n)}),(n,t)=>{const i=K("RouterLink");return d(),c("div",J,[e("div",X,[A(i,{to:"/dashboard",class:"mb-5 inline-flex items-center gap-1.5 text-[12px] text-[var(--ink3)] hover:text-[var(--ink)] transition-colors"},{default:_(()=>[...t[1]||(t[1]=[e("svg",{width:"13",height:"13",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("polyline",{points:"15 18 9 12 15 6"})],-1),q(" Về trang chủ ",-1)])]),_:1}),f.value?(d(),c(j,{key:1},[e("div",Z,[t[11]||(t[11]=e("div",{class:"h-1 w-full",style:{background:"#34d399"}},null,-1)),e("div",ee,[e("div",te,[e("div",null,[t[4]||(t[4]=e("div",{class:"text-[11px] font-bold uppercase tracking-wider text-[var(--ink3)]"},"Kết quả luyện tập",-1)),e("div",se,a(l.value.subject),1)]),e("div",oe,[t[5]||(t[5]=e("div",{class:"text-[10px] text-[var(--ink3)]"},"Điểm",-1)),e("div",ne,a(l.value.score)+"/"+a(l.value.total),1)])]),e("div",re,[e("div",ie,[(d(),c("svg",le,[t[6]||(t[6]=e("circle",{cx:"60",cy:"60",r:"50",fill:"none",stroke:"#f3f4f6","stroke-width":"10"},null,-1)),e("circle",{cx:"60",cy:"60",r:"50",fill:"none",stroke:"#34d399","stroke-width":"10","stroke-linecap":"round","stroke-dasharray":`${l.value.percentage*3.14} 314`,transform:"rotate(-90 60 60)",style:{transition:"stroke-dasharray 1.2s ease"}},null,8,ae)])),e("div",de,[e("span",ce,a(l.value.percentage)+"%",1),t[7]||(t[7]=e("span",{class:"text-[10px] text-[var(--ink3)]"},"Score",-1))])]),e("div",ue,[e("div",pe,[t[8]||(t[8]=e("div",{class:"text-[10px] font-semibold uppercase tracking-wide text-[#059669]"},"Đúng",-1)),e("div",ve,a(l.value.score),1)]),e("div",xe,[t[9]||(t[9]=e("div",{class:"text-[10px] font-semibold uppercase tracking-wide text-[var(--rose)]"},"Sai",-1)),e("div",fe,a(l.value.total-l.value.score),1)]),e("div",be,[t[10]||(t[10]=e("div",{class:"text-[10px] font-semibold uppercase tracking-wide text-[var(--ink3)]"},"Tổng",-1)),e("div",he,a(l.value.total),1)])])])])]),l.value.detailedAnswers.length?(d(),c("div",we,[t[15]||(t[15]=e("div",{class:"border-b border-[var(--border)] px-5 py-3.5"},[e("span",{class:"text-[14px] font-bold text-[var(--ink)]"},"Answer Key")],-1)),(d(!0),c(j,null,O(k.value,(o,r)=>(d(),c("div",{key:r,class:"border-b border-[var(--border)] last:border-0 px-5 py-4"},[e("div",me,[e("span",ge,a(o.label),1),e("span",{class:"rounded-full px-2.5 py-0.5 text-[11px] font-semibold",style:C(o.correct===o.total?"background:#d1fae5;color:#065f46":"background:#ffe4e6;color:#be123c")},a(o.correct)+"/"+a(o.total)+" đúng ",5)]),e("div",ye,[(d(!0),c(j,null,O(o.answers,s=>(d(),c("div",{key:s.questionId,class:"flex items-center gap-2 text-[12px]"},[e("div",{class:"flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] font-bold",style:C(s.isCorrect?"background:#d1fae5;color:#065f46":"background:#ffe4e6;color:#be123c")},a(s.order),5),e("span",ke,a(s.correctAnswer??"—"),1),t[14]||(t[14]=e("span",{class:"text-[var(--ink3)]"}," : ",-1)),e("span",{class:"font-mono font-semibold",style:C(s.isCorrect?"color:#059669":"color:#e11d48")},a(s.userAnswer??"—"),5),s.isCorrect?(d(),c("svg",_e,[...t[12]||(t[12]=[e("polyline",{points:"20 6 9 17 4 12"},null,-1)])])):(d(),c("svg",qe,[...t[13]||(t[13]=[e("circle",{cx:"12",cy:"12",r:"10"},null,-1),e("line",{x1:"15",y1:"9",x2:"9",y2:"15"},null,-1),e("line",{x1:"9",y1:"9",x2:"15",y2:"15"},null,-1)])]))]))),128))])]))),128))])):B("",!0),l.value.detailedAnswers.length?(d(),c("div",Ce,[e("div",Ae,[t[16]||(t[16]=e("span",{class:"text-[13px] font-bold text-[var(--ink)]"},"Review đáp án chi tiết",-1)),e("span",Se,a(l.value.score)+" đúng",1)]),e("div",Ie,[(d(!0),c(j,null,O(l.value.detailedAnswers,(o,r)=>(d(),c("div",{key:o.questionId||r,class:"ct-card overflow-hidden"},[e("div",Pe,[e("div",{class:"w-1 shrink-0 rounded-l-xl",style:C(o.isCorrect?"background:#34d399":"background:#f43f5e")},null,4),e("div",je,[e("div",Be,[e("span",Te,"Câu "+a(o.order||r+1),1),e("span",{class:"rounded-full px-2 py-0.5 text-[10px] font-bold uppercase",style:C(o.isCorrect?"background:#d1fae5;color:#065f46":"background:#ffe4e6;color:#be123c")},a(o.isCorrect?"✓ Đúng":"✗ Sai"),5)]),o.question?(d(),c("p",Ne,a(o.question),1)):B("",!0),e("div",Oe,[e("div",ze,[t[17]||(t[17]=e("span",{class:"text-[var(--ink3)]"},"Đáp án của tôi",-1)),e("span",{class:"font-semibold font-mono",style:C(o.isCorrect?"color:#059669":"color:#e11d48")},a(o.userAnswer??"—"),5)]),o.isCorrect?B("",!0):(d(),c("div",Me,[t[18]||(t[18]=e("span",{class:"text-[var(--ink3)]"},"Đáp án đúng",-1)),e("span",Le,a(o.correctAnswer??"—"),1)]))])])])]))),128))])])):(d(),c("div",Re," Chưa có thông tin chi tiết đáp án. ")),e("div",$e,[A(i,{to:"/dashboard",class:"ct-btn"},{default:_(()=>[...t[19]||(t[19]=[e("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("rect",{x:"3",y:"3",width:"7",height:"7"}),e("rect",{x:"14",y:"3",width:"7",height:"7"}),e("rect",{x:"14",y:"14",width:"7",height:"7"}),e("rect",{x:"3",y:"14",width:"7",height:"7"})],-1),q(" Dashboard ",-1)])]),_:1}),A(i,{to:`/${String(l.value.subject).toLowerCase()}`,class:"ct-btn",onClick:t[0]||(t[0]=o=>{var r,s;return(s=(r=F(x)).clearResult)==null?void 0:s.call(r)})},{default:_(()=>[...t[20]||(t[20]=[e("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("polyline",{points:"1 4 1 10 7 10"}),e("path",{d:"M3.51 15a9 9 0 1 0 .49-3.5"})],-1),q(" Thử lại ",-1)])]),_:1},8,["to"]),S.value?(d(),U(i,{key:0,to:S.value,class:"ct-btn",style:{"border-color":"#15803d",color:"#15803d",background:"#f0fdf4"}},{default:_(()=>[...t[21]||(t[21]=[e("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"}),e("circle",{cx:"12",cy:"12",r:"3"})],-1),q(" Xem lời giải ",-1)])]),_:1},8,["to"])):B("",!0),A(i,{to:"/history",class:"ct-btn"},{default:_(()=>[...t[22]||(t[22]=[e("svg",{class:"mr-1.5",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2"},[e("circle",{cx:"12",cy:"12",r:"10"}),e("polyline",{points:"12 6 12 12 16 14"})],-1),q(" Lịch sử ",-1)])]),_:1})])],64)):(d(),c("div",Y,[t[3]||(t[3]=e("div",{class:"mb-2 text-base font-semibold text-[var(--ink)]"},"Không tìm thấy kết quả",-1)),A(i,{to:"/dashboard",class:"ct-btn mt-4 inline-flex"},{default:_(()=>[...t[2]||(t[2]=[q("Về Dashboard",-1)])]),_:1})]))])])}}};export{Qe as default};
|