{"id":30966,"date":"2025-11-06T15:53:32","date_gmt":"2025-11-06T07:53:32","guid":{"rendered":"https:\/\/www.hkmu.edu.hk\/oetools\/?page_id=30966"},"modified":"2026-03-04T09:22:53","modified_gmt":"2026-03-04T01:22:53","slug":"agent-9-mandarin-flashcard-generator","status":"publish","type":"page","link":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/","title":{"rendered":"Agent 9 Mandarin Flashcard Generator"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"30966\" class=\"elementor elementor-30966\" data-elementor-settings=\"[]\">\n\t\t\t\t\t\t\t<div class=\"elementor-section-wrap\">\n\t\t\t\t\t\t\t<section class=\"has_eae_slider wavo-column-gap-default elementor-section elementor-top-section elementor-element elementor-element-5f831d4 elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"5f831d4\" data-element_type=\"section\" data-settings=\"{&quot;jet_parallax_layout_list&quot;:[]}\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"has_eae_slider elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-658cfd4\" data-id=\"658cfd4\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t\t\t<div class=\"elementor-element elementor-element-bdf07b4 elementor-widget elementor-widget-html\" data-id=\"bdf07b4\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n    <title>Agent 9 Putonghua Vocabulary Flashcards Generator<\/title>\r\n    <style>\r\n        \/* ================ GLOBAL STYLES ================ *\/\r\n        :root {--primary-color: #fa975e; --primary-light: #e8864a; --primary-dark: #d87a40; --primary-blue: #6495ED; --accent-color: #fa975e; --accent-green: #7AC143; --light-gray: #f9f5f3; --medium-gray: #e0e0e0; --dark-gray: #707070; --success-color: #7AC143; --danger-color: #f44336; --warning-color: #ff9800; --info-color: #fa975e; --primary-red: #e53e3e; --accent-orange: #ed8936; --dark-text: #333; --light-text: #666; --card-bg: rgba(255, 255, 255, 0.95); --bg-light: linear-gradient(135deg, #fa975e, #e8864a); --shadow: 0 15px 40px rgba(0, 0, 0, 0.2);\r\n        }\r\n        \r\n        * {    margin: 0;\r\n            padding: 0;\r\n            box-sizing: border-box;\r\n            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\r\n        }\r\n        \r\n        body {\r\n            background: var(--bg-light);\r\n            color: var(--dark-text);\r\n            min-height: 100vh;\r\n            padding: 20px;\r\n            line-height: 1.6;\r\n        }\r\n        \r\n        .container {\r\n            max-width: 1800px;\r\n            margin: 0 auto;\r\n            background: rgba(255, 255, 255, 0.95);\r\n            border-radius: 15px;\r\n            box-shadow: 0 15px 40px rgba(0, 0, 0, 0.2);\r\n            overflow: hidden;\r\n            position: relative;\r\n        }\r\n        \r\n        .header {\r\n            background: linear-gradient(45deg, #fa975e, #e8864a);\r\n            color: #ffffff;\r\n            padding: 25px;\r\n            text-align: center;\r\n            position: relative;\r\n            overflow: hidden;\r\n        }\r\n\r\n        .header::before {\r\n            content: \"\";\r\n            position: absolute;\r\n            top: -50%;\r\n            left: -50%;\r\n            width: 200%;\r\n            height: 200%;\r\n            background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%);\r\n            transform: rotate(30deg);\r\n        }\r\n\r\n        .header h1 {\r\n            font-size: 2.5rem;\r\n            font-weight: 700;\r\n            margin-bottom: 10px;\r\n            color: #ffffff;\r\n            position: relative;\r\n            text-shadow: 0 2px 4px rgba(0,0,0,0.2);\r\n        }\r\n\r\n        .header p {\r\n            font-size: 1.1rem;\r\n            font-weight: 400;\r\n            color: #ffffff;\r\n            opacity: 0.9;\r\n            max-width: 800px;\r\n            margin: 0 auto;\r\n            position: relative;\r\n        }\r\n        \r\n        .section {\r\n            padding: 30px;\r\n            border-bottom: 1px solid #eee;\r\n            position: relative;\r\n            display: flex;\r\n            flex-direction: column;\r\n        }\r\n        \r\n        .section h2 {\r\n            margin-bottom: 16px !important;\r\n            color: #fa975e !important;\r\n            font-size: 2.5rem !important;\r\n            font-weight: 600 !important;\r\n            display: flex !important;\r\n            align-items: center !important;\r\n            gap: 10px !important;\r\n        }\r\n                \r\n        .input-section h1 {\r\n            text-align: center;\r\n            color: var(--primary-color);\r\n            margin-bottom: 30px;\r\n            font-size: 2.5rem;\r\n            font-weight: 700;\r\n        }\r\n        \r\n        .input-area {\r\n            margin-bottom: 20px;\r\n        }\r\n        \r\n        textarea {\r\n            width: 100%;\r\n            height: 120px;\r\n            padding: 15px;\r\n            border: 2px solid var(--medium-gray);\r\n            border-radius: 8px;\r\n            font-size: 16px;\r\n            resize: none;\r\n            transition: all 0.3s;\r\n            margin-bottom: 15px;\r\n            font-family: inherit;\r\n            position: relative;\r\n        }\r\n        \r\n        textarea:focus {\r\n            outline: none;\r\n            border-color: var(--primary-color);\r\n            box-shadow: 0 0 0 3px rgba(250, 151, 94, 0.1);\r\n        }\r\n        \r\n        .textarea-container {\r\n            position: relative;\r\n            display: inline-block;\r\n            width: 100%;\r\n        }\r\n        \r\n        .drag-overlay {\r\n            position: absolute;\r\n            top: 0;\r\n            left: 0;\r\n            right: 0;\r\n            bottom: 0;\r\n            background: rgba(250, 151, 94, 0.1);\r\n            border: 2px dashed var(--primary-color);\r\n            border-radius: 8px;\r\n            display: none;\r\n            align-items: center;\r\n            justify-content: center;\r\n            z-index: 10;\r\n            pointer-events: none;\r\n        }\r\n        \r\n        .drag-overlay.active {\r\n            display: flex;\r\n        }\r\n        \r\n        .drag-text {\r\n            color: var(--primary-color);\r\n            font-size: 18px;\r\n            font-weight: 600;\r\n            text-align: center;\r\n        }\r\n        \r\n        \/* ================ BUTTON STYLES ================ *\/\r\n        button {\r\n            padding: 8px 28px !important;\r\n            border: none;\r\n            border-radius: 8px !important;\r\n            cursor: pointer !important;\r\n            font-size: 24px !important;\r\n            font-weight: 600 !important;\r\n            transition: all 0.3s ease;\r\n            display: flex;\r\n            align-items: center;\r\n            justify-content: center;\r\n            gap: 10px;\r\n            letter-spacing: 1px;\r\n            width: 100% !important;\r\n            margin-top: 20px !important;\r\n            box-shadow: 0 4px 8px rgba(250, 151, 94, 0.3) !important;\r\n        }\r\n        \r\n        .generate-btn {\r\n            background: var(--accent-green);\r\n            color: white;\r\n            width: 100%;\r\n            box-shadow: 0 4px 8px rgba(122, 193, 67, 0.3);\r\n        }\r\n        \r\n        .generate-btn:hover:not(:disabled) {\r\n            background-color: #5a8f32;\r\n            transform: translateY(-2px);\r\n            box-shadow: 0 6px 12px rgba(122, 193, 67, 0.4);\r\n        }\r\n        \r\n        .generate-btn:disabled {\r\n            background-color: var(--medium-gray);\r\n            cursor: not-allowed;\r\n            transform: none;\r\n            box-shadow: none;\r\n        }\r\n        \r\n        .progress-section {\r\n            background: var(--card-bg);\r\n            border-radius: 15px;\r\n            box-shadow: var(--shadow);\r\n            padding: 20px;\r\n            margin-bottom: 30px;\r\n            display: none;\r\n        }\r\n        \r\n        .progress-bar {\r\n            width: 100%;\r\n            height: 20px;\r\n            background: var(--medium-gray);\r\n            border-radius: 10px;\r\n            overflow: hidden;\r\n            margin-bottom: 15px;\r\n        }\r\n        \r\n        .progress-fill {\r\n            height: 100%;\r\n            background: var(--bg-light);\r\n            width: 0%;\r\n            transition: width 0.5s ease;\r\n            position: relative;\r\n        }\r\n        \r\n        .progress-fill::after {\r\n            content: '';\r\n            position: absolute;\r\n            top: 0;\r\n            left: 0;\r\n            right: 0;\r\n            bottom: 0;\r\n            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);\r\n            animation: shimmer 2s infinite;\r\n        }\r\n        \r\n        @keyframes shimmer {\r\n            0% { transform: translateX(-100%); }\r\n            100% { transform: translateX(100%); }\r\n        }\r\n        \r\n        .progress-text {\r\n            text-align: center;\r\n            font-weight: 600;\r\n            color: var(--light-text);\r\n        }\r\n        \r\n        .flashcards-section {\r\n            display: none;\r\n        }\r\n        \r\n        .flashcard-controls {\r\n            background: var(--card-bg);\r\n            border-radius: 15px;\r\n            box-shadow: var(--shadow);\r\n            padding: 20px;\r\n            margin-bottom: 20px;\r\n            text-align: center;\r\n        }\r\n        \r\n        .card-counter {\r\n            font-size: 18px;\r\n            font-weight: 600;\r\n            color: var(--primary-color);\r\n            margin-bottom: 15px;\r\n        }\r\n        \r\n        .control-buttons {\r\n            display: flex;\r\n            gap: 15px;\r\n            justify-content: center;\r\n            flex-wrap: wrap;\r\n        }\r\n        \r\n        .control-buttons .control-btn {\r\n            width: auto !important;\r\n            margin-top: 0 !important;\r\n        }\r\n        \r\n        .control-btn {\r\n            padding: 10px 20px;\r\n            min-width: 120px;\r\n            width: auto !important;\r\n            flex: 1;\r\n            max-width: 400px;\r\n        }\r\n        \r\n        .prev-btn {\r\n            background: var(--dark-gray);\r\n            color: white;\r\n            box-shadow: 0 4px 8px rgba(112, 112, 112, 0.3);\r\n        }\r\n        \r\n        .prev-btn:hover:not(:disabled) {\r\n            background-color: #5a5a5a;\r\n            transform: translateY(-2px);\r\n            box-shadow: 0 6px 12px rgba(112, 112, 112, 0.4);\r\n        }\r\n        \r\n        .next-btn {\r\n            background: var(--accent-green);\r\n            color: white;\r\n            box-shadow: 0 4px 8px rgba(122, 193, 67, 0.3);\r\n        }\r\n        \r\n        .next-btn:hover:not(:disabled) {\r\n            background-color: #5a8f32;\r\n            transform: translateY(-2px);\r\n            box-shadow: 0 6px 12px rgba(122, 193, 67, 0.4);\r\n        }\r\n        \r\n        .flip-btn {\r\n            background: var(--warning-color);\r\n            color: white;\r\n            box-shadow: 0 4px 8px rgba(255, 152, 0, 0.3);\r\n        }\r\n        \r\n        .flip-btn:hover:not(:disabled) {\r\n            background-color: #e68900;\r\n            transform: translateY(-2px);\r\n            box-shadow: 0 6px 12px rgba(255, 152, 0, 0.4);\r\n        }\r\n        \r\n        .control-btn:disabled {\r\n            background-color: var(--medium-gray);\r\n            cursor: not-allowed;\r\n            transform: none;\r\n            box-shadow: none;\r\n        }\r\n        \r\n        .flashcard-container {\r\n            perspective: 1000px;\r\n            margin: 0 auto;\r\n            max-width: 800px;\r\n        }\r\n        \r\n        .flashcard {\r\n            font-family: Microsoft Yahei;\r\n            background-color: #fdf6e3;\r\n            line-height: 200%;\r\n            text-align: left;\r\n            color: black;\r\n            max-width: 800px;\r\n            margin: 0 auto;\r\n            min-height: 600px;\r\n            max-height: 800px;\r\n            height: auto;\r\n            border-radius: 15px;\r\n            box-shadow: var(--shadow);\r\n            transition: transform 0.6s;\r\n            transform-style: preserve-3d;\r\n            position: relative;\r\n            cursor: pointer;\r\n        }\r\n        \r\n        .flashcard.flipped {\r\n            transform: rotateY(180deg);\r\n        }\r\n        \r\n        .card-face {\r\n            position: absolute;\r\n            width: 100%;\r\n            height: 100%;\r\n            min-height: 600px;\r\n            max-height: 800px;\r\n            backface-visibility: hidden;\r\n            border-radius: 15px;\r\n            padding: 10px 30px;\r\n            display: flex;\r\n            flex-direction: column;\r\n            justify-content: flex-start;\r\n            overflow-y: auto;\r\n            box-sizing: border-box;\r\n        }\r\n        \r\n        .card-front {\r\n            background: linear-gradient(135deg, #fdf6e3, #f9f2e7);\r\n        }\r\n        \r\n        .card-back {\r\n            background: linear-gradient(135deg, #f0f8f0, #e8f5e8);\r\n            transform: rotateY(180deg);\r\n        }\r\n        \r\n        .word-display {\r\n            text-align: center;\r\n            padding: 15px 0;\r\n            flex: 1;\r\n            display: flex;\r\n            flex-direction: column;\r\n            justify-content: center;\r\n        }\r\n        \r\n        .word-text {\r\n            font-size: 64px;\r\n            font-weight: 700;\r\n            color: #2c3e50;\r\n            margin-bottom: 30px;\r\n            display: flex;\r\n            align-items: center;\r\n            justify-content: center;\r\n            font-family: 'Microsoft YaHei', 'SimSun', 'SimHei', 'PingFang SC', 'Noto Sans SC', sans-serif;\r\n        }\r\n        \r\n        .pronunciation-container {\r\n            display: flex;\r\n            align-items: center;\r\n            justify-content: center;\r\n            margin-bottom: 30px;\r\n        }\r\n        \r\n        .pronunciation-icon {\r\n            font-size: 32px;\r\n            cursor: pointer;\r\n            transition: all 0.3s ease;\r\n            display: inline-flex;\r\n            align-items: center;\r\n            justify-content: center;\r\n            width: 45px;\r\n            height: 45px;\r\n            border-radius: 50%;\r\n            background: rgba(250, 151, 94, 0.1);\r\n            color: var(--primary-color);\r\n        }\r\n        \r\n        .pronunciation-icon:hover {\r\n            background: var(--primary-color);\r\n            color: white;\r\n            transform: scale(1.1);\r\n        }\r\n        \r\n        .pronunciation-icon:active {\r\n            transform: scale(0.95);\r\n        }\r\n        \r\n        .word-pinyin {\r\n            font-family: 'Lucida Sans Unicode', Arial;\r\n            font-size: 32px;\r\n            color: #7f8c8d;\r\n            margin-bottom: 10px;\r\n        }\r\n        \r\n        \/* Pronunciation icon color adjustments for dark themes *\/\r\n        .card-front[style*=\"background: #222222\"] .pronunciation-icon,\r\n        .card-front[style*=\"background: #416D19\"] .pronunciation-icon {\r\n            background: rgba(255, 255, 255, 0.2);\r\n            color: #fdf6e3;\r\n        }\r\n        \r\n        .card-front[style*=\"background: #222222\"] .pronunciation-icon:hover,\r\n        .card-front[style*=\"background: #416D19\"] .pronunciation-icon:hover {\r\n            background: rgba(255, 255, 255, 0.3);\r\n            color: #ffffff;\r\n        }\r\n        \r\n        .answer-divider {\r\n            height: 3px;\r\n            color: #073642;\r\n            background-color: #073642;\r\n            border-width: 0px;\r\n            width: 80%;\r\n            margin: 25px auto;\r\n            border-radius: 2px;\r\n        }\r\n        \r\n        .back-content {\r\n            margin: 10px 0;\r\n            padding: 10px 0;\r\n            flex: 1;\r\n            display: flex;\r\n            flex-direction: column;\r\n            justify-content: center;\r\n        }\r\n        \r\n        .field-group {\r\n            margin-bottom: 20px;\r\n        }\r\n        \r\n        .field-label {\r\n            font-weight: 600;\r\n            font-size: 18px;\r\n            color: var(--primary-color);\r\n            margin-bottom: 8px;\r\n        }\r\n        \r\n        .traditional-meaning {\r\n            font-size: 32px;\r\n            color: #2c3e50;\r\n            font-weight: 500;\r\n        }\r\n        \r\n        .english-meaning {\r\n            color: #e74c3c;\r\n            font-size: 24px;\r\n            line-height: 1.6;\r\n        }\r\n        \r\n        .example {\r\n            color: #27ae60;\r\n            font-size: 20px;\r\n            line-height: 1.6;\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 10px;\r\n            font-family: 'Microsoft YaHei', 'SimSun', 'SimHei', 'PingFang SC', 'Noto Sans SC', sans-serif;\r\n        }\r\n\r\n        .example-text {\r\n            flex: 1;\r\n        }\r\n        \r\n        .example-audio-icon {\r\n            font-size: 24px;\r\n            cursor: pointer;\r\n            transition: all 0.3s ease;\r\n            display: inline-flex;\r\n            align-items: center;\r\n            justify-content: center;\r\n            width: 35px;\r\n            height: 35px;\r\n            border-radius: 50%;\r\n            background: rgba(39, 174, 96, 0.1);\r\n            color: #27ae60;\r\n            flex-shrink: 0;\r\n        }\r\n        \r\n        .example-audio-icon:hover {\r\n            background: #27ae60;\r\n            color: white;\r\n            transform: scale(1.1);\r\n        }\r\n        \r\n        .example-audio-icon:active {\r\n            transform: scale(0.95);\r\n        }\r\n        \r\n        .error-message {\r\n            background: #f8d7da;\r\n            color: #721c24;\r\n            padding: 15px;\r\n            border-radius: 8px;\r\n            margin: 20px 0;\r\n            text-align: center;\r\n            font-weight: 600;\r\n        }\r\n        \r\n        .loading-spinner {\r\n            display: inline-block;\r\n            width: 20px;\r\n            height: 20px;\r\n            border: 3px solid rgba(255,255,255,.3);\r\n            border-radius: 50%;\r\n            border-top-color: #fff;\r\n            animation: spin 1s ease-in-out infinite;\r\n        }\r\n        \r\n        @keyframes spin {\r\n            to { transform: rotate(360deg); }\r\n        }\r\n        \r\n        \/* ================ SETTINGS PANEL STYLES ================ *\/\r\n        .settings-panel {\r\n            \/* Inherits from .section class *\/\r\n            background: var(--card-bg);\r\n            border-radius: 15px;\r\n            box-shadow: var(--shadow);\r\n            margin-top: 20px;\r\n        }\r\n        \r\n        .settings-header {\r\n            color: var(--primary-color);\r\n            font-size: 1.3rem;\r\n            font-weight: 600;\r\n            margin-bottom: 20px;\r\n            text-align: center;\r\n        }\r\n        \r\n        .settings-section {\r\n            margin-bottom: 25px;\r\n        }\r\n        \r\n        .settings-section h4 {\r\n            color: var(--dark-text);\r\n            font-size: 1.1rem;\r\n            font-weight: 600;\r\n            margin-bottom: 15px;\r\n            border-bottom: 2px solid var(--medium-gray);\r\n            padding-bottom: 8px;\r\n        }\r\n        \r\n        .checkbox-group {\r\n            display: grid;\r\n            grid-template-columns: 1fr 1fr;\r\n            gap: 10px;\r\n            margin-bottom: 15px;\r\n        }\r\n        \r\n        .checkbox-item {\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 8px;\r\n        }\r\n        \r\n        .checkbox-item input[type=\"checkbox\"] {\r\n            width: 16px;\r\n            height: 16px;\r\n            accent-color: var(--primary-color);\r\n        }\r\n        \r\n        .checkbox-item label {\r\n            font-size: 14px;\r\n            color: var(--dark-text);\r\n            cursor: pointer;\r\n        }\r\n        \r\n        .color-options {\r\n            display: grid;\r\n            grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));\r\n            gap: 15px;\r\n        }\r\n        \r\n        .color-option {\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 10px;\r\n            padding: 10px;\r\n            border: 2px solid transparent;\r\n            border-radius: 8px;\r\n            cursor: pointer;\r\n            transition: all 0.3s ease;\r\n        }\r\n        \r\n        .color-option:hover {\r\n            border-color: var(--primary-color);\r\n            background: rgba(250, 151, 94, 0.1);\r\n        }\r\n        \r\n        .color-option.selected {\r\n            border-color: var(--primary-color);\r\n            background: rgba(250, 151, 94, 0.2);\r\n        }\r\n        \r\n        .color-preview {\r\n            width: 24px;\r\n            height: 24px;\r\n            border-radius: 50%;\r\n            border: 2px solid #fff;\r\n            box-shadow: 0 2px 4px rgba(0,0,0,0.2);\r\n        }\r\n        \r\n        .color-name {\r\n            font-size: 14px;\r\n            color: var(--dark-text);\r\n            font-weight: 500;\r\n        }\r\n        \r\n        @media (max-width: 768px) {\r\n            .input-section h1 {\r\n                font-size: 2rem;\r\n            }\r\n            \r\n            .word-text {\r\n                font-size: 48px;\r\n            }\r\n            \r\n            .word-pinyin {\r\n                font-size: 26px;\r\n            }\r\n            \r\n            .traditional-meaning {\r\n                font-size: 26px;\r\n            }\r\n            \r\n            .control-buttons {\r\n                flex-direction: column;\r\n            }\r\n            \r\n            .control-btn {\r\n                width: 100%;\r\n            }\r\n        }\r\n    <\/style>\r\n<\/head>\r\n<body>\r\n    <div class=\"container\">\r\n        <!-- Header Section -->\r\n        <div class=\"header\">\r\n            <h1>Mandarin Flashcard Generator<\/h1>\r\n            <p>Transform Chinese words and phrases into comprehensive study cards featuring PinYin, traditional Chinese, English translations, and example sentences with audio for effective Mandarin learning.<\/p>\r\n        <\/div>\r\n        \r\n        <!-- Input Section -->\r\n        <div class=\"input-section section\">\r\n            <!-- Step 1 Heading -->\r\n            <div class=\"step-heading\">\r\n                <h2 style=\"color: var(--primary-color); font-size: 1.5rem; margin-bottom: 20px; text-align: left; font-weight: 600;\">\r\n                    Step 1: Input the Chinese Words or Phrases You Would Like to Learn\r\n                <\/h2>\r\n                <p style=\"text-align: left; color: var(--light-text); margin-bottom: 25px; font-size: 16px; font-weight:400;\">\r\n                    Enter Chinese words or phrases below. Or <strong>drag-and-drop<\/strong> a CSV file to import existing vocabulary data.\r\n                <\/p>\r\n            <\/div>\r\n            \r\n            <div class=\"input-area\">\r\n                <div class=\"textarea-container\">\r\n                    <textarea id=\"vocabularyInput\" placeholder=\"Enter Chinese words or phrases in any format (e.g., \u6211 \u4f60\u597d \u5b66\u751f \u4e2d\u6587 \u6c49\u8bed)\"><\/textarea>\r\n                    <div id=\"dragOverlay\" class=\"drag-overlay\">\r\n                        <div class=\"drag-text\">\r\n                            Drop CSV file here to import vocabulary\r\n                        <\/div>\r\n                    <\/div>\r\n                <\/div>\r\n                <button id=\"generateBtn\" class=\"generate-btn\">\r\n                    <span>Generate Flash Cards<\/span>\r\n                <\/button>\r\n            <\/div>\r\n        <\/div>\r\n\r\n        <!-- Progress Section -->\r\n        <div id=\"progressSection\" class=\"progress-section\">\r\n            <div class=\"progress-bar\">\r\n                <div id=\"progressFill\" class=\"progress-fill\"><\/div>\r\n            <\/div>\r\n            <div id=\"progressText\" class=\"progress-text\">Processing words...<\/div>\r\n        <\/div>\r\n\r\n        <!-- Flashcards Section -->\r\n        <div id=\"flashcardsSection\" class=\"flashcards-section section\">\r\n            <!-- Step 2 Heading -->\r\n            <div class=\"step-heading\" style=\"background: var(--card-bg); border-radius: 15px; box-shadow: var(--shadow); padding: 20px; margin-bottom: 20px; text-align: left;\">\r\n                <h2 style=\"color: var(--primary-color); font-size: 1.5rem; margin-bottom: 15px; font-weight: 600;\">\r\n                    Step 2: Learn with the Flash Cards\r\n                <\/h2>\r\n                <p style=\"color: var(--light-text); margin-bottom: 0; font-size: 16px;\">\r\n                    Click on cards to flip them, use navigation buttons, or keyboard arrows to browse your vocabulary!\r\n                <\/p>\r\n            <\/div>\r\n            \r\n            <div class=\"flashcard-container\">\r\n                <div id=\"flashcard\" class=\"flashcard\">\r\n                    <div class=\"card-face card-front\">\r\n                        <div class=\"word-display\">\r\n                            <div id=\"pronunciationContainer\" class=\"pronunciation-container\">\r\n                                <span id=\"pronunciationIcon\" class=\"pronunciation-icon\" title=\"Click to hear pronunciation\">\ud83d\udd0a<\/span>\r\n                            <\/div>\r\n                            <div id=\"wordText\" class=\"word-text\">\r\n                                <span id=\"wordTextContent\">Loading...<\/span>\r\n                            <\/div>\r\n                            <div id=\"wordPinyin\" class=\"word-pinyin\"><\/div>\r\n                        <\/div>\r\n                    <\/div>\r\n                    <div class=\"card-face card-back\">\r\n                        <div class=\"back-content\">\r\n                            <div class=\"field-group\">\r\n                                <div class=\"field-label\">Traditional Chinese:<\/div>\r\n                                <div id=\"traditionalMeaning\" class=\"traditional-meaning\"><\/div>\r\n                            <\/div>\r\n                            <hr class=\"answer-divider\">\r\n                            <div class=\"field-group\">\r\n                                <div class=\"field-label\">English Meaning:<\/div>\r\n                                <div id=\"englishMeaning\" class=\"english-meaning\"><\/div>\r\n                            <\/div>\r\n                            <div class=\"field-group\">\r\n                                <div class=\"field-label\">Example Sentence:<\/div>\r\n                                <div id=\"example\" class=\"example\">\r\n                                    <span id=\"exampleText\" class=\"example-text\"><\/span>\r\n                                    <span id=\"exampleAudioIcon\" class=\"example-audio-icon\" title=\"Click to hear example\">\ud83d\udd0a<\/span>\r\n                                <\/div>\r\n                            <\/div>\r\n                        <\/div>\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n            \r\n            <div class=\"flashcard-controls\">\r\n                <div id=\"cardCounter\" class=\"card-counter\">Card 1 of 1<\/div>\r\n                <p style=\"color: var(--light-text); margin: 10px 0; font-size: 14px; text-align: center;\">\r\n                    <strong>\u2191 \u2193<\/strong> - Flip card  |  <strong>\u2190 \u2192<\/strong> - Navigate cards  |  <strong>Click<\/strong> - Flip card\r\n                <\/p>\r\n                <div class=\"control-buttons\">\r\n                    <button id=\"prevBtn\" class=\"control-btn prev-btn\">\u2190 Previous<\/button>\r\n                    <button id=\"flipBtn\" class=\"control-btn flip-btn\">Flip Card<\/button>\r\n                    <button id=\"nextBtn\" class=\"control-btn next-btn\">Next \u2192<\/button>\r\n                <\/div>\r\n            <\/div>\r\n            \r\n            <!-- Settings Panel -->\r\n            <div id=\"settingsPanel\" class=\"settings-panel section\" style=\"display: none;\">\r\n                <h2 class=\"settings-header\">Customize Your Flashcards<\/h2>\r\n                \r\n                <!-- Card Content Settings -->\r\n                <div class=\"settings-section\">\r\n                    <h4>Card Content Display<\/h4>\r\n                    \r\n                    <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 30px;\">\r\n                        <!-- Front Side -->\r\n                        <div>\r\n                            <h5 style=\"color: var(--primary-color); font-size: 1rem; margin-bottom: 15px;\">Front Side:<\/h5>\r\n                            <div class=\"checkbox-group\" style=\"grid-template-columns: 1fr;\">\r\n                                <div class=\"checkbox-item\">\r\n                                    <input type=\"checkbox\" id=\"frontPronunciation\" checked>\r\n                                    <label for=\"frontPronunciation\">Pronunciation Audio<\/label>\r\n                                <\/div>\r\n                                <div class=\"checkbox-item\">\r\n                                    <input type=\"checkbox\" id=\"frontWord\" checked>\r\n                                    <label for=\"frontWord\">Word<\/label>\r\n                                <\/div>\r\n                                <div class=\"checkbox-item\">\r\n                                    <input type=\"checkbox\" id=\"frontPinyin\" checked>\r\n                                    <label for=\"frontPinyin\">PinYin<\/label>\r\n                                <\/div>\r\n                            <\/div>\r\n                        <\/div>\r\n                        \r\n                        <!-- Back Side -->\r\n                        <div>\r\n                            <h5 style=\"color: var(--primary-color); font-size: 1rem; margin-bottom: 15px;\">Back Side:<\/h5>\r\n                            <div class=\"checkbox-group\" style=\"grid-template-columns: 1fr;\">\r\n                                <div class=\"checkbox-item\">\r\n                                    <input type=\"checkbox\" id=\"backTraditional\" checked>\r\n                                    <label for=\"backTraditional\">Traditional Chinese<\/label>\r\n                                <\/div>\r\n                                <div class=\"checkbox-item\">\r\n                                    <input type=\"checkbox\" id=\"backEnglish\" checked>\r\n                                    <label for=\"backEnglish\">English Meaning<\/label>\r\n                                <\/div>\r\n                                <div class=\"checkbox-item\">\r\n                                    <input type=\"checkbox\" id=\"backExample\" checked>\r\n                                    <label for=\"backExample\">Example Sentence<\/label>\r\n                                <\/div>\r\n                            <\/div>\r\n                        <\/div>\r\n                    <\/div>\r\n                <\/div>\r\n                \r\n                <!-- Card Color Settings -->\r\n                <div class=\"settings-section\">\r\n                    <h4>Card Color Theme<\/h4>\r\n                    <div class=\"color-options\">\r\n                        <div class=\"color-option selected\" data-color=\"default\">\r\n                            <div class=\"color-preview\" style=\"background: linear-gradient(135deg, #fdf6e3, #f0f8f0);\"><\/div>\r\n                            <span class=\"color-name\">Default<\/span>\r\n                        <\/div>\r\n                        <div class=\"color-option\" data-color=\"#222222\">\r\n                            <div class=\"color-preview\" style=\"background: #222222;\"><\/div>\r\n                            <span class=\"color-name\">Dark Charcoal<\/span>\r\n                        <\/div>\r\n                        <div class=\"color-option\" data-color=\"#416D19\">\r\n                            <div class=\"color-preview\" style=\"background: #416D19;\"><\/div>\r\n                            <span class=\"color-name\">Forest Green<\/span>\r\n                        <\/div>\r\n                        <div class=\"color-option\" data-color=\"#E5C287\">\r\n                            <div class=\"color-preview\" style=\"background: #E5C287;\"><\/div>\r\n                            <span class=\"color-name\">Warm Orange<\/span>\r\n                        <\/div>\r\n                        <div class=\"color-option\" data-color=\"#d1ebed\">\r\n                            <div class=\"color-preview\" style=\"background: #d1ebed;\"><\/div>\r\n                            <span class=\"color-name\">Relaxing Blue<\/span>\r\n                        <\/div>\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n            \r\n            <!-- Step 3: Export Section -->\r\n            <div id=\"exportSection\" class=\"settings-panel section\" style=\"display: none;\">\r\n                <h2 class=\"settings-header\">Step 3: Export Your Vocabulary<\/h2>\r\n                <p style=\"text-align: left; color: var(--light-text); margin-bottom: 25px; font-size: 16px;\">\r\n                    Download your vocabulary collection as a CSV file for offline study or backup\r\n                <\/p>\r\n                \r\n                <div style=\"text-align: center;\">\r\n                    <button id=\"exportBtn\" class=\"generate-btn\" style=\"width: auto; padding: 15px 40px; background: var(--info-color); margin: 0 auto;\">\r\n                        <span>Export to CSV<\/span>\r\n                    <\/button>\r\n                <\/div>\r\n                \r\n                <div style=\"margin-top: 20px; padding: 15px; background: rgba(250, 151, 94, 0.1); border-radius: 8px; font-size: 14px; color: var(--light-text);\">\r\n                    <strong>CSV Format:<\/strong> Word, PinYin, Traditional Chinese Meaning, English Meaning, Example Sentence\r\n                <\/div>\r\n            <\/div>\r\n        <\/div>\r\n\r\n        <div id=\"errorMessage\" class=\"error-message\" style=\"display: none;\"><\/div>\r\n    <\/div>\r\n\r\n    <script>\r\n         \/\/ =====================================\r\n         \/\/ BACKEND AUTHORISATION SYSTEM\r\n         \/\/ =====================================\r\n         function modInverse(k1, mod = 256) {\r\n            let t = 0, newT = 1, r = mod, newR = k1;\r\n            while (newR !== 0) {\r\n                const quotient = Math.floor(r \/ newR);\r\n                [t, newT] = [newT, t - quotient * newT];\r\n                [r, newR] = [newR, r - quotient * newR];\r\n            }\r\n            if (r > 1) throw new Error('k1 is not invertible');\r\n            if (t < 0) t += mod;\r\n            return t;\r\n        }\r\n\r\n        function decryptServerPayload(payload) {\r\n            if (payload.length < 12) throw new Error('Invalid payload: too short');\r\n            const suffix = payload.slice(-12), b64 = payload.slice(0, -12);\r\n            const numericKey = BigInt(suffix.slice(0, 10));\r\n            const k1 = Number((numericKey % 127n) * 2n + 1n), k2 = Number(numericKey % 256n);\r\n            const invK1 = modInverse(k1, 256);\r\n            const binaryString = atob(b64);\r\n            const bytes = new Uint8Array(binaryString.length);\r\n            for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);\r\n            const decryptedBytes = new Uint8Array(bytes.length);\r\n            for (let i = 0; i < bytes.length; i++) {\r\n                const temp = (bytes[i] - k2 + 256) % 256;\r\n                decryptedBytes[i] = (invK1 * temp) % 256;\r\n            }\r\n            return new TextDecoder('utf-8').decode(decryptedBytes);\r\n        }\r\n\r\n        async function fetchDecryptedKey(service) {\r\n            const sanitizedBase = API_BASE_URL.endsWith('\/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;\r\n            const url = `${sanitizedBase}\/get_encrypted_key?service=${service}`;\r\n            const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application\/json' } });\r\n            if (!response.ok) throw new Error(`Failed to fetch ${service} key: ${response.status}`);\r\n            const data = await response.json();\r\n            if (!data.encrypted) throw new Error(`No encrypted key returned for ${service}`);\r\n            return decryptServerPayload(data.encrypted);\r\n        }\r\n\r\n        async function initializeApiKeys() {\r\n            const [cozeKey, minimaxKey] = await Promise.all([\r\n                fetchDecryptedKey('coze'),\r\n                fetchDecryptedKey('minimax')\r\n            ]);\r\n            return { cozeKey, minimaxKey };\r\n        }\r\n\r\n        function clearApiKeys() {\r\n            if (typeof COZE_API_KEY_RUNTIME !== 'undefined') COZE_API_KEY_RUNTIME = null;\r\n            if (typeof MINIMAX_API_KEY_RUNTIME !== 'undefined') MINIMAX_API_KEY_RUNTIME = null;\r\n        }\r\n\r\n        async function retryWithKeyRefresh(apiCallFn, maxRetries = 3) {\r\n            try {\r\n                return await apiCallFn();\r\n            } catch (error) {\r\n                const errorMsg = error && error.message ? error.message : '';\r\n                const is401or403 = errorMsg.includes('401') || errorMsg.includes('403');\r\n                \r\n                \/\/ Refresh key on auth errors and retry once immediately\r\n                \/\/ The outer retry mechanism (sendCozeRequestWithRetry) will handle additional retries\r\n                if (is401or403) {\r\n                    try {\r\n                        COZE_API_KEY_RUNTIME = await fetchDecryptedKey('coze');\r\n                        \/\/ Retry once with refreshed key\r\n                        return await apiCallFn();\r\n                    } catch (refreshError) {\r\n                        \/\/ Key refresh failed or retry with new key failed - throw to let outer retry handle it\r\n                        throw refreshError;\r\n                    }\r\n                } else {\r\n                    \/\/ Not an auth error - throw to let outer retry handle it\r\n                    throw error;\r\n                }\r\n            }\r\n        }\r\n\r\n        \/\/ =====================================\r\n        \/\/ API Configuration\r\n        \/\/ =====================================\r\n        let COZE_API_KEY_RUNTIME = null;\r\n        let MINIMAX_API_KEY_RUNTIME = null;\r\n\r\n        Object.defineProperty(window, 'COZE_API_KEY', {\r\n            get() { return COZE_API_KEY_RUNTIME; },\r\n            configurable: false\r\n        });\r\n\r\n        Object.defineProperty(window, 'MINIMAX_API_KEY', {\r\n            get() { return MINIMAX_API_KEY_RUNTIME; },\r\n            configurable: false\r\n        });\r\n\r\n        const API_BASE_URL = (() => {\r\n            try {\r\n                const { protocol, hostname } = window.location;\r\n                if (protocol === 'https:' && (hostname === 'oetools.net' || hostname.endsWith('.oetools.net'))) {\r\n                    return '\/api';\r\n                }\r\n            } catch (error) {}\r\n            return 'https:\/\/oetools.net\/api';\r\n        })();\r\n\r\n        const COZE_API_URL = 'https:\/\/api.coze.com\/open_api\/v2\/chat';\r\n        const COZE_BOT_ID = '7569501405833281544';\r\n        const USER_ID = 'flashcard_user_' + Date.now();\r\n        const BATCH_SIZE = 8;\r\n\r\n        \/\/ Minimax API Configuration for Pronunciation\r\n        const MINIMAX_API_URL = 'https:\/\/oetools.net\/minimax\/tts';\r\n        const MINIMAX_GROUP_ID = '1892726332706529707';\r\n\r\n        \/\/ =====================================\r\n        \/\/ MINIMAX PRONUNCIATION FUNCTIONS\r\n        \/\/ =====================================\r\n        \r\n        \/**\r\n         * Convert hex string to byte array\r\n         *\/\r\n        function hexStringToBytes(hexString) {\r\n            const bytes = new Uint8Array(hexString.length \/ 2);\r\n            for (let i = 0; i < hexString.length; i += 2) {\r\n                bytes[i \/ 2] = parseInt(hexString.substr(i, 2), 16);\r\n            }\r\n            return bytes;\r\n        }\r\n\r\n        \/**\r\n         * Create audio blob from byte array\r\n         *\/\r\n        function createAudioBlob(audioData) {\r\n            return new Blob([audioData], { type: 'audio\/mpeg' });\r\n        }\r\n\r\n        \/**\r\n         * Process Minimax streaming response to extract audio data\r\n         *\/\r\n        async function processMinimaxResponse(response) {\r\n            const reader = response.body.getReader();\r\n            const decoder = new TextDecoder();\r\n            let buffer = '';\r\n            let finalAudioHex = null;\r\n            let isComplete = false;\r\n            \r\n            try {\r\n                while (!isComplete) {\r\n                    const { done, value } = await reader.read();\r\n                    if (done) break;\r\n                    \r\n                    buffer += decoder.decode(value, { stream: true });\r\n                    let startPos = 0;\r\n                    \r\n                    while (startPos < buffer.length) {\r\n                        const openBrace = buffer.indexOf('{', startPos);\r\n                        if (openBrace === -1) break;\r\n                        \r\n                        let braceCount = 0;\r\n                        let endPos = openBrace;\r\n                        \r\n                        for (let i = openBrace; i < buffer.length; i++) {\r\n                            if (buffer[i] === '{') braceCount++;\r\n                            else if (buffer[i] === '}') {\r\n                                braceCount--;\r\n                                if (braceCount === 0) {\r\n                                    endPos = i;\r\n                                    break;\r\n                                }\r\n                            }\r\n                        }\r\n                        \r\n                        if (braceCount === 0 && endPos > openBrace) {\r\n                            const jsonStr = buffer.substring(openBrace, endPos + 1);\r\n                            try {\r\n                                const jsonData = JSON.parse(jsonStr);\r\n                                if (jsonData.data && jsonData.data.audio && jsonData.data.status === 2) {\r\n                                    finalAudioHex = jsonData.data.audio;\r\n                                    isComplete = true;\r\n                                    break;\r\n                                }\r\n                            } catch (parseError) {\r\n                                console.warn('Failed to parse JSON chunk:', parseError);\r\n                            }\r\n                            startPos = endPos + 1;\r\n                        } else {\r\n                            break;\r\n                        }\r\n                    }\r\n                    \r\n                    if (startPos > 0) {\r\n                        buffer = buffer.substring(startPos);\r\n                    }\r\n                }\r\n            } finally {\r\n                reader.releaseLock();\r\n            }\r\n            \r\n            if (!finalAudioHex) {\r\n                throw new Error('No final audio data received');\r\n            }\r\n            \r\n            const audioData = hexStringToBytes(finalAudioHex);\r\n            const audioBlob = createAudioBlob(audioData);\r\n            \r\n            return audioBlob;\r\n        }\r\n\r\n        \/**\r\n         * Generate pronunciation audio for Chinese text using Minimax API\r\n         *\/\r\n        async function generateChineseAudio(text) {\r\n            \/\/ Ensure Minimax key is available (should be initialized at page load)\r\n            if (!MINIMAX_API_KEY) {\r\n                throw new Error('Minimax API key not initialized. Please refresh the page.');\r\n            }\r\n            \r\n            const url = MINIMAX_API_URL;\r\n            const requestBody = {\r\n                model: \"speech-2.6-turbo\",\r\n                text: text,\r\n                stream: false,\r\n                output_format: \"hex\",\r\n                language_boost: \"Chinese\",\r\n                voice_setting: {\r\n                    voice_id: \"Chinese (Mandarin)_News_Anchor\",\r\n                    speed: 1.0,\r\n                    vol: 1.2,\r\n                    pitch: 0,\r\n                    emotion: null\r\n                },\r\n                audio_setting: {\r\n                    sample_rate: 32000,\r\n                    bitrate: 128000,\r\n                    format: \"mp3\",\r\n                    channel: 1\r\n                }\r\n            };\r\n            \r\n            const headers = {\r\n                'Authorization': `Bearer ${MINIMAX_API_KEY}`,\r\n                'Content-Type': 'application\/json',\r\n                'X-Origin-PW': 'origin',\r\n                'X-Minimax-Group': MINIMAX_GROUP_ID\r\n            };\r\n            \r\n            const response = await fetch(url, {\r\n                method: 'POST',\r\n                headers: headers,\r\n                body: JSON.stringify(requestBody)\r\n            });\r\n            \r\n            if (!response.ok) {\r\n                const errorText = await response.text();\r\n                \/\/ Try to refresh Minimax key on 401\/403 errors\r\n                if (response.status === 401 || response.status === 403) {\r\n                    try {\r\n                        MINIMAX_API_KEY_RUNTIME = await fetchDecryptedKey('minimax');\r\n                        \/\/ Retry once with new key\r\n                        const retryResponse = await fetch(url, {\r\n                            method: 'POST',\r\n                            headers: {\r\n                                'Authorization': `Bearer ${MINIMAX_API_KEY}`,\r\n                                'Content-Type': 'application\/json',\r\n                                'X-Origin-PW': 'origin',\r\n                                'X-Minimax-Group': MINIMAX_GROUP_ID\r\n                            },\r\n                            body: JSON.stringify(requestBody)\r\n                        });\r\n                        if (!retryResponse.ok) {\r\n                            const retryErrorText = await retryResponse.text();\r\n                            throw new Error(`HTTP ${retryResponse.status}: ${retryErrorText}`);\r\n                        }\r\n                        \/\/ Use retryResponse for processing\r\n                        return await processMinimaxResponse(retryResponse);\r\n                    } catch (refreshError) {\r\n                        throw new Error(`HTTP ${response.status}: ${errorText} (Key refresh failed: ${refreshError.message})`);\r\n                    }\r\n                }\r\n                throw new Error(`HTTP ${response.status}: ${errorText}`);\r\n            }\r\n            \r\n            return await processMinimaxResponse(response);\r\n        }\r\n\r\n        \/**\r\n         * Play pronunciation for the current word\r\n         *\/\r\n        async function playPronunciation(word, iconElement) {\r\n            if (!word) return;\r\n            \r\n            const cardIndex = currentCardIndex;\r\n            const originalHTML = iconElement.innerHTML;\r\n            \r\n            try {\r\n                \/\/ Check if pronunciation is already cached for this card\r\n                if (!pronunciationCache[cardIndex]) {\r\n                    \/\/ First time clicking - generate pronunciation\r\n                    iconElement.innerHTML = '\u23f3';\r\n                    iconElement.style.pointerEvents = 'none';\r\n                    \r\n                    const audioBlob = await generateChineseAudio(word);\r\n                    \r\n                    \/\/ Cache the pronunciation\r\n                    pronunciationCache[cardIndex] = {\r\n                        audioBlob: audioBlob,\r\n                        audioUrl: null\r\n                    };\r\n                }\r\n                \r\n                \/\/ Get the cached data\r\n                const cache = pronunciationCache[cardIndex];\r\n                \r\n                \/\/ Create audio from the cached blob\r\n                const audioUrl = URL.createObjectURL(cache.audioBlob);\r\n                cache.audioUrl = audioUrl; \/\/ Store for cleanup\r\n                \r\n                const audio = new Audio(audioUrl);\r\n                \r\n                iconElement.innerHTML = '\ud83d\udd0a';\r\n                iconElement.style.pointerEvents = 'none';\r\n                \r\n                audio.onended = () => {\r\n                    \/\/ Revoke the URL after playback (blob is still cached)\r\n                    URL.revokeObjectURL(audioUrl);\r\n                    cache.audioUrl = null;\r\n                    \r\n                    iconElement.innerHTML = originalHTML;\r\n                    iconElement.style.pointerEvents = 'auto';\r\n                };\r\n                \r\n                audio.onerror = () => {\r\n                    \/\/ Revoke the URL on error\r\n                    URL.revokeObjectURL(audioUrl);\r\n                    cache.audioUrl = null;\r\n                    \r\n                    iconElement.innerHTML = originalHTML;\r\n                    iconElement.style.pointerEvents = 'auto';\r\n                    console.error('Audio playback error');\r\n                };\r\n                \r\n                await audio.play();\r\n                \r\n            } catch (error) {\r\n                console.error('Pronunciation error:', error);\r\n                iconElement.innerHTML = originalHTML;\r\n                iconElement.style.pointerEvents = 'auto';\r\n                const errorMsg = error && error.message ? error.message : (error ? String(error) : 'Unknown error');\r\n                showError(`Pronunciation failed: ${errorMsg}`);\r\n            }\r\n        }\r\n\r\n        \/**\r\n         * Play example sentence audio\r\n         *\/\r\n        async function playExampleAudio(sentence, iconElement) {\r\n            if (!sentence) return;\r\n            \r\n            const cardIndex = currentCardIndex;\r\n            const originalHTML = iconElement.innerHTML;\r\n            \r\n            try {\r\n                \/\/ Check if example audio is already cached for this card\r\n                if (!exampleAudioCache[cardIndex]) {\r\n                    \/\/ First time clicking - generate audio\r\n                    iconElement.innerHTML = '\u23f3';\r\n                    iconElement.style.pointerEvents = 'none';\r\n                    \r\n                    const audioBlob = await generateChineseAudio(sentence);\r\n                    \r\n                    \/\/ Cache the audio\r\n                    exampleAudioCache[cardIndex] = {\r\n                        audioBlob: audioBlob,\r\n                        audioUrl: null\r\n                    };\r\n                }\r\n                \r\n                \/\/ Get the cached data\r\n                const cache = exampleAudioCache[cardIndex];\r\n                \r\n                \/\/ Create audio from the cached blob\r\n                const audioUrl = URL.createObjectURL(cache.audioBlob);\r\n                cache.audioUrl = audioUrl; \/\/ Store for cleanup\r\n                \r\n                const audio = new Audio(audioUrl);\r\n                \r\n                iconElement.innerHTML = '\ud83d\udd0a';\r\n                iconElement.style.pointerEvents = 'none';\r\n                \r\n                audio.onended = () => {\r\n                    \/\/ Revoke the URL after playback (blob is still cached)\r\n                    URL.revokeObjectURL(audioUrl);\r\n                    cache.audioUrl = null;\r\n                    \r\n                    iconElement.innerHTML = originalHTML;\r\n                    iconElement.style.pointerEvents = 'auto';\r\n                };\r\n                \r\n                audio.onerror = () => {\r\n                    \/\/ Revoke the URL on error\r\n                    URL.revokeObjectURL(audioUrl);\r\n                    cache.audioUrl = null;\r\n                    \r\n                    iconElement.innerHTML = originalHTML;\r\n                    iconElement.style.pointerEvents = 'auto';\r\n                    console.error('Audio playback error');\r\n                };\r\n                \r\n                await audio.play();\r\n                \r\n            } catch (error) {\r\n                console.error('Example audio error:', error);\r\n                iconElement.innerHTML = originalHTML;\r\n                iconElement.style.pointerEvents = 'auto';\r\n                const errorMsg = error && error.message ? error.message : (error ? String(error) : 'Unknown error');\r\n                showError(`Example audio failed: ${errorMsg}`);\r\n            }\r\n        }\r\n\r\n        \/\/ DOM Elements\r\n        const vocabularyInput = document.getElementById('vocabularyInput');\r\n        const generateBtn = document.getElementById('generateBtn');\r\n        const progressSection = document.getElementById('progressSection');\r\n        const progressFill = document.getElementById('progressFill');\r\n        const progressText = document.getElementById('progressText');\r\n        const flashcardsSection = document.getElementById('flashcardsSection');\r\n        const cardCounter = document.getElementById('cardCounter');\r\n        const prevBtn = document.getElementById('prevBtn');\r\n        const nextBtn = document.getElementById('nextBtn');\r\n        const flipBtn = document.getElementById('flipBtn');\r\n        const flashcard = document.getElementById('flashcard');\r\n        const errorMessage = document.getElementById('errorMessage');\r\n        const settingsPanel = document.getElementById('settingsPanel');\r\n\r\n        \/\/ Card display elements\r\n        const wordText = document.getElementById('wordText');\r\n        const wordTextContent = document.getElementById('wordTextContent');\r\n        const pronunciationIcon = document.getElementById('pronunciationIcon');\r\n        const wordPinyin = document.getElementById('wordPinyin');\r\n        const traditionalMeaning = document.getElementById('traditionalMeaning');\r\n        const englishMeaning = document.getElementById('englishMeaning');\r\n        const exampleText = document.getElementById('exampleText');\r\n        const exampleAudioIcon = document.getElementById('exampleAudioIcon');\r\n        const dragOverlay = document.getElementById('dragOverlay');\r\n\r\n        \/\/ State\r\n        let flashcardData = [];\r\n        let currentCardIndex = 0;\r\n        let isFlipped = false;\r\n        \r\n        \/\/ Pronunciation audio cache: { cardIndex: { audioBlob: blob, audioUrl: url } }\r\n        let pronunciationCache = {};\r\n        \/\/ Example audio cache: { cardIndex: { audioBlob: blob, audioUrl: url } }\r\n        let exampleAudioCache = {};\r\n        \r\n        let cardSettings = {\r\n            front: {\r\n                pronunciation: true,\r\n                word: true,\r\n                pinyin: true\r\n            },\r\n            back: {\r\n                traditional: true,\r\n                english: true,\r\n                example: true\r\n            },\r\n            color: 'default'\r\n        };\r\n\r\n        \/**\r\n         * Parse CSV content\r\n         *\/\r\n        function parseCSV(csvText) {\r\n            const lines = csvText.trim().split('\\n');\r\n            if (lines.length < 2) {\r\n                throw new Error('CSV file must contain at least a header row and one data row');\r\n            }\r\n            \r\n            \/\/ Parse header\r\n            const headers = lines[0].split(',').map(h => h.trim().replace(\/\"\/g, ''));\r\n            \r\n            \/\/ Expected headers for complete flashcard data\r\n            const expectedHeaders = [\r\n                'Word', 'PinYin', 'Traditional', 'English', 'Example'\r\n            ];\r\n            \r\n            \/\/ Check if CSV has all required fields\r\n            const hasAllFields = expectedHeaders.every(header => \r\n                headers.some(h => h.toLowerCase().includes(header.toLowerCase()))\r\n            );\r\n            \r\n            const data = [];\r\n            \r\n            \/\/ Parse data rows\r\n            for (let i = 1; i < lines.length; i++) {\r\n                const values = parseCSVLine(lines[i]);\r\n                if (values.length > 0 && values[0].trim()) {\r\n                    const row = {};\r\n                    headers.forEach((header, index) => {\r\n                        row[header] = values[index] || '';\r\n                    });\r\n                    data.push(row);\r\n                }\r\n            }\r\n            \r\n            return { data, headers, hasAllFields };\r\n        }\r\n        \r\n        \/**\r\n         * Parse a CSV line handling quotes and commas\r\n         *\/\r\n        function parseCSVLine(line) {\r\n            const result = [];\r\n            let current = '';\r\n            let inQuotes = false;\r\n            \r\n            for (let i = 0; i < line.length; i++) {\r\n                const char = line[i];\r\n                \r\n                if (char === '\"') {\r\n                    if (inQuotes && line[i + 1] === '\"') {\r\n                        current += '\"';\r\n                        i++; \/\/ Skip next quote\r\n                    } else {\r\n                        inQuotes = !inQuotes;\r\n                    }\r\n                } else if (char === ',' && !inQuotes) {\r\n                    result.push(current.trim());\r\n                    current = '';\r\n                } else {\r\n                    current += char;\r\n                }\r\n            }\r\n            \r\n            result.push(current.trim());\r\n            return result;\r\n        }\r\n        \r\n        \/**\r\n         * Convert CSV data to flashcard format\r\n         *\/\r\n        function convertCSVToFlashcards(csvData, headers) {\r\n            return csvData.map(row => {\r\n                \/\/ Find the appropriate fields by header matching\r\n                const getField = (patterns) => {\r\n                    for (const pattern of patterns) {\r\n                        const header = headers.find(h => h.toLowerCase().includes(pattern.toLowerCase()));\r\n                        if (header && row[header]) {\r\n                            return row[header].trim();\r\n                        }\r\n                    }\r\n                    return '';\r\n                };\r\n                \r\n                return {\r\n                    word: getField(['word']) || Object.values(row)[0] || '',\r\n                    pinyin: getField(['pinyin', 'pin']) || '',\r\n                    traditional: getField(['traditional', 'trad']) || '',\r\n                    english: getField(['english', 'meaning']) || '',\r\n                    example_sentence: getField(['example', 'sentence']) || ''\r\n                };\r\n            });\r\n        }\r\n        \r\n        \/**\r\n         * Handle CSV file import\r\n         *\/\r\n        function handleCSVImport(file) {\r\n            const reader = new FileReader();\r\n            reader.onload = function(e) {\r\n                try {\r\n                    const csvText = e.target.result;\r\n                    const { data, headers, hasAllFields } = parseCSV(csvText);\r\n                    \r\n                    if (hasAllFields) {\r\n                        \/\/ CSV has all required fields - convert to flashcards directly\r\n                        const flashcards = convertCSVToFlashcards(data, headers);\r\n                        flashcardData = flashcards;\r\n                        \r\n                        \/\/ Show words in text area\r\n                        const words = flashcards.map(card => card.word).filter(word => word);\r\n                        vocabularyInput.value = words.join(', ');\r\n                        \r\n                        \/\/ Display flashcards immediately\r\n                        displayFlashcards(flashcards);\r\n                        \r\n                    } else {\r\n                        \/\/ CSV doesn't have all fields - extract words only\r\n                        const words = data.map(row => Object.values(row)[0]).filter(word => word && word.trim());\r\n                        vocabularyInput.value = words.join(', ');\r\n                        \r\n                        \/\/ Show message to user\r\n                        showError(`Imported ${words.length} words from CSV. Click \"Generate Flash Cards\" to create complete flashcards.`);\r\n                        setTimeout(() => {\r\n                            errorMessage.style.display = 'none';\r\n                        }, 3000);\r\n                    }\r\n                    \r\n                } catch (error) {\r\n                    const errorMsg = error && error.message ? error.message : (error ? String(error) : 'Unknown error');\r\n                    showError(`Failed to import CSV: ${errorMsg}`);\r\n                }\r\n            };\r\n            \r\n            reader.onerror = function() {\r\n                showError('Failed to read the CSV file.');\r\n            };\r\n            \r\n            reader.readAsText(file, 'utf-8');\r\n        }\r\n        \r\n        \/**\r\n         * Prepare query with instruction prefix\r\n         *\/\r\n        function prepareQuery(words) {\r\n            const instruction = \"Generate the 'word', 'pinyin', 'traditional', 'english', 'example_sentence' for the following Chinese words or phrases: \";\r\n            return instruction + words;\r\n        }\r\n\r\n        \/**\r\n         * Clean input by removing meaningless symbols but keeping Chinese characters\r\n         *\/\r\n        function cleanInput(input) {\r\n            \/\/ Keep Chinese characters, letters, numbers, spaces, commas, and common punctuation\r\n            \/\/ Chinese Unicode ranges: \\u4e00-\\u9fff (common), \\u3400-\\u4dbf (extension A)\r\n            let cleaned = input\r\n                .replace(\/[^\\u4e00-\\u9fff\\u3400-\\u4dbfa-zA-Z0-9\\s,;\uff0c\u3002\u3001\uff01\uff1f\uff1b\uff1a\"\"''\uff08\uff09\\-']\/g, ' ')\r\n                .replace(\/\\s+\/g, ' ')\r\n                .trim();\r\n            \r\n            return cleaned;\r\n        }\r\n\r\n        \/**\r\n         * Verify and format input to ensure words are properly separated\r\n         *\/\r\n        function verifyAndFormatInput(input) {\r\n            \/\/ First clean the input\r\n            let cleanedInput = cleanInput(input);\r\n            \r\n            \/\/ Split by various possible separators (including Chinese punctuation)\r\n            let words = cleanedInput.split(\/[,\uff0c\\s\\n\\r\\t\uff1a\u3002\u3010\u3011\uff08\uff09\u3001]+\/)\r\n                .map(word => word.trim())\r\n                .filter(word => word.length > 0)\r\n                .filter(word => \/[\\u4e00-\\u9fff\\u3400-\\u4dbf]\/.test(word)); \/\/ Keep words with Chinese characters\r\n            \r\n            \/\/ Join with commas and spaces for clean formatting\r\n            let formattedInput = words.join(', ');\r\n            \r\n            return {\r\n                words: words,\r\n                formattedInput: formattedInput,\r\n                isModified: input !== formattedInput\r\n            };\r\n        }\r\n\r\n        \/**\r\n         * Parse assistant response to extract flashcard data\r\n         *\/\r\n        function parseAssistantResponse(apiResponse, batchIndex) {\r\n            try {\r\n                if (!apiResponse || !apiResponse.messages || !Array.isArray(apiResponse.messages)) {\r\n                    throw new Error(\"Invalid response structure - no messages array found\");\r\n                }\r\n                \r\n                const assistantMessage = apiResponse.messages.find(msg =>\r\n                    msg.role === \"assistant\" && msg.type === \"answer\"\r\n                );\r\n                \r\n                if (!assistantMessage) {\r\n                    throw new Error(\"No assistant answer found in response\");\r\n                }\r\n                \r\n                \/\/ Convert the full response to string to work with the raw structure\r\n                const fullResponseStr = JSON.stringify(apiResponse, null, 2);\r\n                \r\n                \/\/ Look for the specific pattern: role: \"assistant\", type: \"answer\", content: \"...\"\r\n                const contentPattern = \/\"role\":\\s*\"assistant\",\\s*\"type\":\\s*\"answer\",\\s*\"content\":\\s*\"([\\s\\S]*?)\"\\s*,\\s*\"content_type\":\\s*\"text\"\/;\r\n                let contentMatch = fullResponseStr.match(contentPattern);\r\n                \r\n                \/\/ Extract the content\r\n                let extractedContent;\r\n                let needsParsing = true;\r\n                \r\n                if (contentMatch && contentMatch[1]) {\r\n                    extractedContent = contentMatch[1];\r\n                    needsParsing = true;\r\n                } else if (assistantMessage && assistantMessage.content) {\r\n                    if (typeof assistantMessage.content === 'string') {\r\n                        extractedContent = assistantMessage.content;\r\n                        needsParsing = true;\r\n                    } else if (typeof assistantMessage.content === 'object') {\r\n                        parsedData = assistantMessage.content;\r\n                        needsParsing = false;\r\n                    } else {\r\n                        throw new Error(\"Invalid content type in assistant message\");\r\n                    }\r\n                } else {\r\n                    throw new Error(\"Could not find assistant content pattern in response\");\r\n                }\r\n                \r\n                \/\/ Clean the escaped JSON content and parse if needed\r\n                let parsedData;\r\n                if (needsParsing) {\r\n                    let cleanedContent = extractedContent\r\n                        .replace(\/\\\\\\\\\/g, '\\\\')\r\n                        .replace(\/\\\\\"\/g, '\"')\r\n                        .replace(\/\\\\n\/g, '\\n')\r\n                        .replace(\/\\\\r\/g, '\\r')\r\n                        .replace(\/\\\\t\/g, '\\t');\r\n                    \r\n                    \/\/ Now try to parse the JSON\r\n                    try {\r\n                        parsedData = JSON.parse(cleanedContent);\r\n                    } catch (parseError) {\r\n                        let firstJsonObject = extractFirstCompleteJsonObject(cleanedContent);\r\n                        \r\n                        if (firstJsonObject) {\r\n                            try {\r\n                                parsedData = JSON.parse(firstJsonObject);\r\n                            } catch (firstObjectParseError) {\r\n                                throw new Error(`Failed to parse first JSON object: ${firstObjectParseError.message}`);\r\n                            }\r\n                        } else {\r\n                            throw new Error(`No valid JSON structure found: ${parseError.message}`);\r\n                        }\r\n                    }\r\n                }\r\n                \r\n                \/\/ Check for the new response format: { words: [...] }\r\n                if (parsedData && parsedData.words && Array.isArray(parsedData.words)) {\r\n                    return parsedData.words;\r\n                } else if (Array.isArray(parsedData)) {\r\n                    return parsedData;\r\n                } else if (parsedData && parsedData.word && typeof parsedData.word === 'string') {\r\n                    return [parsedData];\r\n                } else {\r\n                    const dataType = parsedData ? typeof parsedData : 'null\/undefined';\r\n                    const keys = parsedData && typeof parsedData === 'object' ? Object.keys(parsedData).join(', ') : 'N\/A';\r\n                    throw new Error(`No valid words array or word object found. Type: ${dataType}, Keys: ${keys}`);\r\n                }\r\n            } catch (error) {\r\n                const errorMsg = error && error.message ? error.message : (error ? String(error) : 'Unknown error');\r\n                throw new Error(`Error parsing response: ${errorMsg}`);\r\n            }\r\n        }\r\n\r\n        \/**\r\n         * Extract the first complete JSON object from content\r\n         *\/\r\n        function extractFirstCompleteJsonObject(content) {\r\n            let braceCount = 0;\r\n            let startIndex = -1;\r\n            let inString = false;\r\n            let escapeNext = false;\r\n            \r\n            for (let i = 0; i < content.length; i++) {\r\n                const char = content[i];\r\n                \r\n                if (escapeNext) {\r\n                    escapeNext = false;\r\n                    continue;\r\n                }\r\n                \r\n                if (char === '\\\\') {\r\n                    escapeNext = true;\r\n                    continue;\r\n                }\r\n                \r\n                if (char === '\"' && !escapeNext) {\r\n                    inString = !inString;\r\n                    continue;\r\n                }\r\n                \r\n                if (!inString) {\r\n                    if (char === '{') {\r\n                        if (startIndex === -1) {\r\n                            startIndex = i;\r\n                        }\r\n                        braceCount++;\r\n                    } else if (char === '}') {\r\n                        braceCount--;\r\n                        \r\n                        if (braceCount === 0 && startIndex !== -1) {\r\n                            const extractedJson = content.substring(startIndex, i + 1);\r\n                            return extractedJson;\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            \r\n            return null;\r\n        }\r\n\r\n        \/**\r\n         * Send request to Coze API with retry mechanism\r\n         *\/\r\n        async function sendCozeRequestWithRetry(words, batchIndex, maxRetries = 3) {\r\n            let lastError = null;\r\n            \r\n            for (let attempt = 1; attempt <= maxRetries; attempt++) {\r\n                try {\r\n                    const result = await sendCozeRequest(words, batchIndex, attempt);\r\n                    const cardData = parseAssistantResponse(result, batchIndex);\r\n                    \r\n                    return cardData;\r\n                } catch (error) {\r\n                    lastError = error;\r\n                    \r\n                    if (attempt < maxRetries) {\r\n                        const waitTime = Math.pow(2, attempt) * 1000;\r\n                        await new Promise(resolve => setTimeout(resolve, waitTime));\r\n                    }\r\n                }\r\n            }\r\n            \r\n            return [];\r\n        }\r\n\r\n        \/**\r\n         * Send request to Coze API (base function)\r\n         *\/\r\n        async function sendCozeRequest(words, batchIndex, attempt = 1) {\r\n            const query = prepareQuery(words);\r\n            \r\n            const payload = {\r\n                bot_id: COZE_BOT_ID,\r\n                user: USER_ID,\r\n                query: query,\r\n                stream: false\r\n            };\r\n\r\n            const performCozeCall = async () => {\r\n                const response = await fetch(COZE_API_URL, {\r\n                    method: 'POST',\r\n                    headers: {\r\n                        'Content-Type': 'application\/json',\r\n                        'Authorization': `Bearer ${COZE_API_KEY}`,\r\n                        'Accept': '*\/*'\r\n                    },\r\n                    body: JSON.stringify(payload)\r\n                });\r\n\r\n                if (!response.ok) {\r\n                    const errorText = await response.text();\r\n                    throw new Error(`HTTP ${response.status}: ${errorText}`);\r\n                }\r\n\r\n                return await response.json();\r\n            };\r\n\r\n            const responseData = await retryWithKeyRefresh(performCozeCall);\r\n            return responseData;\r\n        }\r\n\r\n        \/**\r\n         * Process words in batches using Coze API\r\n         *\/\r\n        async function processVocabulary(words) {\r\n            const batches = [];\r\n            for (let i = 0; i < words.length; i += BATCH_SIZE) {\r\n                batches.push(words.slice(i, i + BATCH_SIZE));\r\n            }\r\n\r\n            const allCards = [];\r\n            let failedBatches = 0;\r\n            \r\n            for (let i = 0; i < batches.length; i++) {\r\n                const progressBefore = (i \/ batches.length) * 100;\r\n                updateProgress(progressBefore, `Processing batch ${i + 1} of ${batches.length}...`);\r\n                \r\n                const batchWords = batches[i].join(', ');\r\n                \r\n                const cardData = await sendCozeRequestWithRetry(batchWords, i);\r\n                \r\n                if (cardData.length > 0) {\r\n                    allCards.push(...cardData);\r\n                } else {\r\n                    failedBatches++;\r\n                }\r\n                \r\n                const progressAfter = ((i + 1) \/ batches.length) * 100;\r\n                updateProgress(progressAfter, `Completed batch ${i + 1} of ${batches.length}...`);\r\n                \r\n                if (i < batches.length - 1) {\r\n                    await new Promise(resolve => setTimeout(resolve, 1000));\r\n                }\r\n            }\r\n\r\n            updateProgress(100, 'Processing complete!');\r\n            \r\n            return allCards;\r\n        }\r\n\r\n        \/**\r\n         * Update progress bar\r\n         *\/\r\n        function updateProgress(percentage, text) {\r\n            progressFill.style.width = percentage + '%';\r\n            progressText.textContent = text;\r\n        }\r\n\r\n        \/**\r\n         * Clear pronunciation and example audio caches\r\n         *\/\r\n        function clearAudioCaches() {\r\n            \/\/ Cleanup pronunciation cache\r\n            Object.values(pronunciationCache).forEach(cache => {\r\n                if (cache.audioUrl) {\r\n                    try {\r\n                        URL.revokeObjectURL(cache.audioUrl);\r\n                    } catch (e) {\r\n                        console.warn('Failed to revoke URL:', e);\r\n                    }\r\n                }\r\n            });\r\n            pronunciationCache = {};\r\n            \r\n            \/\/ Cleanup example audio cache\r\n            Object.values(exampleAudioCache).forEach(cache => {\r\n                if (cache.audioUrl) {\r\n                    try {\r\n                        URL.revokeObjectURL(cache.audioUrl);\r\n                    } catch (e) {\r\n                        console.warn('Failed to revoke URL:', e);\r\n                    }\r\n                }\r\n            });\r\n            exampleAudioCache = {};\r\n        }\r\n\r\n        \/**\r\n         * Display flashcards\r\n         *\/\r\n        function displayFlashcards(cards) {\r\n            \/\/ Clear old audio caches when displaying new cards\r\n            clearAudioCaches();\r\n            \r\n            flashcardData = cards;\r\n            currentCardIndex = 0;\r\n            isFlipped = false;\r\n            \r\n            progressSection.style.display = 'none';\r\n            flashcardsSection.style.display = 'block';\r\n            settingsPanel.style.display = 'block';\r\n            document.getElementById('exportSection').style.display = 'block';\r\n            \r\n            updateCard();\r\n            updateControls();\r\n        }\r\n\r\n        \/**\r\n         * Update current card display based on settings\r\n         *\/\r\n        function updateCard() {\r\n            if (flashcardData.length === 0) return;\r\n            \r\n            const card = flashcardData[currentCardIndex];\r\n            \r\n            \/\/ Apply color theme\r\n            applyCardColor();\r\n            \r\n            \/\/ Front side - show based on settings\r\n            const pronunciationContainer = document.getElementById('pronunciationContainer');\r\n            pronunciationContainer.style.display = cardSettings.front.pronunciation ? 'flex' : 'none';\r\n            wordText.style.display = cardSettings.front.word ? 'flex' : 'none';\r\n            wordPinyin.style.display = cardSettings.front.pinyin ? 'block' : 'none';\r\n            \r\n            wordTextContent.textContent = card.word || 'Unknown';\r\n            wordPinyin.textContent = card.pinyin || '';\r\n            \r\n            \/\/ Back side - show based on settings\r\n            const traditionalGroup = traditionalMeaning.parentElement;\r\n            const englishGroup = englishMeaning.parentElement;\r\n            const exampleGroup = document.getElementById('example').parentElement;\r\n            \r\n            traditionalGroup.style.display = cardSettings.back.traditional ? 'block' : 'none';\r\n            englishGroup.style.display = cardSettings.back.english ? 'block' : 'none';\r\n            exampleGroup.style.display = cardSettings.back.example ? 'block' : 'none';\r\n            \r\n            traditionalMeaning.textContent = card.traditional || '';\r\n            englishMeaning.textContent = card.english || '';\r\n            exampleText.textContent = card.example_sentence || '';\r\n            \r\n            \/\/ Reset flip state\r\n            flashcard.classList.remove('flipped');\r\n            isFlipped = false;\r\n        }\r\n\r\n        \/**\r\n         * Apply card color theme\r\n         *\/\r\n        function applyCardColor() {\r\n            const cardFront = document.querySelector('.card-front');\r\n            const cardBack = document.querySelector('.card-back');\r\n            \r\n            if (cardSettings.color === 'default') {\r\n                cardFront.style.background = 'linear-gradient(135deg, #fdf6e3, #f9f2e7)';\r\n                cardBack.style.background = 'linear-gradient(135deg, #f0f8f0, #e8f5e8)';\r\n                cardFront.style.color = '#000';\r\n                cardBack.style.color = '#000';\r\n                \r\n                wordTextContent.style.color = '';\r\n                traditionalMeaning.style.color = '';\r\n                exampleText.style.color = '';\r\n            } else {\r\n                cardFront.style.background = cardSettings.color;\r\n                cardBack.style.background = cardSettings.color;\r\n                \r\n                if (cardSettings.color === '#222222' || cardSettings.color === '#416D19') {\r\n                    cardFront.style.color = '#fff';\r\n                    cardBack.style.color = '#fff';\r\n                    \r\n                    wordTextContent.style.color = '#f0f8f0';\r\n                    traditionalMeaning.style.color = '#f0f8f0';\r\n                    exampleText.style.color = '#fdf6e3';\r\n                } else {\r\n                    cardFront.style.color = '#000';\r\n                    cardBack.style.color = '#000';\r\n                    \r\n                    wordTextContent.style.color = '#000';\r\n                    traditionalMeaning.style.color = '#000';\r\n                    exampleText.style.color = '#000';\r\n                }\r\n            }\r\n        }\r\n\r\n        \/**\r\n         * Initialize settings event handlers\r\n         *\/\r\n        function initializeSettings() {\r\n            \/\/ Content display checkboxes\r\n            const checkboxes = {\r\n                frontPronunciation: 'front.pronunciation',\r\n                frontWord: 'front.word',\r\n                frontPinyin: 'front.pinyin',\r\n                backTraditional: 'back.traditional',\r\n                backEnglish: 'back.english',\r\n                backExample: 'back.example'\r\n            };\r\n            \r\n            Object.entries(checkboxes).forEach(([id, path]) => {\r\n                const checkbox = document.getElementById(id);\r\n                if (checkbox) {\r\n                    checkbox.addEventListener('change', () => {\r\n                        const [section, property] = path.split('.');\r\n                        cardSettings[section][property] = checkbox.checked;\r\n                        updateCard();\r\n                    });\r\n                }\r\n            });\r\n            \r\n            \/\/ Color options\r\n            document.querySelectorAll('.color-option').forEach(option => {\r\n                option.addEventListener('click', () => {\r\n                    document.querySelectorAll('.color-option').forEach(opt => \r\n                        opt.classList.remove('selected')\r\n                    );\r\n                    \r\n                    option.classList.add('selected');\r\n                    \r\n                    cardSettings.color = option.dataset.color;\r\n                    updateCard();\r\n                });\r\n            });\r\n        }\r\n\r\n        \/**\r\n         * Export flashcards to CSV\r\n         *\/\r\n        function exportToCSV() {\r\n            if (flashcardData.length === 0) {\r\n                showError('No flashcards to export. Please generate some flashcards first.');\r\n                return;\r\n            }\r\n            \r\n            \/\/ CSV headers\r\n            const headers = [\r\n                'Word',\r\n                'PinYin', \r\n                'Traditional Chinese Meaning',\r\n                'English Meaning',\r\n                'Example Sentence'\r\n            ];\r\n            \r\n            \/\/ Create CSV content with UTF-8 BOM\r\n            const BOM = '\\uFEFF';\r\n            let csvContent = BOM + headers.join(',') + '\\n';\r\n            \r\n            flashcardData.forEach(card => {\r\n                const row = [\r\n                    escapeCSVField(card.word || ''),\r\n                    escapeCSVField(card.pinyin || ''),\r\n                    escapeCSVField(card.traditional || ''),\r\n                    escapeCSVField(card.english || ''),\r\n                    escapeCSVField(card.example_sentence || '')\r\n                ];\r\n                csvContent += row.join(',') + '\\n';\r\n            });\r\n            \r\n            \/\/ Create and download file with proper UTF-8 encoding\r\n            const blob = new Blob([csvContent], { type: 'text\/csv;charset=utf-8;' });\r\n            const link = document.createElement('a');\r\n            const url = URL.createObjectURL(blob);\r\n            \r\n            link.setAttribute('href', url);\r\n            link.setAttribute('download', `mandarin_flashcards_${new Date().toISOString().slice(0, 10)}.csv`);\r\n            link.style.visibility = 'hidden';\r\n            \r\n            document.body.appendChild(link);\r\n            link.click();\r\n            document.body.removeChild(link);\r\n        }\r\n        \r\n        \/**\r\n         * Escape CSV field content\r\n         *\/\r\n        function escapeCSVField(field) {\r\n            if (typeof field !== 'string') field = String(field);\r\n            \r\n            if (field.includes(',') || field.includes('\\n') || field.includes('\"')) {\r\n                return '\"' + field.replace(\/\"\/g, '\"\"') + '\"';\r\n            }\r\n            return field;\r\n        }\r\n\r\n        \/**\r\n         * Update control buttons\r\n         *\/\r\n        function updateControls() {\r\n            cardCounter.textContent = `Card ${currentCardIndex + 1} of ${flashcardData.length}`;\r\n            prevBtn.disabled = currentCardIndex === 0;\r\n            nextBtn.disabled = currentCardIndex === flashcardData.length - 1;\r\n        }\r\n\r\n        \/**\r\n         * Show error message\r\n         *\/\r\n        function showError(message) {\r\n            errorMessage.textContent = message;\r\n            errorMessage.style.display = 'block';\r\n            progressSection.style.display = 'none';\r\n            setTimeout(() => {\r\n                errorMessage.style.display = 'none';\r\n            }, 5000);\r\n        }\r\n\r\n        \/**\r\n         * Main generation function\r\n         *\/\r\n        async function generateFlashcards() {\r\n            const input = vocabularyInput.value.trim();\r\n            if (!input) {\r\n                showError('Please enter some Chinese words or phrases.');\r\n                return;\r\n            }\r\n\r\n            \/\/ Verify and format the input\r\n            const verification = verifyAndFormatInput(input);\r\n            const words = verification.words;\r\n            \r\n            if (words.length === 0) {\r\n                showError('Please enter valid Chinese words or phrases.');\r\n                return;\r\n            }\r\n            \r\n            \/\/ Update the input field with the formatted version\r\n            if (verification.isModified) {\r\n                vocabularyInput.value = verification.formattedInput;\r\n            }\r\n\r\n            \/\/ Reset UI\r\n            errorMessage.style.display = 'none';\r\n            flashcardsSection.style.display = 'none';\r\n            progressSection.style.display = 'block';\r\n            generateBtn.disabled = true;\r\n            generateBtn.innerHTML = '<span class=\"loading-spinner\"><\/span> <span>Processing...<\/span>';\r\n\r\n            try {\r\n                const cards = await processVocabulary(words);\r\n                displayFlashcards(cards);\r\n            } catch (error) {\r\n                const errorMsg = error && error.message ? error.message : (error ? String(error) : 'Unknown error');\r\n                console.error('Error generating flashcards:', error);\r\n                showError(`Failed to generate flashcards: ${errorMsg}`);\r\n            } finally {\r\n                generateBtn.disabled = false;\r\n                generateBtn.innerHTML = '<span>Generate Flash Cards<\/span>';\r\n            }\r\n        }\r\n\r\n        \/\/ Event Listeners\r\n        generateBtn.addEventListener('click', generateFlashcards);\r\n        \r\n        \/\/ Export button\r\n        document.getElementById('exportBtn').addEventListener('click', exportToCSV);\r\n\r\n        prevBtn.addEventListener('click', () => {\r\n            if (currentCardIndex > 0) {\r\n                currentCardIndex--;\r\n                updateCard();\r\n                updateControls();\r\n            }\r\n        });\r\n\r\n        nextBtn.addEventListener('click', () => {\r\n            if (currentCardIndex < flashcardData.length - 1) {\r\n                currentCardIndex++;\r\n                updateCard();\r\n                updateControls();\r\n            }\r\n        });\r\n\r\n        flipBtn.addEventListener('click', () => {\r\n            flashcard.classList.toggle('flipped');\r\n            isFlipped = !isFlipped;\r\n        });\r\n\r\n        \/\/ Click card to flip\r\n        flashcard.addEventListener('click', (e) => {\r\n            \/\/ Don't flip if clicking the pronunciation or example audio icons\r\n            if (e.target.id === 'pronunciationIcon' || e.target.closest('#pronunciationIcon') ||\r\n                e.target.id === 'exampleAudioIcon' || e.target.closest('#exampleAudioIcon')) {\r\n                return;\r\n            }\r\n            flashcard.classList.toggle('flipped');\r\n            isFlipped = !isFlipped;\r\n        });\r\n\r\n        \/\/ Pronunciation icon click handler\r\n        pronunciationIcon.addEventListener('click', async (e) => {\r\n            e.stopPropagation();\r\n            const currentWord = flashcardData[currentCardIndex]?.word;\r\n            if (currentWord) {\r\n                await playPronunciation(currentWord, pronunciationIcon);\r\n            }\r\n        });\r\n\r\n        \/\/ Example audio icon click handler\r\n        exampleAudioIcon.addEventListener('click', async (e) => {\r\n            e.stopPropagation();\r\n            const currentExample = flashcardData[currentCardIndex]?.example_sentence;\r\n            if (currentExample) {\r\n                await playExampleAudio(currentExample, exampleAudioIcon);\r\n            }\r\n        });\r\n\r\n        \/\/ Keyboard navigation\r\n        document.addEventListener('keydown', (e) => {\r\n            if (flashcardsSection.style.display === 'none') return;\r\n            \r\n            switch(e.key) {\r\n                case 'ArrowLeft':\r\n                    e.preventDefault();\r\n                    if (currentCardIndex > 0) {\r\n                        currentCardIndex--;\r\n                        updateCard();\r\n                        updateControls();\r\n                    }\r\n                    break;\r\n                case 'ArrowRight':\r\n                    e.preventDefault();\r\n                    if (currentCardIndex < flashcardData.length - 1) {\r\n                        currentCardIndex++;\r\n                        updateCard();\r\n                        updateControls();\r\n                    }\r\n                    break;\r\n                case 'ArrowUp':\r\n                case 'ArrowDown':\r\n                    e.preventDefault();\r\n                    flashcard.classList.toggle('flipped');\r\n                    isFlipped = !isFlipped;\r\n                    break;\r\n            }\r\n        });\r\n\r\n        \/\/ Enter key to generate\r\n        vocabularyInput.addEventListener('keydown', (e) => {\r\n            if (e.key === 'Enter' && e.ctrlKey) {\r\n                e.preventDefault();\r\n                generateFlashcards();\r\n            }\r\n        });\r\n        \r\n        \/\/ Drag and drop functionality\r\n        const textareaContainer = vocabularyInput.parentElement;\r\n        \r\n        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {\r\n            textareaContainer.addEventListener(eventName, preventDefaults, false);\r\n            document.body.addEventListener(eventName, preventDefaults, false);\r\n        });\r\n        \r\n        function preventDefaults(e) {\r\n            e.preventDefault();\r\n            e.stopPropagation();\r\n        }\r\n        \r\n        ['dragenter', 'dragover'].forEach(eventName => {\r\n            textareaContainer.addEventListener(eventName, highlight, false);\r\n        });\r\n        \r\n        ['dragleave', 'drop'].forEach(eventName => {\r\n            textareaContainer.addEventListener(eventName, unhighlight, false);\r\n        });\r\n        \r\n        function highlight(e) {\r\n            if (e.dataTransfer.types.includes('Files')) {\r\n                dragOverlay.classList.add('active');\r\n            }\r\n        }\r\n        \r\n        function unhighlight(e) {\r\n            dragOverlay.classList.remove('active');\r\n        }\r\n        \r\n        textareaContainer.addEventListener('drop', handleDrop, false);\r\n        \r\n        function handleDrop(e) {\r\n            const dt = e.dataTransfer;\r\n            const files = dt.files;\r\n            \r\n            if (files.length > 0) {\r\n                const file = files[0];\r\n                \r\n                if (file.name.toLowerCase().endsWith('.csv') || file.type === 'text\/csv') {\r\n                    handleCSVImport(file);\r\n                } else {\r\n                    showError('Please drop a CSV file only.');\r\n                }\r\n            }\r\n        }\r\n        \r\n        \/\/ Initialize settings when page loads\r\n        document.addEventListener('DOMContentLoaded', async () => {\r\n            initializeSettings();\r\n            \r\n            \/\/ Fetch API keys from backend\r\n            try {\r\n                const { cozeKey, minimaxKey } = await initializeApiKeys();\r\n                COZE_API_KEY_RUNTIME = cozeKey;\r\n                MINIMAX_API_KEY_RUNTIME = minimaxKey;\r\n                console.log('\u2713 Coze API key initialized successfully');\r\n                console.log('\u2713 Minimax API key initialized successfully');\r\n            } catch (error) {\r\n                console.error('Failed to initialize API keys:', error);\r\n                alert('Authorisation was not granted by the server. This may be due to a network issue. Please check your connection and try again.');\r\n            }\r\n        });\r\n\r\n        window.addEventListener('beforeunload', () => {\r\n            clearApiKeys();\r\n            clearAudioCaches();\r\n        });\r\n    <\/script>\r\n<\/body>\r\n<\/html>\r\n\r\n\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>&nbsp; Mandarin Flashcard Generator Transform Chinese words and phrases into comprehensive study cards featuring PinYin, traditional Chinese, English translations, and example sentences with audio for effective Mandarin learning. Step 1: Input the Chinese Words or Phrases You Would Like to Learn Enter Chinese words or phrases below. Or drag-and-drop a CSV file to import existing...<\/p>\n","protected":false},"author":748,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"redux-templates_canvas","meta":{"_expiration-date-status":"","_expiration-date":0,"_expiration-date-type":"","_expiration-date-categories":[],"_expiration-date-options":[]},"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v17.3 (Yoast SEO v21.2) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Agent 9 Mandarin Flashcard Generator - Hong Kong Metropolitan University<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Agent 9 Mandarin Flashcard Generator\" \/>\n<meta property=\"og:description\" content=\"&nbsp; Mandarin Flashcard Generator Transform Chinese words and phrases into comprehensive study cards featuring PinYin, traditional Chinese, English\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/\" \/>\n<meta property=\"og:site_name\" content=\"Hong Kong Metropolitan University\" \/>\n<meta property=\"article:modified_time\" content=\"2026-03-04T01:22:53+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Agent 9 Mandarin Flashcard Generator - Hong Kong Metropolitan University","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/","og_locale":"en_US","og_type":"article","og_title":"Agent 9 Mandarin Flashcard Generator","og_description":"&nbsp; Mandarin Flashcard Generator Transform Chinese words and phrases into comprehensive study cards featuring PinYin, traditional Chinese, English","og_url":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/","og_site_name":"Hong Kong Metropolitan University","article_modified_time":"2026-03-04T01:22:53+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/","url":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/","name":"Agent 9 Mandarin Flashcard Generator - Hong Kong Metropolitan University","isPartOf":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/#website"},"datePublished":"2025-11-06T07:53:32+00:00","dateModified":"2026-03-04T01:22:53+00:00","breadcrumb":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent-9-mandarin-flashcard-generator\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Open Educational Tools","item":"\/oetools\/"},{"@type":"ListItem","position":2,"name":"Agent 9 Mandarin Flashcard Generator"}]},{"@type":"WebSite","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/#website","url":"https:\/\/www.hkmu.edu.hk\/oetools\/","name":"Hong Kong Metropolitan University","description":"Open Educational Tools - Hong Kong Metropolitan University","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.hkmu.edu.hk\/oetools\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"}]}},"_links":{"self":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/30966"}],"collection":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/users\/748"}],"replies":[{"embeddable":true,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/comments?post=30966"}],"version-history":[{"count":23,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/30966\/revisions"}],"predecessor-version":[{"id":32311,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/30966\/revisions\/32311"}],"wp:attachment":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/media?parent=30966"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}