refactor_exam_workshop.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. import re
  2. import os
  3. filepath = '/Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/ExamWorkshop.vue'
  4. with open(filepath, 'r', encoding='utf-8') as f:
  5. content = f.read()
  6. parts = content.split('<script setup>')
  7. template_part = parts[0]
  8. rest = '<script setup>' + parts[1]
  9. script_match = re.search(r'<script setup>([\s\S]*?)</script>', rest)
  10. script_content_original = script_match.group(1)
  11. script_content = script_content_original
  12. # Add questionBasis
  13. if 'const questionBasis = ref("");' not in script_content:
  14. script_content = script_content.replace(
  15. 'const examName = ref("桥梁工程施工技术考核");', 'const examName = ref("");\nconst questionBasis = ref("");')
  16. # Modify questionTypes
  17. old_qtypes = """const questionTypes = ref([
  18. { name: "单选题", scorePerQuestion: 5, questionCount: 5, romanNumeral: "一" },
  19. { name: "判断题", scorePerQuestion: 3, questionCount: 5, romanNumeral: "二" },
  20. { name: "多选题", scorePerQuestion: 8, questionCount: 5, romanNumeral: "三" },
  21. { name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四" },
  22. ]);"""
  23. new_qtypes = """const questionTypes = ref([
  24. { key: 'single', name: "单选题", scorePerQuestion: 2, questionCount: 15, romanNumeral: "一", max: 50 },
  25. { key: 'judge', name: "判断题", scorePerQuestion: 2, questionCount: 10, romanNumeral: "二", max: 50 },
  26. { key: 'multiple', name: "多选题", scorePerQuestion: 3, questionCount: 10, romanNumeral: "三", max: 50 },
  27. { key: 'short', name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四", max: 10 },
  28. ]);"""
  29. script_content = script_content.replace(old_qtypes, new_qtypes)
  30. # Modify totalScore to be computed
  31. old_total_score = 'const totalScore = ref(100);'
  32. new_total_score = '''const totalScore = computed(() => {
  33. return questionTypes.value.reduce((total, type) => {
  34. return total + (type.scorePerQuestion * type.questionCount);
  35. }, 0);
  36. });'''
  37. script_content = script_content.replace(old_total_score, new_total_score)
  38. # Replace projectType usage in fetchExamPrompt
  39. script_content = script_content.replace(
  40. "projectType: projectTypes[selectedProjectType.value]?.name || '',", "projectType: questionBasis.value,")
  41. # Clear config logic
  42. script_content = script_content.replace("const clearSettings = () => {", """const clearSettings = () => {
  43. examName.value = '';
  44. questionBasis.value = '';
  45. removeSelectedFile();
  46. questionTypes.value.forEach(t => t.questionCount = 0);
  47. """)
  48. # 2. Build new template
  49. new_template = """<template>
  50. <div class="chat-container">
  51. <!-- 最左侧边栏 -->
  52. <Sidebar v-if="!hideSidebar" />
  53. <!-- 中间历史记录区域 -->
  54. <div class="history-sidebar" v-if="!hideSidebar">
  55. <div class="history-header">
  56. <span class="section-title">历史记录</span>
  57. <img src="@/assets/Chat/2.png" alt="新建任务" class="new-chat-btn" @click="createNewChat" />
  58. </div>
  59. <div class="history-list">
  60. <div v-if="isLoadingHistory && historyTotal === 0" class="history-loading">
  61. <div class="loading-spinner"></div>
  62. <div class="loading-text">正在加载历史记录...</div>
  63. </div>
  64. <div v-else-if="historyTotal > 0" v-for="(item, index) in historyData" :key="index"
  65. :class="['history-item', { active: item.isActive }]"
  66. @click="item.isActive ? null : (isGenerating || isLoadingHistoryItem ? null : handleHistoryItem(item))"
  67. :style="{ cursor: item.isActive ? 'default' : (isGenerating || isLoadingHistoryItem ? 'not-allowed' : 'pointer'), opacity: item.isActive ? '0.8' : '1' }">
  68. <div class="history-content">
  69. <div class="history-title">{{ item.title }}</div>
  70. <div class="history-time">{{ item.time }}</div>
  71. </div>
  72. <div class="delete-btn" @click.stop="deleteHistoryItem(item, index)" :class="{ 'always-visible': item.isActive }">
  73. <img src="/src/assets/AIWriting/8.png" alt="删除" class="delete-icon" />
  74. </div>
  75. </div>
  76. <div v-else class="empty-history">
  77. <img src="@/assets/Chat/22.png" alt="暂无数据" class="empty-icon">
  78. <div class="empty-text">暂无数据</div>
  79. </div>
  80. </div>
  81. </div>
  82. <!-- MAIN WORK AREA -->
  83. <main class="flex-1 bg-surface-container-lowest min-h-screen flex flex-col relative w-full overflow-hidden" style="background-color: #f7f9fb;">
  84. <!-- EXAM CONFIGURATION UI (When not showing detail) -->
  85. <div v-if="!showExamDetail" class="flex-1 flex w-full h-full relative">
  86. <!-- Editor Pane -->
  87. <div class="flex-1 overflow-y-auto p-10 max-w-5xl mx-auto w-full space-y-8 pb-32" style="margin-right: 214px;">
  88. <!-- Exam Name Input -->
  89. <section class="space-y-2">
  90. <label class="block text-sm font-bold text-on-surface-variant mb-2">试卷名称</label>
  91. <div class="relative">
  92. <input v-model="examName" class="w-full bg-white border border-gray-200 rounded-xl px-4 py-3 text-lg font-medium focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none" maxlength="32" placeholder="请输入试卷名称..." type="text" :disabled="isGenerating"/>
  93. <span class="absolute right-4 bottom-3 text-xs text-gray-400">{{ examName.length }}/32</span>
  94. </div>
  95. </section>
  96. <!-- Question Basis -->
  97. <section class="space-y-4 mt-8">
  98. <div class="flex items-center justify-between mb-2">
  99. <label class="block text-sm font-bold text-on-surface-variant">出题依据内容</label>
  100. </div>
  101. <textarea v-model="questionBasis" class="w-full h-48 bg-white border border-gray-200 rounded-xl px-4 py-3 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none resize-none" placeholder="在此输入知识点、章节或培训内容..." :disabled="isGenerating || selectedFile"></textarea>
  102. <!-- Smart Fill Module -->
  103. <div class="w-full bg-white border border-gray-200 rounded-xl p-6 flex items-center gap-6 cursor-pointer hover:bg-gray-50 transition-colors group mt-4" @click="!isGenerating && !selectedFile ? triggerFileUpload() : null">
  104. <div class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center text-gray-500 group-hover:text-blue-500 transition-colors">
  105. <span class="material-symbols-outlined text-3xl">cloud_upload</span>
  106. </div>
  107. <div class="flex-1">
  108. <div class="flex items-center gap-2">
  109. <h3 class="text-base font-bold text-gray-900">从PPT生成考题</h3>
  110. </div>
  111. <p class="text-sm text-gray-500 mt-1">上传培训PPT,智能提取关键内容生成考题(单个文件可上传20M内)</p>
  112. <div v-if="selectedFile" class="mt-2 p-2 bg-blue-50 rounded text-blue-600 text-sm flex justify-between items-center">
  113. <span>已上传: {{ selectedFile.name }}</span>
  114. <span @click.stop="removeSelectedFile" class="cursor-pointer text-red-500 font-bold px-2">×</span>
  115. </div>
  116. </div>
  117. <span class="material-symbols-outlined text-gray-300">chevron_right</span>
  118. </div>
  119. </section>
  120. <!-- Configuration Sliders -->
  121. <section class="space-y-4 mt-8">
  122. <h2 class="text-gray-600 mb-4">
  123. <div class="flex items-center justify-between w-full">
  124. <span class="font-bold">题型配置</span>
  125. <div class="flex items-center gap-4">
  126. <span class="text-sm font-normal text-gray-500">试卷总分</span>
  127. <div class="bg-white border border-gray-200 rounded-lg px-6 py-2 text-lg font-bold text-gray-900 min-w-[80px] text-center">
  128. {{ totalScore }}
  129. </div>
  130. </div>
  131. </div>
  132. </h2>
  133. <div class="grid-2-cols">
  134. <div v-for="(type, index) in questionTypes" :key="index" class="bg-white rounded-2xl border border-gray-200 shadow-sm p-4">
  135. <div class="flex justify-between items-center mb-4">
  136. <span class="text-sm font-bold text-gray-800">{{ type.name }}</span>
  137. <span class="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded-full">每题 {{ type.scorePerQuestion }} 分</span>
  138. </div>
  139. <div class="space-y-2">
  140. <div class="flex justify-between items-center text-xs font-bold mb-2">
  141. <span class="text-gray-500">数量</span>
  142. <span class="text-blue-600 text-sm">{{ type.questionCount }}</span>
  143. </div>
  144. <div class="custom-slider-container">
  145. <input type="range" v-model.number="type.questionCount" min="0" :max="type.max" class="custom-slider" :disabled="isGenerating">
  146. <div class="slider-track">
  147. <div class="slider-fill" :style="{ width: (type.questionCount / type.max * 100) + '%' }"></div>
  148. </div>
  149. </div>
  150. </div>
  151. </div>
  152. </div>
  153. </section>
  154. </div>
  155. <!-- Footer Action Bar -->
  156. <footer class="absolute bottom-0 left-0 w-full bg-white/90 backdrop-blur-md border-t border-gray-200 p-6 flex items-center justify-between z-40" style="padding-right: 238px;">
  157. <div class="flex items-center gap-6">
  158. <button class="flex items-center gap-2 text-gray-500 hover:text-red-500 transition-colors" @click="clearSettings" :disabled="isGenerating">
  159. <span class="material-symbols-outlined">delete_sweep</span>
  160. <span class="text-sm font-medium">清空当前配置</span>
  161. </button>
  162. <div class="h-8 w-px bg-gray-200"></div>
  163. </div>
  164. <button v-if="!isGenerating" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2.5 rounded-xl text-sm font-bold shadow-lg flex items-center gap-3 transition-colors" @click="generateAIExam">
  165. 开始智能生成试卷
  166. </button>
  167. <button v-else class="bg-blue-300 text-white px-6 py-2.5 rounded-xl text-sm font-bold shadow-lg flex items-center gap-3 cursor-not-allowed">
  168. 试卷生成中...
  169. </button>
  170. </footer>
  171. <!-- RIGHT SIDEBAR (The Intelligence Pane) -->
  172. <aside class="absolute right-0 top-0 w-[214px] bg-gray-50 border-l border-gray-200 flex flex-col h-full z-30">
  173. <div class="p-6">
  174. <h2 class="text-gray-900 font-bold text-lg mb-6">实时预览</h2>
  175. <div class="space-y-8">
  176. <div class="bg-white p-5 rounded-2xl shadow-sm border border-gray-100">
  177. <h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3">试卷名称</h3>
  178. <p class="text-gray-800 font-medium italic" :class="{'opacity-40': !examName}">{{ examName || '未命名试卷...' }}</p>
  179. </div>
  180. <div class="space-y-4">
  181. <h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider">结构大纲</h3>
  182. <ul class="space-y-4">
  183. <li v-for="(type, index) in questionTypes" :key="index" class="flex flex-col gap-1 group">
  184. <div class="flex items-center justify-between">
  185. <div class="flex items-center gap-2">
  186. <div class="w-1.5 h-1.5 rounded-full" :style="{ backgroundColor: ['#2563eb', '#3b82f6', '#60a5fa', '#93c5fd'][index % 4] }"></div>
  187. <span class="text-sm text-gray-800">{{ type.name }}</span>
  188. </div>
  189. <span class="text-xs font-bold text-gray-500">{{ type.questionCount }}题</span>
  190. </div>
  191. <span class="text-[10px] text-gray-400 ml-3.5">{{ type.questionCount * type.scorePerQuestion }} 分</span>
  192. </li>
  193. </ul>
  194. </div>
  195. <div class="pt-6 border-t border-gray-200 space-y-3">
  196. <div class="flex justify-between items-center">
  197. <span class="text-xs text-gray-500">试卷总分</span>
  198. <span class="text-xs font-bold text-gray-900">{{ totalScore }}</span>
  199. </div>
  200. </div>
  201. </div>
  202. </div>
  203. </aside>
  204. </div>
  205. <!-- EXAM COMPLETED STATE UI -->
  206. <div v-else-if="showExamDetail && !isGenerating" class="flex-1 flex flex-col w-full h-full bg-white relative">
  207. <!-- Exam Header -->
  208. <header class="bg-white px-8 py-6 border-b border-gray-100">
  209. <div class="max-w-5xl mx-auto">
  210. <div class="flex items-center justify-between mb-4">
  211. <h1 class="text-gray-900 text-lg font-normal">{{ currentExam?.title || examName }}</h1>
  212. <button class="flex items-center space-x-2 border border-green-500 text-green-500 px-4 py-1.5 rounded-lg text-sm hover:bg-green-50 transition-colors font-medium" @click="downloadWord">
  213. <span class="material-symbols-outlined text-[18px]">download</span>
  214. <span>下载Word</span>
  215. </button>
  216. </div>
  217. <div class="bg-gray-50 rounded-xl p-8 border border-gray-100">
  218. <div class="flex items-end justify-between">
  219. <div>
  220. <h2 class="font-bold text-gray-900 mb-6 text-2xl">{{ currentExam?.title || examName }}</h2>
  221. <div class="flex items-center space-x-8 text-[15px] text-gray-500">
  222. <span>总分: {{ currentExam?.totalScore || totalScore }}分</span>
  223. <span>题量: {{ currentExam?.totalQuestions || 0 }}题</span>
  224. </div>
  225. </div>
  226. <div class="text-[15px] text-gray-500">生成时间: {{ currentTime }}</div>
  227. </div>
  228. </div>
  229. </div>
  230. </header>
  231. <!-- Exam Content Area -->
  232. <div class="flex-1 overflow-y-auto p-10 space-y-8 pb-20">
  233. <div class="max-w-5xl mx-auto">
  234. <!-- Render Single Choice -->
  235. <div v-if="currentExam?.singleChoice?.questions?.length > 0" class="mb-8">
  236. <div class="flex items-center justify-between bg-gray-50 p-3 px-4 rounded-lg mb-4 cursor-pointer" @click="toggleSection('single')">
  237. <div class="flex items-center space-x-3">
  238. <div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white">
  239. <span class="material-symbols-outlined text-[16px]">{{ expandedSections.single ? 'remove' : 'add' }}</span>
  240. </div>
  241. <span class="font-bold text-gray-900">单选题</span>
  242. <span class="text-sm text-gray-500">(每题{{ currentExam.singleChoice.scorePerQuestion }}分, 共{{ currentExam.singleChoice.totalScore }}分)</span>
  243. </div>
  244. <div class="flex items-center space-x-2 text-gray-500">
  245. <span class="text-sm">{{ currentExam.singleChoice.count }}题</span>
  246. <span class="material-symbols-outlined">{{ expandedSections.single ? 'expand_less' : 'expand_more' }}</span>
  247. </div>
  248. </div>
  249. <div v-show="expandedSections.single" class="space-y-6">
  250. <div v-for="(q, qIndex) in currentExam.singleChoice.questions" :key="'single_'+qIndex" class="bg-white rounded-xl border border-gray-200 shadow-sm relative py-4 px-6 hover:border-blue-300 transition-colors">
  251. <h3 class="text-[15px] font-medium text-gray-900 leading-relaxed pr-8">
  252. <span class="text-blue-500 font-bold mr-2">{{ qIndex + 1 }}.</span>{{ q.text }}
  253. </h3>
  254. <div class="grid grid-cols-2 mt-3 gap-y-2">
  255. <label v-for="(opt, oIndex) in q.options" :key="oIndex" class="flex items-center space-x-3 cursor-pointer">
  256. <div class="w-4 h-4 rounded-full border flex items-center justify-center" :class="q.selectedAnswer === opt.key ? 'border-blue-500' : 'border-gray-300'">
  257. <div v-if="q.selectedAnswer === opt.key" class="w-2 h-2 bg-blue-500 rounded-full"></div>
  258. </div>
  259. <span class="text-sm" :class="q.selectedAnswer === opt.key ? 'text-blue-500 font-medium' : 'text-gray-600'">
  260. <span class="font-bold mr-2">{{ opt.key }}.</span>{{ opt.text }}
  261. </span>
  262. </label>
  263. </div>
  264. <div v-if="q.selectedAnswer" class="mt-4 p-3 bg-green-50 rounded-lg text-sm text-green-700">
  265. <span class="font-bold">正确答案:</span>{{ q.selectedAnswer }}
  266. </div>
  267. </div>
  268. </div>
  269. </div>
  270. <!-- Render Judge -->
  271. <div v-if="currentExam?.judge?.questions?.length > 0" class="mb-8">
  272. <div class="flex items-center justify-between bg-gray-50 p-3 px-4 rounded-lg mb-4 cursor-pointer" @click="toggleSection('judge')">
  273. <div class="flex items-center space-x-3">
  274. <div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white">
  275. <span class="material-symbols-outlined text-[16px]">{{ expandedSections.judge ? 'remove' : 'add' }}</span>
  276. </div>
  277. <span class="font-bold text-gray-900">判断题</span>
  278. <span class="text-sm text-gray-500">(每题{{ currentExam.judge.scorePerQuestion }}分, 共{{ currentExam.judge.totalScore }}分)</span>
  279. </div>
  280. <div class="flex items-center space-x-2 text-gray-500">
  281. <span class="text-sm">{{ currentExam.judge.count }}题</span>
  282. <span class="material-symbols-outlined">{{ expandedSections.judge ? 'expand_less' : 'expand_more' }}</span>
  283. </div>
  284. </div>
  285. <div v-show="expandedSections.judge" class="space-y-6">
  286. <div v-for="(q, qIndex) in currentExam.judge.questions" :key="'judge_'+qIndex" class="bg-white rounded-xl border border-gray-200 shadow-sm relative py-4 px-6 hover:border-blue-300 transition-colors">
  287. <h3 class="text-[15px] font-medium text-gray-900 leading-relaxed pr-8">
  288. <span class="text-blue-500 font-bold mr-2">{{ qIndex + 1 }}.</span>{{ q.text }}
  289. </h3>
  290. <div v-if="q.selectedAnswer" class="mt-4 p-3 bg-green-50 rounded-lg text-sm text-green-700">
  291. <span class="font-bold">正确答案:</span>{{ q.selectedAnswer }}
  292. </div>
  293. </div>
  294. </div>
  295. </div>
  296. <!-- Render Multiple -->
  297. <div v-if="currentExam?.multiple?.questions?.length > 0" class="mb-8">
  298. <div class="flex items-center justify-between bg-gray-50 p-3 px-4 rounded-lg mb-4 cursor-pointer" @click="toggleSection('multiple')">
  299. <div class="flex items-center space-x-3">
  300. <div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white">
  301. <span class="material-symbols-outlined text-[16px]">{{ expandedSections.multiple ? 'remove' : 'add' }}</span>
  302. </div>
  303. <span class="font-bold text-gray-900">多选题</span>
  304. <span class="text-sm text-gray-500">(每题{{ currentExam.multiple.scorePerQuestion }}分, 共{{ currentExam.multiple.totalScore }}分)</span>
  305. </div>
  306. <div class="flex items-center space-x-2 text-gray-500">
  307. <span class="text-sm">{{ currentExam.multiple.count }}题</span>
  308. <span class="material-symbols-outlined">{{ expandedSections.multiple ? 'expand_less' : 'expand_more' }}</span>
  309. </div>
  310. </div>
  311. <div v-show="expandedSections.multiple" class="space-y-6">
  312. <div v-for="(q, qIndex) in currentExam.multiple.questions" :key="'multiple_'+qIndex" class="bg-white rounded-xl border border-gray-200 shadow-sm relative py-4 px-6 hover:border-blue-300 transition-colors">
  313. <h3 class="text-[15px] font-medium text-gray-900 leading-relaxed pr-8">
  314. <span class="text-blue-500 font-bold mr-2">{{ qIndex + 1 }}.</span>{{ q.text }}
  315. </h3>
  316. <div class="grid grid-cols-2 mt-3 gap-y-2">
  317. <label v-for="(opt, oIndex) in q.options" :key="oIndex" class="flex items-center space-x-3 cursor-pointer">
  318. <div class="w-4 h-4 rounded border flex items-center justify-center" :class="q.selectedAnswers?.includes(opt.key) ? 'border-blue-500 bg-blue-500 text-white' : 'border-gray-300'">
  319. <span v-if="q.selectedAnswers?.includes(opt.key)" class="material-symbols-outlined text-[12px]">check</span>
  320. </div>
  321. <span class="text-sm" :class="q.selectedAnswers?.includes(opt.key) ? 'text-blue-500 font-medium' : 'text-gray-600'">
  322. <span class="font-bold mr-2">{{ opt.key }}.</span>{{ opt.text }}
  323. </span>
  324. </label>
  325. </div>
  326. <div v-if="q.selectedAnswers?.length > 0" class="mt-4 p-3 bg-green-50 rounded-lg text-sm text-green-700">
  327. <span class="font-bold">正确答案:</span>{{ q.selectedAnswers.join(', ') }}
  328. </div>
  329. </div>
  330. </div>
  331. </div>
  332. <!-- Render Short Answer -->
  333. <div v-if="currentExam?.short?.questions?.length > 0" class="mb-8">
  334. <div class="flex items-center justify-between bg-gray-50 p-3 px-4 rounded-lg mb-4 cursor-pointer" @click="toggleSection('short')">
  335. <div class="flex items-center space-x-3">
  336. <div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white">
  337. <span class="material-symbols-outlined text-[16px]">{{ expandedSections.short ? 'remove' : 'add' }}</span>
  338. </div>
  339. <span class="font-bold text-gray-900">简答题</span>
  340. <span class="text-sm text-gray-500">(每题{{ currentExam.short.scorePerQuestion }}分, 共{{ currentExam.short.totalScore }}分)</span>
  341. </div>
  342. <div class="flex items-center space-x-2 text-gray-500">
  343. <span class="text-sm">{{ currentExam.short.count }}题</span>
  344. <span class="material-symbols-outlined">{{ expandedSections.short ? 'expand_less' : 'expand_more' }}</span>
  345. </div>
  346. </div>
  347. <div v-show="expandedSections.short" class="space-y-6">
  348. <div v-for="(q, qIndex) in currentExam.short.questions" :key="'short_'+qIndex" class="bg-white rounded-xl border border-gray-200 shadow-sm relative py-4 px-6 hover:border-blue-300 transition-colors">
  349. <h3 class="text-[15px] font-medium text-gray-900 leading-relaxed pr-8">
  350. <span class="text-blue-500 font-bold mr-2">{{ qIndex + 1 }}.</span>{{ q.text }}
  351. </h3>
  352. <div v-if="q.outline?.keyFactors" class="mt-4 p-4 bg-green-50 rounded-lg text-sm text-green-800 leading-relaxed border border-green-100">
  353. <div class="font-bold mb-2 flex items-center gap-2"><span class="material-symbols-outlined text-[18px]">lightbulb</span>答题要点:</div>
  354. <div v-html="q.outline.keyFactors"></div>
  355. </div>
  356. </div>
  357. </div>
  358. </div>
  359. </div>
  360. </div>
  361. </div>
  362. </main>
  363. <!-- 隐藏的文件输入框 -->
  364. <input ref="fileInput" type="file" accept=".ppt,.pptx" style="display: none" @change="handleFileSelect" />
  365. <!-- 删除确认弹窗 -->
  366. <DeleteConfirmModal :visible="showDeleteModal" title="删除历史记录" :message="deleteConfirmMessage" @confirm="confirmDeleteHistory" @cancel="cancelDeleteHistory" @close="cancelDeleteHistory" />
  367. </div>
  368. </template>\n"""
  369. # Replace script
  370. content = content.replace(script_content_original, script_content)
  371. # We rebuild the content from parts using the new template.
  372. # But since we have `rest`, let's do this:
  373. # 1. Update `<script setup>` block
  374. new_rest = rest.replace(script_content_original, script_content)
  375. # 2. Append styles
  376. css = """
  377. <style scoped>
  378. .chat-container {
  379. display: flex;
  380. height: 100vh;
  381. width: 100%;
  382. overflow: hidden;
  383. background-color: #f7f9fb;
  384. }
  385. /* Material Icons font is usually loaded globally, but if missing, fallback to text */
  386. @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap');
  387. .material-symbols-outlined {
  388. font-family: 'Material Symbols Outlined';
  389. font-weight: normal;
  390. font-style: normal;
  391. font-size: 24px;
  392. display: inline-block;
  393. line-height: 1;
  394. text-transform: none;
  395. letter-spacing: normal;
  396. word-wrap: normal;
  397. white-space: nowrap;
  398. direction: ltr;
  399. }
  400. /* Base tailwind classes simulation */
  401. .flex { display: flex; }
  402. .flex-1 { flex: 1 1 0%; }
  403. .flex-col { flex-direction: column; }
  404. .items-center { align-items: center; }
  405. .justify-between { justify-content: space-between; }
  406. .justify-center { justify-content: center; }
  407. .relative { position: relative; }
  408. .absolute { position: absolute; }
  409. .w-full { width: 100%; }
  410. .h-full { height: 100%; }
  411. .h-screen { height: 100vh; }
  412. .overflow-hidden { overflow: hidden; }
  413. .overflow-y-auto { overflow-y: auto; }
  414. .grid-2-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
  415. .space-x-2 > * + * { margin-left: 0.5rem; }
  416. .space-x-3 > * + * { margin-left: 0.75rem; }
  417. .space-x-8 > * + * { margin-left: 2rem; }
  418. .space-y-2 > * + * { margin-top: 0.5rem; }
  419. .space-y-3 > * + * { margin-top: 0.75rem; }
  420. .space-y-4 > * + * { margin-top: 1rem; }
  421. .space-y-6 > * + * { margin-top: 1.5rem; }
  422. .space-y-8 > * + * { margin-top: 2rem; }
  423. .gap-1 { gap: 0.25rem; }
  424. .gap-2 { gap: 0.5rem; }
  425. .gap-3 { gap: 0.75rem; }
  426. .gap-4 { gap: 1rem; }
  427. .gap-6 { gap: 1.5rem; }
  428. .gap-y-2 { row-gap: 0.5rem; }
  429. /* Tailwind Utilities */
  430. .bg-surface-container-lowest { background-color: #ffffff; }
  431. .bg-white { background-color: #ffffff; }
  432. .bg-gray-50 { background-color: #f9fafb; }
  433. .bg-gray-100 { background-color: #f3f4f6; }
  434. .bg-gray-200 { background-color: #e5e7eb; }
  435. .bg-blue-50 { background-color: #eff6ff; }
  436. .bg-blue-300 { background-color: #93c5fd; }
  437. .bg-blue-500 { background-color: #3b82f6; }
  438. .bg-blue-600 { background-color: #2563eb; }
  439. .bg-blue-700 { background-color: #1d4ed8; }
  440. .bg-green-50 { background-color: #f0fdf4; }
  441. .text-white { color: #ffffff; }
  442. .text-gray-300 { color: #d1d5db; }
  443. .text-gray-400 { color: #9ca3af; }
  444. .text-gray-500 { color: #6b7280; }
  445. .text-gray-600 { color: #4b5563; }
  446. .text-gray-800 { color: #1f2937; }
  447. .text-gray-900 { color: #111827; }
  448. .text-blue-500 { color: #3b82f6; }
  449. .text-blue-600 { color: #2563eb; }
  450. .text-red-500 { color: #ef4444; }
  451. .text-green-500 { color: #22c55e; }
  452. .text-green-700 { color: #15803d; }
  453. .text-green-800 { color: #166534; }
  454. .text-on-surface-variant { color: #424753; }
  455. .border { border-width: 1px; }
  456. .border-t { border-top-width: 1px; }
  457. .border-l { border-left-width: 1px; }
  458. .border-gray-100 { border-color: #f3f4f6; }
  459. .border-gray-200 { border-color: #e5e7eb; }
  460. .border-gray-300 { border-color: #d1d5db; }
  461. .border-blue-300 { border-color: #93c5fd; }
  462. .border-blue-500 { border-color: #3b82f6; }
  463. .border-green-100 { border-color: #dcfce3; }
  464. .border-green-500 { border-color: #22c55e; }
  465. .rounded { border-radius: 0.25rem; }
  466. .rounded-lg { border-radius: 0.5rem; }
  467. .rounded-xl { border-radius: 0.75rem; }
  468. .rounded-2xl { border-radius: 1rem; }
  469. .rounded-full { border-radius: 9999px; }
  470. .shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); }
  471. .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
  472. .p-2 { padding: 0.5rem; }
  473. .p-3 { padding: 0.75rem; }
  474. .p-4 { padding: 1rem; }
  475. .p-5 { padding: 1.25rem; }
  476. .p-6 { padding: 1.5rem; }
  477. .p-8 { padding: 2rem; }
  478. .p-10 { padding: 2.5rem; }
  479. .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
  480. .px-4 { padding-left: 1rem; padding-right: 1rem; }
  481. .px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
  482. .px-8 { padding-left: 2rem; padding-right: 2rem; }
  483. .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
  484. .py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
  485. .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
  486. .py-2\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
  487. .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
  488. .py-4 { padding-top: 1rem; padding-bottom: 1rem; }
  489. .py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
  490. .pt-6 { padding-top: 1.5rem; }
  491. .pt-8 { padding-top: 2rem; }
  492. .pb-20 { padding-bottom: 5rem; }
  493. .pb-32 { padding-bottom: 8rem; }
  494. .mb-2 { margin-bottom: 0.5rem; }
  495. .mb-3 { margin-bottom: 0.75rem; }
  496. .mb-4 { margin-bottom: 1rem; }
  497. .mb-6 { margin-bottom: 1.5rem; }
  498. .mb-8 { margin-bottom: 2rem; }
  499. .mt-1 { margin-top: 0.25rem; }
  500. .mt-2 { margin-top: 0.5rem; }
  501. .mt-3 { margin-top: 0.75rem; }
  502. .mt-4 { margin-top: 1rem; }
  503. .mt-8 { margin-top: 2rem; }
  504. .ml-3\.5 { margin-left: 0.875rem; }
  505. .mr-2 { margin-right: 0.5rem; }
  506. .w-1\.5 { width: 0.375rem; }
  507. .w-2 { width: 0.5rem; }
  508. .w-4 { width: 1rem; }
  509. .w-6 { width: 1.5rem; }
  510. .w-12 { width: 3rem; }
  511. .w-\[214px\] { width: 214px; }
  512. .h-1\.5 { height: 0.375rem; }
  513. .h-2 { height: 0.5rem; }
  514. .h-4 { height: 1rem; }
  515. .h-6 { height: 1.5rem; }
  516. .h-8 { height: 2rem; }
  517. .h-12 { height: 3rem; }
  518. .h-48 { height: 12rem; }
  519. .min-h-screen { min-height: 100vh; }
  520. .min-w-\[80px\] { min-width: 80px; }
  521. .max-w-5xl { max-width: 64rem; }
  522. .mx-auto { margin-left: auto; margin-right: auto; }
  523. .text-\[10px\] { font-size: 10px; }
  524. .text-xs { font-size: 0.75rem; line-height: 1rem; }
  525. .text-sm { font-size: 0.875rem; line-height: 1.25rem; }
  526. .text-\[15px\] { font-size: 15px; }
  527. .text-base { font-size: 1rem; line-height: 1.5rem; }
  528. .text-lg { font-size: 1.125rem; line-height: 1.75rem; }
  529. .text-2xl { font-size: 1.5rem; line-height: 2rem; }
  530. .text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
  531. .font-normal { font-weight: 400; }
  532. .font-medium { font-weight: 500; }
  533. .font-bold { font-weight: 700; }
  534. .italic { font-style: italic; }
  535. .uppercase { text-transform: uppercase; }
  536. .tracking-wider { letter-spacing: 0.05em; }
  537. .leading-relaxed { line-height: 1.625; }
  538. .text-center { text-align: center; }
  539. .cursor-pointer { cursor: pointer; }
  540. .cursor-not-allowed { cursor: not-allowed; }
  541. .transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
  542. .transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
  543. .outline-none { outline: 2px solid transparent; outline-offset: 2px; }
  544. .resize-none { resize: none; }
  545. .z-30 { z-index: 30; }
  546. .z-40 { z-index: 40; }
  547. .opacity-40 { opacity: 0.4; }
  548. .opacity-60 { opacity: 0.6; }
  549. .backdrop-blur-md { backdrop-filter: blur(12px); }
  550. /* Hover states */
  551. .hover\:bg-gray-50:hover { background-color: #f9fafb; }
  552. .hover\:bg-blue-700:hover { background-color: #1d4ed8; }
  553. .hover\:bg-green-50:hover { background-color: #f0fdf4; }
  554. .hover\:border-blue-300:hover { border-color: #93c5fd; }
  555. .hover\:text-red-500:hover { color: #ef4444; }
  556. /* Group hover */
  557. .group:hover .group-hover\:text-blue-500 { color: #3b82f6; }
  558. /* Focus states */
  559. .focus\:border-blue-500:focus { border-color: #3b82f6; }
  560. .focus\:ring-2:focus { box-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); }
  561. .focus\:ring-blue-100:focus { --tw-ring-color: #dbeafe; }
  562. /* Custom Slider */
  563. .custom-slider-container {
  564. position: relative;
  565. height: 4px;
  566. width: 100%;
  567. background-color: #e0e3e5;
  568. border-radius: 9999px;
  569. margin-top: 12px;
  570. margin-bottom: 8px;
  571. }
  572. .slider-track {
  573. position: absolute;
  574. top: 0;
  575. left: 0;
  576. height: 100%;
  577. width: 100%;
  578. border-radius: 9999px;
  579. pointer-events: none;
  580. }
  581. .slider-fill {
  582. height: 100%;
  583. background-color: #0058bd;
  584. border-radius: 9999px;
  585. }
  586. .custom-slider {
  587. position: absolute;
  588. top: -8px;
  589. left: 0;
  590. width: 100%;
  591. height: 20px;
  592. opacity: 0;
  593. cursor: pointer;
  594. z-index: 10;
  595. }
  596. .custom-slider::-webkit-slider-thumb {
  597. width: 16px;
  598. height: 16px;
  599. appearance: none;
  600. }
  601. /* Thumb indicator (visual only) */
  602. .custom-slider-container::after {
  603. content: '';
  604. position: absolute;
  605. top: 50%;
  606. transform: translateY(-50%);
  607. width: 16px;
  608. height: 16px;
  609. background-color: #0058bd;
  610. border: 2px solid white;
  611. border-radius: 50%;
  612. box-shadow: 0 1px 3px rgba(0,0,0,0.3);
  613. pointer-events: none;
  614. left: var(--thumb-pos, 0%);
  615. margin-left: -8px;
  616. }
  617. /* History list (from original) */
  618. .history-sidebar {
  619. width: 260px;
  620. background: #fff;
  621. border-right: 1px solid #e2e8f0;
  622. display: flex;
  623. flex-direction: column;
  624. }
  625. .history-header {
  626. padding: 20px;
  627. display: flex;
  628. justify-content: space-between;
  629. align-items: center;
  630. border-bottom: 1px solid #f1f5f9;
  631. }
  632. .section-title {
  633. font-size: 16px;
  634. font-weight: 600;
  635. color: #1e293b;
  636. }
  637. .new-chat-btn {
  638. width: 24px;
  639. height: 24px;
  640. cursor: pointer;
  641. transition: transform 0.2s;
  642. }
  643. .new-chat-btn:hover {
  644. transform: scale(1.1);
  645. }
  646. .history-list {
  647. flex: 1;
  648. overflow-y: auto;
  649. padding: 12px;
  650. }
  651. .history-item {
  652. padding: 12px;
  653. border-radius: 8px;
  654. margin-bottom: 8px;
  655. cursor: pointer;
  656. transition: all 0.2s;
  657. display: flex;
  658. justify-content: space-between;
  659. align-items: center;
  660. }
  661. .history-item:hover {
  662. background: #f8fafc;
  663. }
  664. .history-item.active {
  665. background: #eff6ff;
  666. }
  667. .history-content {
  668. flex: 1;
  669. overflow: hidden;
  670. }
  671. .history-title {
  672. font-size: 14px;
  673. color: #334155;
  674. margin-bottom: 4px;
  675. white-space: nowrap;
  676. overflow: hidden;
  677. text-overflow: ellipsis;
  678. }
  679. .history-time {
  680. font-size: 12px;
  681. color: #94a3b8;
  682. }
  683. .delete-btn {
  684. padding: 4px;
  685. opacity: 0;
  686. transition: opacity 0.2s;
  687. }
  688. .history-item:hover .delete-btn, .delete-btn.always-visible {
  689. opacity: 1;
  690. }
  691. .delete-icon {
  692. width: 16px;
  693. height: 16px;
  694. }
  695. .empty-history {
  696. text-align: center;
  697. padding: 40px 0;
  698. }
  699. .empty-icon {
  700. width: 64px;
  701. margin-bottom: 12px;
  702. }
  703. .empty-text {
  704. color: #94a3b8;
  705. font-size: 14px;
  706. }
  707. .history-loading {
  708. display: flex;
  709. flex-direction: column;
  710. align-items: center;
  711. justify-content: center;
  712. padding: 40px 0;
  713. color: #64748b;
  714. font-size: 14px;
  715. }
  716. .loading-spinner {
  717. width: 24px;
  718. height: 24px;
  719. border: 2px solid #e2e8f0;
  720. border-top-color: #3b82f6;
  721. border-radius: 50%;
  722. animation: spin 1s linear infinite;
  723. margin-bottom: 12px;
  724. }
  725. @keyframes spin {
  726. to { transform: rotate(360deg); }
  727. }
  728. </style>
  729. """
  730. # Strip old styles
  731. new_rest_without_style = re.sub(r'<style[\s\S]*?</style>', '', new_rest)
  732. final_content = new_template + "\n" + new_rest_without_style + "\n" + css
  733. with open(filepath, 'w', encoding='utf-8') as f:
  734. f.write(final_content)