AI.Demo/src/views/QuestionGenerator.vue

1445 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, computed, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import axios from "axios";
import {
GRS_API_KEY,
QUESTION_API_URL,
QUESTION_MODEL,
QUESTION_TEMPERATURE,
QUESTION_MAX_TOKENS,
QUESTION_GENERATOR_PROMPT,
} from "@/config/index.js";
const router = useRouter();
const API_KEY = GRS_API_KEY;
const API_URL = QUESTION_API_URL;
// ── 状态 ──
const status = ref("idle"); // 'idle' | 'generating' | 'done' | 'error'
const errorMsg = ref("");
const rawContent = ref("");
const cancelTokenSource = ref(null);
// ── 参数配置 ──
const difficulty = ref("medium"); // 'easy' | 'medium' | 'hard'
const questionCount = ref(5);
// 新增维度:教材章节、知识点、题型
const textbookChapter = ref(""); // 教材章节(预设)
const customTextbookChapter = ref(""); // 自定义教材章节输入
const selectedKnowledgePoints = ref([]); // 选中的知识点
const customKnowledgePoint = ref(""); // 自定义知识点输入
const questionFormat = ref("单项选择"); // 题型(预设)
const customQuestionFormat = ref(""); // 自定义题型输入
// ── 配置选项 ──
const DIFFICULTY_OPTIONS = [
{ value: "very_easy", label: "容易", color: "#10b981" },
{ value: "easy", label: "较易", color: "#34d399" },
{ value: "medium", label: "适中", color: "#f59e0b" },
{ value: "hard", label: "较难", color: "#f97316" },
{ value: "very_hard", label: "困难", color: "#ef4444" },
];
// 教材章节预设选项
const CHAPTER_OPTIONS = [
{ value: "Starter Unit1 You and Me", label: "Starter Unit1 You and Me" },
{ value: "Starter Unit2 Keep Tidy", label: "Starter Unit2 Keep Tidy" },
{ value: "Starter Unit3 Welcome!", label: "Starter Unit3 Welcome!" },
{ value: "Unit 1 You and Me", label: "Unit 1 You and Me" },
{ value: "Unit 2 We're Family!", label: "Unit 2 We're Family!" },
{ value: "Unit 3 My School", label: "Unit 3 My School" },
];
// 知识点预设选项
const KNOWLEDGE_POINT_OPTIONS = [
{ value: "语法", label: "语法" },
{ value: "词汇", label: "词汇" },
{ value: "冠词", label: "冠词" },
{ value: "不定冠词", label: "不定冠词" },
{ value: "定冠词", label: "定冠词" },
{ value: "名词", label: "名词" },
{ value: "动词", label: "动词" },
{ value: "形容词", label: "形容词" },
{ value: "代词", label: "代词" },
{ value: "介词", label: "介词" },
{ value: "时态", label: "时态" },
{ value: "一般现在时", label: "一般现在时" },
{ value: "现在进行时", label: "现在进行时" },
{ value: "一般过去时", label: "一般过去时" },
{ value: "句型结构", label: "句型结构" },
{ value: "阅读理解", label: "阅读理解" },
{ value: "写作", label: "写作" },
];
// 题型预设选项
const FORMAT_OPTIONS = [
{ value: "单项选择", label: "单项选择" },
{ value: "完形填空", label: "完形填空" },
{ value: "句型转换", label: "句型转换" },
{ value: "词汇运用", label: "词汇运用" },
{ value: "翻译句子", label: "翻译句子" },
{ value: "阅读理解", label: "阅读理解" },
{ value: "书面表达", label: "书面表达" },
{ value: "短文填空", label: "短文填空" },
{ value: "语法填空", label: "语法填空" },
{ value: "选词填空", label: "选词填空" },
];
// 切换知识点选中状态
const toggleKnowledgePoint = (value) => {
const index = selectedKnowledgePoints.value.indexOf(value);
if (index > -1) {
selectedKnowledgePoints.value.splice(index, 1);
} else {
selectedKnowledgePoints.value.push(value);
}
};
// ── 解析试题内容 ──
const parsedQuestions = computed(() => {
const raw = rawContent.value;
if (!raw) return [];
const questions = [];
const questionBlocks = raw.split(/##\s*题目\s*\d+/i);
questionBlocks.forEach((block, index) => {
if (index === 0) return; // 跳过第一个空块
const question = {
id: index,
type: extractField(block, "题目类型"),
difficulty: extractField(block, "难度等级"),
content: extractField(block, "题目内容"),
options: extractOptions(block),
answer: extractField(block, "正确答案"),
explanation: extractField(block, "解析"),
knowledgePoint: extractField(block, "知识点"),
};
if (question.content) {
questions.push(question);
}
});
return questions;
});
function extractField(text, fieldName) {
const regex = new RegExp(`\\*\\*${fieldName}\\*\\*[:]\\s*([\\s\\S]*?)(?=\\*\\*|$)`, "i");
const match = text.match(regex);
if (!match) return "";
let content = match[1].trim();
// 清理可能混入的下一个字段
content = content.split(/\*\*/)[0].trim();
return content;
}
function extractOptions(text) {
const options = [];
// 先尝试提取"选项"字段的内容块
const optionsBlockMatch = text.match(/\*\*选项\*\*[:]\s*([\s\S]*?)(?=\*\*正确答案\*\*|$)/i);
if (optionsBlockMatch) {
const optionsBlock = optionsBlockMatch[1];
// 在选项块中提取各个选项
const lines = optionsBlock.split('\n');
for (const line of lines) {
const match = line.match(/^([A-D])[\..。:]\s*(.+)$/i);
if (match) {
options.push({
label: match[1].toUpperCase(),
content: match[2].trim(),
});
}
}
}
return options;
}
// ── 构建请求体 ──
const buildRequestBody = () => {
const difficultyLabel =
DIFFICULTY_OPTIONS.find((d) => d.value === difficulty.value)?.label || "中等";
// 构建知识点字符串:预设选项 + 自定义输入
const allKnowledgePoints = [
...selectedKnowledgePoints.value,
...(customKnowledgePoint.value ? [customKnowledgePoint.value] : []),
];
const knowledgePointStr = allKnowledgePoints.length > 0 ? allKnowledgePoints.join("、") : "";
// 教材章节:优先使用自定义输入,否则使用预设选择
const chapterStr = customTextbookChapter.value || textbookChapter.value || "";
// 题型:优先使用自定义输入,否则使用预设选择
const formatStr = customQuestionFormat.value || questionFormat.value || "";
const userPrompt = `请生成 ${questionCount.value}${difficultyLabel}难度的英语试题。
要求:
1. 题型:${formatStr}
2. 难度等级:${difficultyLabel}
3. 题目数量:${questionCount.value}
${chapterStr ? `4. 教材章节:${chapterStr}` : ""}
${knowledgePointStr ? `5. 知识点:${knowledgePointStr}` : ""}
请严格按照指定的输出格式生成试题。`;
return {
model: QUESTION_MODEL,
messages: [
{ role: "system", content: QUESTION_GENERATOR_PROMPT },
{ role: "user", content: userPrompt },
],
temperature: QUESTION_TEMPERATURE,
max_tokens: QUESTION_MAX_TOKENS,
stream: true,
};
};
// ── 提取 SSE delta 文本 ──
const extractTextDelta = (chunk) => {
let result = "";
const lines = chunk.split("\n");
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const jsonStr = line.slice(6).trim();
if (!jsonStr || jsonStr === "[DONE]") continue;
try {
const data = JSON.parse(jsonStr);
const text = data?.choices?.[0]?.delta?.content;
if (text) result += text;
} catch {
// 忽略解析失败的行
}
}
return result;
};
// ── 过滤 HexString思考块 ──
const filterThinkBlocks = (text) => {
let filtered = text.replace(/HexString[\s\S]*?<\/think>/g, "");
filtered = filtered.replace(/HexString[\s\S]*$/, "");
return filtered;
};
// ── 开始生成 ──
const canGenerate = computed(() => {
return status.value !== "generating" && questionCount.value >= 1 && questionCount.value <= 20;
});
const startGeneration = async () => {
if (!canGenerate.value) return;
status.value = "generating";
errorMsg.value = "";
rawContent.value = "";
cancelTokenSource.value = axios.CancelToken.source();
let lastLength = 0;
await axios
.post(API_URL, buildRequestBody(), {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
responseType: "text",
cancelToken: cancelTokenSource.value.token,
onDownloadProgress: (event) => {
const text = (event.target || event.event?.target)?.responseText || "";
if (!text) return;
const newChunk = text.slice(lastLength);
lastLength = text.length;
const delta = extractTextDelta(newChunk);
if (delta) {
rawContent.value = filterThinkBlocks(rawContent.value + delta);
}
},
})
.then(() => {
status.value = "done";
})
.catch((err) => {
if (axios.isCancel(err)) return;
console.error(err);
errorMsg.value =
err?.response?.data?.error?.message || err.message || "请求失败,请稍后重试";
status.value = "error";
});
};
const resetAll = () => {
status.value = "idle";
rawContent.value = "";
errorMsg.value = "";
};
const loadExample = () => {
difficulty.value = "medium";
questionCount.value = 3;
textbookChapter.value = "Starter Unit2 Keep Tidy";
customTextbookChapter.value = "";
selectedKnowledgePoints.value = ["语法", "冠词", "不定冠词"];
customKnowledgePoint.value = "";
questionFormat.value = "句型转换";
customQuestionFormat.value = "";
};
const goBack = () => router.back();
onUnmounted(() => {
if (cancelTokenSource.value) {
cancelTokenSource.value.cancel("组件卸载,取消请求");
}
});
</script>
<template>
<div class="page-container">
<!-- Nav Bar -->
<div class="nav-bar">
<button class="back-btn" @click="goBack">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="15 18 9 12 15 6" />
</svg>
<span>返回</span>
</button>
<h1 class="nav-title">AI 试题生成</h1>
<div style="width: 72px"></div>
</div>
<!-- Error Banner -->
<div v-if="errorMsg" class="error-banner">
<span>{{ errorMsg }}</span>
<button class="error-close" @click="errorMsg = ''">✕</button>
</div>
<!-- Main Layout: Left + Right -->
<div class="main-layout">
<!-- Left Panel: Config -->
<div class="left-panel">
<div class="config-card">
<div class="config-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="3" />
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
/>
</svg>
<span>参数配置</span>
</div>
<div class="config-body">
<!-- 难度等级 -->
<div class="config-item">
<label class="config-label">难度等级</label>
<div class="difficulty-group">
<button
v-for="opt in DIFFICULTY_OPTIONS"
:key="opt.value"
class="difficulty-btn"
:class="{ active: difficulty === opt.value }"
:style="{ '--accent-color': opt.color }"
@click="difficulty = opt.value"
:disabled="status === 'generating'"
>
{{ opt.label }}
</button>
</div>
</div>
<!-- 题目数量 -->
<div class="config-item">
<label class="config-label">
题目数量
<span class="count-value">{{ questionCount }} 道</span>
</label>
<input
type="range"
v-model.number="questionCount"
min="1"
max="20"
class="range-slider"
:disabled="status === 'generating'"
/>
<div class="range-labels">
<span>1</span>
<span>10</span>
<span>20</span>
</div>
</div>
<!-- 教材章节 -->
<div class="config-item">
<label class="config-label">教材章节(可选)</label>
<select
v-model="textbookChapter"
class="select-input"
:disabled="status === 'generating'"
>
<option value="">请选择教材章节</option>
<option
v-for="opt in CHAPTER_OPTIONS"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</option>
</select>
<input
type="text"
v-model="customTextbookChapter"
class="topic-input mt-2"
placeholder="或输入自定义教材章节..."
:disabled="status === 'generating'"
/>
</div>
<!-- 知识点 -->
<div class="config-item">
<label class="config-label">知识点(可多选)</label>
<div class="knowledge-grid">
<button
v-for="opt in KNOWLEDGE_POINT_OPTIONS"
:key="opt.value"
class="knowledge-btn"
:class="{ active: selectedKnowledgePoints.includes(opt.value) }"
@click="toggleKnowledgePoint(opt.value)"
:disabled="status === 'generating'"
>
{{ opt.label }}
</button>
</div>
<input
type="text"
v-model="customKnowledgePoint"
class="topic-input mt-2"
placeholder="或输入自定义知识点..."
:disabled="status === 'generating'"
/>
</div>
<!-- 题型 -->
<div class="config-item">
<label class="config-label">题型</label>
<select
v-model="questionFormat"
class="select-input"
:disabled="status === 'generating'"
>
<option
v-for="opt in FORMAT_OPTIONS"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</option>
</select>
<input
type="text"
v-model="customQuestionFormat"
class="topic-input mt-2"
placeholder="或输入自定义题型..."
:disabled="status === 'generating'"
/>
</div>
<!-- 示例按钮 -->
<button class="example-btn" @click="loadExample" :disabled="status === 'generating'">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
</svg>
<span>加载示例</span>
</button>
</div>
</div>
<!-- Generate Button -->
<div class="btn-wrap">
<button
class="generate-btn"
:disabled="!canGenerate"
@click="startGeneration"
>
<span v-if="status === 'generating'" class="btn-spinner"></span>
<svg
v-else
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
<span>{{ status === "generating" ? "生成中..." : "开始生成" }}</span>
</button>
<button
v-if="status === 'done' || status === 'error'"
class="reset-btn"
@click="resetAll"
>
重新生成
</button>
</div>
</div>
<!-- Right Panel: Result -->
<div class="right-panel">
<!-- Empty State -->
<div v-if="status === 'idle'" class="empty-state">
<div class="empty-icon">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<p class="empty-title">等待生成</p>
<p class="empty-hint">在左侧配置参数,点击「开始生成」创建试题</p>
</div>
<!-- Result Area -->
<div
v-else-if="status === 'generating' || status === 'done'"
class="result-area"
>
<div class="result-header">
<div class="result-title-wrap">
<div
class="result-dot"
:class="{ pulse: status === 'generating' }"
></div>
<span class="result-title">{{
status === "generating" ? "AI 正在生成..." : "生成完成"
}}</span>
</div>
<span v-if="parsedQuestions.length > 0" class="question-count-badge">
已生成 {{ parsedQuestions.length }} 题
</span>
</div>
<div class="questions-list">
<div
v-for="(q, idx) in parsedQuestions"
:key="q.id"
class="question-card"
:style="{ animationDelay: `${idx * 0.1}s` }"
>
<div class="question-header">
<div class="question-number">题目 {{ q.id }}</div>
<div class="question-meta">
<span class="meta-tag type-tag">{{ q.type }}</span>
<span class="meta-tag difficulty-tag">{{ q.difficulty }}</span>
</div>
</div>
<div class="question-content">
<p class="content-text">{{ q.content }}</p>
</div>
<div v-if="q.options && q.options.length > 0" class="options-grid">
<div
v-for="opt in q.options"
:key="opt.label"
class="option-item"
>
<span class="option-label">{{ opt.label }}.</span>
<span class="option-text">{{ opt.content }}</span>
</div>
</div>
<div class="answer-section">
<div class="answer-row">
<span class="answer-label">正确答案:</span>
<span class="answer-value">{{ q.answer }}</span>
</div>
</div>
<div class="explanation-section">
<div class="explanation-header">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
<span>解析</span>
</div>
<p class="explanation-text">{{ q.explanation }}</p>
</div>
<div class="knowledge-section">
<span class="knowledge-label">知识点:</span>
<span class="knowledge-value">{{ q.knowledgePoint }}</span>
</div>
</div>
<!-- 生成中的占位符 -->
<div v-if="status === 'generating'" class="loading-card">
<div class="loading-dots">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
<p class="loading-text">正在生成试题,请稍候...</p>
</div>
</div>
</div>
<!-- Error State -->
<div v-else-if="status === 'error'" class="empty-state error-state">
<div class="empty-icon error-icon">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<p class="empty-title">生成失败</p>
<p class="empty-hint">请检查网络连接后重试</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.page-container {
height: 100vh;
overflow: hidden;
background: var(--bg);
display: flex;
flex-direction: column;
font-family: "Inter", "PingFang SC", -apple-system, sans-serif;
}
/* Nav */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid var(--card-border);
backdrop-filter: blur(12px);
position: sticky;
top: 0;
z-index: 10;
}
.back-btn {
display: flex;
align-items: center;
gap: 0.35rem;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 0.95rem;
padding: 0.4rem 0.75rem;
border-radius: 8px;
transition: all 0.2s;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.07);
color: var(--text-primary);
}
.nav-title {
font-size: 1.2rem;
font-weight: 600;
margin: 0;
background: linear-gradient(135deg, #10b981, #059669);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Main Layout */
.main-layout {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
/* Left Panel */
.left-panel {
width: 380px;
min-width: 320px;
max-width: 440px;
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1.5rem;
border-right: 1px solid var(--card-border);
overflow-y: auto;
flex-shrink: 0;
}
/* Right Panel */
.right-panel {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
}
.right-panel::-webkit-scrollbar {
width: 8px;
}
.right-panel::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
margin: 4px 0;
}
.right-panel::-webkit-scrollbar-thumb {
background: rgba(16, 185, 129, 0.3);
border-radius: 4px;
transition: background 0.2s;
}
.right-panel::-webkit-scrollbar-thumb:hover {
background: rgba(16, 185, 129, 0.5);
}
/* Empty State */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
color: var(--text-secondary);
opacity: 0.5;
}
.empty-icon {
color: var(--text-secondary);
opacity: 0.4;
}
.empty-title {
font-size: 1rem;
font-weight: 600;
margin: 0;
color: var(--text-primary);
}
.empty-hint {
font-size: 0.875rem;
margin: 0;
text-align: center;
}
.error-state {
opacity: 1;
}
.error-icon {
color: #f87171;
opacity: 1;
}
.error-state .empty-title {
color: #fca5a5;
}
/* Error Banner */
.error-banner {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(239, 68, 68, 0.12);
border: 1px solid rgba(239, 68, 68, 0.3);
border-bottom: 1px solid rgba(239, 68, 68, 0.3);
padding: 0.75rem 1.5rem;
color: #fca5a5;
font-size: 0.9rem;
animation: fadeInUp 0.2s ease;
flex-shrink: 0;
}
.error-close {
background: none;
border: none;
color: #fca5a5;
cursor: pointer;
font-size: 1rem;
opacity: 0.7;
transition: opacity 0.2s;
}
.error-close:hover {
opacity: 1;
}
/* Config Card */
.config-card {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--card-border);
border-radius: 20px;
overflow: hidden;
backdrop-filter: blur(12px);
}
.config-header {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 1.25rem;
border-bottom: 1px solid var(--card-border);
color: #10b981;
font-weight: 600;
font-size: 1rem;
}
.config-body {
padding: 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;
overflow-y: auto;
min-height: 0;
}
.config-body::-webkit-scrollbar {
width: 6px;
}
.config-body::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
}
.config-body::-webkit-scrollbar-thumb {
background: rgba(16, 185, 129, 0.3);
border-radius: 3px;
transition: background 0.2s;
}
.config-body::-webkit-scrollbar-thumb:hover {
background: rgba(16, 185, 129, 0.5);
}
.config-item {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.config-label {
font-size: 0.9rem;
font-weight: 500;
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: space-between;
}
.count-value {
font-size: 0.85rem;
color: #10b981;
font-weight: 600;
}
/* Difficulty Selection */
.difficulty-group {
display: flex;
gap: 0.75rem;
}
.difficulty-btn {
flex: 1;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.difficulty-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.05);
border-color: var(--accent-color);
}
.difficulty-btn.active {
background: rgba(255, 255, 255, 0.08);
border-color: var(--accent-color);
color: var(--accent-color);
}
.difficulty-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Range Slider */
.range-slider {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
outline: none;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
}
.range-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: #10b981;
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s;
}
.range-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.range-slider:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.range-labels {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--text-secondary);
opacity: 0.6;
}
/* Topic Input */
.topic-input {
width: 100%;
padding: 0.875rem 1rem;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
color: var(--text-primary);
font-size: 0.9rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.topic-input::placeholder {
color: var(--text-secondary);
opacity: 0.5;
}
.topic-input:focus {
border-color: rgba(16, 185, 129, 0.5);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
.topic-input:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Select Input */
.select-input {
width: 100%;
padding: 0.875rem 1rem;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
color: var(--text-primary);
font-size: 0.9rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 1rem center;
padding-right: 2.5rem;
}
.select-input:focus {
border-color: rgba(16, 185, 129, 0.5);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
.select-input:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.select-input option {
background: #1a1a2e;
color: var(--text-primary);
}
/* Knowledge Points Grid */
.knowledge-grid {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.knowledge-btn {
padding: 0.5rem 0.875rem;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: var(--text-secondary);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.knowledge-btn:hover:not(:disabled) {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.3);
color: #10b981;
}
.knowledge-btn.active {
background: rgba(16, 185, 129, 0.15);
border-color: #10b981;
color: #10b981;
}
.knowledge-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.mt-2 {
margin-top: 0.75rem;
}
/* Example Button */
.example-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.25);
border-radius: 10px;
color: #10b981;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
}
.example-btn:hover:not(:disabled) {
background: rgba(16, 185, 129, 0.2);
transform: translateY(-1px);
}
.example-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Generate Button */
.btn-wrap {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.generate-btn {
display: flex;
align-items: center;
gap: 0.6rem;
background: linear-gradient(135deg, #10b981, #059669);
border: none;
color: white;
font-size: 1rem;
font-weight: 600;
padding: 0.875rem 2.5rem;
border-radius: 50px;
cursor: pointer;
transition: all 0.25s;
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.35);
}
.generate-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(16, 185, 129, 0.5);
}
.generate-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
transform: none;
}
.reset-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--text-secondary);
font-size: 0.9rem;
padding: 0.875rem 1.5rem;
border-radius: 50px;
cursor: pointer;
transition: all 0.2s;
}
.reset-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
.btn-spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255, 255, 255, 0.35);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* Result Area */
.result-area {
display: flex;
flex-direction: column;
gap: 1rem;
}
.result-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-title-wrap {
display: flex;
align-items: center;
gap: 0.6rem;
}
.result-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
}
.result-dot.pulse {
animation: pulse 1.2s ease-in-out infinite;
}
.result-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
.question-count-badge {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
padding: 0.4rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
}
/* Questions List */
.questions-list {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.question-card {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--card-border);
border-radius: 16px;
overflow: hidden;
animation: fadeInUp 0.4s ease both;
}
.question-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
background: rgba(16, 185, 129, 0.04);
border-bottom: 1px solid var(--card-border);
}
.question-number {
font-size: 1rem;
font-weight: 600;
color: #10b981;
}
.question-meta {
display: flex;
gap: 0.5rem;
}
.meta-tag {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
.type-tag {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
}
.difficulty-tag {
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
}
.question-content {
padding: 1.25rem;
}
.content-text {
font-size: 0.95rem;
line-height: 1.7;
color: var(--text-primary);
margin: 0;
white-space: pre-wrap;
}
.options-grid {
padding: 0 1.25rem 1.25rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.option-item {
display: flex;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 10px;
}
.option-label {
font-weight: 600;
color: #10b981;
flex-shrink: 0;
}
.option-text {
color: var(--text-primary);
font-size: 0.9rem;
}
.answer-section {
padding: 1rem 1.25rem;
background: rgba(16, 185, 129, 0.06);
border-top: 1px solid var(--card-border);
border-bottom: 1px solid var(--card-border);
}
.answer-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.answer-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-secondary);
}
.answer-value {
font-size: 0.95rem;
font-weight: 600;
color: #10b981;
}
.explanation-section {
padding: 1.25rem;
}
.explanation-header {
display: flex;
align-items: center;
gap: 0.5rem;
color: #10b981;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.explanation-text {
font-size: 0.9rem;
line-height: 1.7;
color: var(--text-secondary);
margin: 0;
}
.knowledge-section {
padding: 0.875rem 1.25rem;
background: rgba(255, 255, 255, 0.02);
border-top: 1px solid var(--card-border);
}
.knowledge-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
}
.knowledge-value {
font-size: 0.85rem;
color: var(--text-primary);
}
/* Loading Card */
.loading-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
background: rgba(255, 255, 255, 0.02);
border: 1px dashed rgba(16, 185, 129, 0.3);
border-radius: 16px;
}
.loading-dots {
display: flex;
gap: 8px;
margin-bottom: 1rem;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(16, 185, 129, 0.4);
animation: bounce 1.2s ease-in-out infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
.loading-text {
font-size: 0.9rem;
color: var(--text-secondary);
margin: 0;
}
/* Animations */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.75);
}
}
@keyframes bounce {
0%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-6px);
}
}
@media (max-width: 768px) {
.main-layout {
flex-direction: column;
height: auto;
overflow: visible;
}
.left-panel {
width: 100%;
max-width: 100%;
border-right: none;
border-bottom: 1px solid var(--card-border);
overflow-y: visible;
}
.right-panel {
padding: 1.25rem 1rem;
}
.options-grid {
grid-template-columns: 1fr;
}
}
</style>