| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782 |
- import re
- import os
- filepath = '/Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/ExamWorkshop.vue'
- with open(filepath, 'r', encoding='utf-8') as f:
- content = f.read()
- parts = content.split('<script setup>')
- template_part = parts[0]
- rest = '<script setup>' + parts[1]
- script_match = re.search(r'<script setup>([\s\S]*?)</script>', rest)
- script_content_original = script_match.group(1)
- script_content = script_content_original
- # Add questionBasis
- if 'const questionBasis = ref("");' not in script_content:
- script_content = script_content.replace(
- 'const examName = ref("桥梁工程施工技术考核");', 'const examName = ref("");\nconst questionBasis = ref("");')
- # Modify questionTypes
- old_qtypes = """const questionTypes = ref([
- { name: "单选题", scorePerQuestion: 5, questionCount: 5, romanNumeral: "一" },
- { name: "判断题", scorePerQuestion: 3, questionCount: 5, romanNumeral: "二" },
- { name: "多选题", scorePerQuestion: 8, questionCount: 5, romanNumeral: "三" },
- { name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四" },
- ]);"""
- new_qtypes = """const questionTypes = ref([
- { key: 'single', name: "单选题", scorePerQuestion: 2, questionCount: 15, romanNumeral: "一", max: 50 },
- { key: 'judge', name: "判断题", scorePerQuestion: 2, questionCount: 10, romanNumeral: "二", max: 50 },
- { key: 'multiple', name: "多选题", scorePerQuestion: 3, questionCount: 10, romanNumeral: "三", max: 50 },
- { key: 'short', name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四", max: 10 },
- ]);"""
- script_content = script_content.replace(old_qtypes, new_qtypes)
- # Modify totalScore to be computed
- old_total_score = 'const totalScore = ref(100);'
- new_total_score = '''const totalScore = computed(() => {
- return questionTypes.value.reduce((total, type) => {
- return total + (type.scorePerQuestion * type.questionCount);
- }, 0);
- });'''
- script_content = script_content.replace(old_total_score, new_total_score)
- # Replace projectType usage in fetchExamPrompt
- script_content = script_content.replace(
- "projectType: projectTypes[selectedProjectType.value]?.name || '',", "projectType: questionBasis.value,")
- # Clear config logic
- script_content = script_content.replace("const clearSettings = () => {", """const clearSettings = () => {
- examName.value = '';
- questionBasis.value = '';
- removeSelectedFile();
- questionTypes.value.forEach(t => t.questionCount = 0);
- """)
- # 2. Build new template
- new_template = """<template>
- <div class="chat-container">
- <!-- 最左侧边栏 -->
- <Sidebar v-if="!hideSidebar" />
- <!-- 中间历史记录区域 -->
- <div class="history-sidebar" v-if="!hideSidebar">
- <div class="history-header">
- <span class="section-title">历史记录</span>
- <img src="@/assets/Chat/2.png" alt="新建任务" class="new-chat-btn" @click="createNewChat" />
- </div>
-
- <div class="history-list">
- <div v-if="isLoadingHistory && historyTotal === 0" class="history-loading">
- <div class="loading-spinner"></div>
- <div class="loading-text">正在加载历史记录...</div>
- </div>
-
- <div v-else-if="historyTotal > 0" v-for="(item, index) in historyData" :key="index"
- :class="['history-item', { active: item.isActive }]"
- @click="item.isActive ? null : (isGenerating || isLoadingHistoryItem ? null : handleHistoryItem(item))"
- :style="{ cursor: item.isActive ? 'default' : (isGenerating || isLoadingHistoryItem ? 'not-allowed' : 'pointer'), opacity: item.isActive ? '0.8' : '1' }">
- <div class="history-content">
- <div class="history-title">{{ item.title }}</div>
- <div class="history-time">{{ item.time }}</div>
- </div>
- <div class="delete-btn" @click.stop="deleteHistoryItem(item, index)" :class="{ 'always-visible': item.isActive }">
- <img src="/src/assets/AIWriting/8.png" alt="删除" class="delete-icon" />
- </div>
- </div>
-
- <div v-else class="empty-history">
- <img src="@/assets/Chat/22.png" alt="暂无数据" class="empty-icon">
- <div class="empty-text">暂无数据</div>
- </div>
- </div>
- </div>
- <!-- MAIN WORK AREA -->
- <main class="flex-1 bg-surface-container-lowest min-h-screen flex flex-col relative w-full overflow-hidden" style="background-color: #f7f9fb;">
-
- <!-- EXAM CONFIGURATION UI (When not showing detail) -->
- <div v-if="!showExamDetail" class="flex-1 flex w-full h-full relative">
- <!-- Editor Pane -->
- <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;">
- <!-- Exam Name Input -->
- <section class="space-y-2">
- <label class="block text-sm font-bold text-on-surface-variant mb-2">试卷名称</label>
- <div class="relative">
- <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"/>
- <span class="absolute right-4 bottom-3 text-xs text-gray-400">{{ examName.length }}/32</span>
- </div>
- </section>
- <!-- Question Basis -->
- <section class="space-y-4 mt-8">
- <div class="flex items-center justify-between mb-2">
- <label class="block text-sm font-bold text-on-surface-variant">出题依据内容</label>
- </div>
- <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>
-
- <!-- Smart Fill Module -->
- <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">
- <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">
- <span class="material-symbols-outlined text-3xl">cloud_upload</span>
- </div>
- <div class="flex-1">
- <div class="flex items-center gap-2">
- <h3 class="text-base font-bold text-gray-900">从PPT生成考题</h3>
- </div>
- <p class="text-sm text-gray-500 mt-1">上传培训PPT,智能提取关键内容生成考题(单个文件可上传20M内)</p>
- <div v-if="selectedFile" class="mt-2 p-2 bg-blue-50 rounded text-blue-600 text-sm flex justify-between items-center">
- <span>已上传: {{ selectedFile.name }}</span>
- <span @click.stop="removeSelectedFile" class="cursor-pointer text-red-500 font-bold px-2">×</span>
- </div>
- </div>
- <span class="material-symbols-outlined text-gray-300">chevron_right</span>
- </div>
- </section>
- <!-- Configuration Sliders -->
- <section class="space-y-4 mt-8">
- <h2 class="text-gray-600 mb-4">
- <div class="flex items-center justify-between w-full">
- <span class="font-bold">题型配置</span>
- <div class="flex items-center gap-4">
- <span class="text-sm font-normal text-gray-500">试卷总分</span>
- <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">
- {{ totalScore }}
- </div>
- </div>
- </div>
- </h2>
-
- <div class="grid-2-cols">
- <div v-for="(type, index) in questionTypes" :key="index" class="bg-white rounded-2xl border border-gray-200 shadow-sm p-4">
- <div class="flex justify-between items-center mb-4">
- <span class="text-sm font-bold text-gray-800">{{ type.name }}</span>
- <span class="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded-full">每题 {{ type.scorePerQuestion }} 分</span>
- </div>
- <div class="space-y-2">
- <div class="flex justify-between items-center text-xs font-bold mb-2">
- <span class="text-gray-500">数量</span>
- <span class="text-blue-600 text-sm">{{ type.questionCount }}</span>
- </div>
- <div class="custom-slider-container">
- <input type="range" v-model.number="type.questionCount" min="0" :max="type.max" class="custom-slider" :disabled="isGenerating">
- <div class="slider-track">
- <div class="slider-fill" :style="{ width: (type.questionCount / type.max * 100) + '%' }"></div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </section>
- </div>
- <!-- Footer Action Bar -->
- <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;">
- <div class="flex items-center gap-6">
- <button class="flex items-center gap-2 text-gray-500 hover:text-red-500 transition-colors" @click="clearSettings" :disabled="isGenerating">
- <span class="material-symbols-outlined">delete_sweep</span>
- <span class="text-sm font-medium">清空当前配置</span>
- </button>
- <div class="h-8 w-px bg-gray-200"></div>
- </div>
-
- <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">
- 开始智能生成试卷
- </button>
- <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">
- 试卷生成中...
- </button>
- </footer>
- <!-- RIGHT SIDEBAR (The Intelligence Pane) -->
- <aside class="absolute right-0 top-0 w-[214px] bg-gray-50 border-l border-gray-200 flex flex-col h-full z-30">
- <div class="p-6">
- <h2 class="text-gray-900 font-bold text-lg mb-6">实时预览</h2>
- <div class="space-y-8">
- <div class="bg-white p-5 rounded-2xl shadow-sm border border-gray-100">
- <h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3">试卷名称</h3>
- <p class="text-gray-800 font-medium italic" :class="{'opacity-40': !examName}">{{ examName || '未命名试卷...' }}</p>
- </div>
- <div class="space-y-4">
- <h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider">结构大纲</h3>
- <ul class="space-y-4">
- <li v-for="(type, index) in questionTypes" :key="index" class="flex flex-col gap-1 group">
- <div class="flex items-center justify-between">
- <div class="flex items-center gap-2">
- <div class="w-1.5 h-1.5 rounded-full" :style="{ backgroundColor: ['#2563eb', '#3b82f6', '#60a5fa', '#93c5fd'][index % 4] }"></div>
- <span class="text-sm text-gray-800">{{ type.name }}</span>
- </div>
- <span class="text-xs font-bold text-gray-500">{{ type.questionCount }}题</span>
- </div>
- <span class="text-[10px] text-gray-400 ml-3.5">{{ type.questionCount * type.scorePerQuestion }} 分</span>
- </li>
- </ul>
- </div>
- <div class="pt-6 border-t border-gray-200 space-y-3">
- <div class="flex justify-between items-center">
- <span class="text-xs text-gray-500">试卷总分</span>
- <span class="text-xs font-bold text-gray-900">{{ totalScore }}</span>
- </div>
- </div>
- </div>
- </div>
- </aside>
- </div>
- <!-- EXAM COMPLETED STATE UI -->
- <div v-else-if="showExamDetail && !isGenerating" class="flex-1 flex flex-col w-full h-full bg-white relative">
- <!-- Exam Header -->
- <header class="bg-white px-8 py-6 border-b border-gray-100">
- <div class="max-w-5xl mx-auto">
- <div class="flex items-center justify-between mb-4">
- <h1 class="text-gray-900 text-lg font-normal">{{ currentExam?.title || examName }}</h1>
- <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">
- <span class="material-symbols-outlined text-[18px]">download</span>
- <span>下载Word</span>
- </button>
- </div>
- <div class="bg-gray-50 rounded-xl p-8 border border-gray-100">
- <div class="flex items-end justify-between">
- <div>
- <h2 class="font-bold text-gray-900 mb-6 text-2xl">{{ currentExam?.title || examName }}</h2>
- <div class="flex items-center space-x-8 text-[15px] text-gray-500">
- <span>总分: {{ currentExam?.totalScore || totalScore }}分</span>
- <span>题量: {{ currentExam?.totalQuestions || 0 }}题</span>
- </div>
- </div>
- <div class="text-[15px] text-gray-500">生成时间: {{ currentTime }}</div>
- </div>
- </div>
- </div>
- </header>
-
- <!-- Exam Content Area -->
- <div class="flex-1 overflow-y-auto p-10 space-y-8 pb-20">
- <div class="max-w-5xl mx-auto">
- <!-- Render Single Choice -->
- <div v-if="currentExam?.singleChoice?.questions?.length > 0" class="mb-8">
- <div class="flex items-center justify-between bg-gray-50 p-3 px-4 rounded-lg mb-4 cursor-pointer" @click="toggleSection('single')">
- <div class="flex items-center space-x-3">
- <div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white">
- <span class="material-symbols-outlined text-[16px]">{{ expandedSections.single ? 'remove' : 'add' }}</span>
- </div>
- <span class="font-bold text-gray-900">单选题</span>
- <span class="text-sm text-gray-500">(每题{{ currentExam.singleChoice.scorePerQuestion }}分, 共{{ currentExam.singleChoice.totalScore }}分)</span>
- </div>
- <div class="flex items-center space-x-2 text-gray-500">
- <span class="text-sm">{{ currentExam.singleChoice.count }}题</span>
- <span class="material-symbols-outlined">{{ expandedSections.single ? 'expand_less' : 'expand_more' }}</span>
- </div>
- </div>
-
- <div v-show="expandedSections.single" class="space-y-6">
- <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">
- <h3 class="text-[15px] font-medium text-gray-900 leading-relaxed pr-8">
- <span class="text-blue-500 font-bold mr-2">{{ qIndex + 1 }}.</span>{{ q.text }}
- </h3>
- <div class="grid grid-cols-2 mt-3 gap-y-2">
- <label v-for="(opt, oIndex) in q.options" :key="oIndex" class="flex items-center space-x-3 cursor-pointer">
- <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'">
- <div v-if="q.selectedAnswer === opt.key" class="w-2 h-2 bg-blue-500 rounded-full"></div>
- </div>
- <span class="text-sm" :class="q.selectedAnswer === opt.key ? 'text-blue-500 font-medium' : 'text-gray-600'">
- <span class="font-bold mr-2">{{ opt.key }}.</span>{{ opt.text }}
- </span>
- </label>
- </div>
- <div v-if="q.selectedAnswer" class="mt-4 p-3 bg-green-50 rounded-lg text-sm text-green-700">
- <span class="font-bold">正确答案:</span>{{ q.selectedAnswer }}
- </div>
- </div>
- </div>
- </div>
- <!-- Render Judge -->
- <div v-if="currentExam?.judge?.questions?.length > 0" class="mb-8">
- <div class="flex items-center justify-between bg-gray-50 p-3 px-4 rounded-lg mb-4 cursor-pointer" @click="toggleSection('judge')">
- <div class="flex items-center space-x-3">
- <div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white">
- <span class="material-symbols-outlined text-[16px]">{{ expandedSections.judge ? 'remove' : 'add' }}</span>
- </div>
- <span class="font-bold text-gray-900">判断题</span>
- <span class="text-sm text-gray-500">(每题{{ currentExam.judge.scorePerQuestion }}分, 共{{ currentExam.judge.totalScore }}分)</span>
- </div>
- <div class="flex items-center space-x-2 text-gray-500">
- <span class="text-sm">{{ currentExam.judge.count }}题</span>
- <span class="material-symbols-outlined">{{ expandedSections.judge ? 'expand_less' : 'expand_more' }}</span>
- </div>
- </div>
-
- <div v-show="expandedSections.judge" class="space-y-6">
- <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">
- <h3 class="text-[15px] font-medium text-gray-900 leading-relaxed pr-8">
- <span class="text-blue-500 font-bold mr-2">{{ qIndex + 1 }}.</span>{{ q.text }}
- </h3>
- <div v-if="q.selectedAnswer" class="mt-4 p-3 bg-green-50 rounded-lg text-sm text-green-700">
- <span class="font-bold">正确答案:</span>{{ q.selectedAnswer }}
- </div>
- </div>
- </div>
- </div>
- <!-- Render Multiple -->
- <div v-if="currentExam?.multiple?.questions?.length > 0" class="mb-8">
- <div class="flex items-center justify-between bg-gray-50 p-3 px-4 rounded-lg mb-4 cursor-pointer" @click="toggleSection('multiple')">
- <div class="flex items-center space-x-3">
- <div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white">
- <span class="material-symbols-outlined text-[16px]">{{ expandedSections.multiple ? 'remove' : 'add' }}</span>
- </div>
- <span class="font-bold text-gray-900">多选题</span>
- <span class="text-sm text-gray-500">(每题{{ currentExam.multiple.scorePerQuestion }}分, 共{{ currentExam.multiple.totalScore }}分)</span>
- </div>
- <div class="flex items-center space-x-2 text-gray-500">
- <span class="text-sm">{{ currentExam.multiple.count }}题</span>
- <span class="material-symbols-outlined">{{ expandedSections.multiple ? 'expand_less' : 'expand_more' }}</span>
- </div>
- </div>
-
- <div v-show="expandedSections.multiple" class="space-y-6">
- <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">
- <h3 class="text-[15px] font-medium text-gray-900 leading-relaxed pr-8">
- <span class="text-blue-500 font-bold mr-2">{{ qIndex + 1 }}.</span>{{ q.text }}
- </h3>
- <div class="grid grid-cols-2 mt-3 gap-y-2">
- <label v-for="(opt, oIndex) in q.options" :key="oIndex" class="flex items-center space-x-3 cursor-pointer">
- <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'">
- <span v-if="q.selectedAnswers?.includes(opt.key)" class="material-symbols-outlined text-[12px]">check</span>
- </div>
- <span class="text-sm" :class="q.selectedAnswers?.includes(opt.key) ? 'text-blue-500 font-medium' : 'text-gray-600'">
- <span class="font-bold mr-2">{{ opt.key }}.</span>{{ opt.text }}
- </span>
- </label>
- </div>
- <div v-if="q.selectedAnswers?.length > 0" class="mt-4 p-3 bg-green-50 rounded-lg text-sm text-green-700">
- <span class="font-bold">正确答案:</span>{{ q.selectedAnswers.join(', ') }}
- </div>
- </div>
- </div>
- </div>
- <!-- Render Short Answer -->
- <div v-if="currentExam?.short?.questions?.length > 0" class="mb-8">
- <div class="flex items-center justify-between bg-gray-50 p-3 px-4 rounded-lg mb-4 cursor-pointer" @click="toggleSection('short')">
- <div class="flex items-center space-x-3">
- <div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white">
- <span class="material-symbols-outlined text-[16px]">{{ expandedSections.short ? 'remove' : 'add' }}</span>
- </div>
- <span class="font-bold text-gray-900">简答题</span>
- <span class="text-sm text-gray-500">(每题{{ currentExam.short.scorePerQuestion }}分, 共{{ currentExam.short.totalScore }}分)</span>
- </div>
- <div class="flex items-center space-x-2 text-gray-500">
- <span class="text-sm">{{ currentExam.short.count }}题</span>
- <span class="material-symbols-outlined">{{ expandedSections.short ? 'expand_less' : 'expand_more' }}</span>
- </div>
- </div>
-
- <div v-show="expandedSections.short" class="space-y-6">
- <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">
- <h3 class="text-[15px] font-medium text-gray-900 leading-relaxed pr-8">
- <span class="text-blue-500 font-bold mr-2">{{ qIndex + 1 }}.</span>{{ q.text }}
- </h3>
- <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">
- <div class="font-bold mb-2 flex items-center gap-2"><span class="material-symbols-outlined text-[18px]">lightbulb</span>答题要点:</div>
- <div v-html="q.outline.keyFactors"></div>
- </div>
- </div>
- </div>
- </div>
-
- </div>
- </div>
- </div>
- </main>
- <!-- 隐藏的文件输入框 -->
- <input ref="fileInput" type="file" accept=".ppt,.pptx" style="display: none" @change="handleFileSelect" />
-
- <!-- 删除确认弹窗 -->
- <DeleteConfirmModal :visible="showDeleteModal" title="删除历史记录" :message="deleteConfirmMessage" @confirm="confirmDeleteHistory" @cancel="cancelDeleteHistory" @close="cancelDeleteHistory" />
- </div>
- </template>\n"""
- # Replace script
- content = content.replace(script_content_original, script_content)
- # We rebuild the content from parts using the new template.
- # But since we have `rest`, let's do this:
- # 1. Update `<script setup>` block
- new_rest = rest.replace(script_content_original, script_content)
- # 2. Append styles
- css = """
- <style scoped>
- .chat-container {
- display: flex;
- height: 100vh;
- width: 100%;
- overflow: hidden;
- background-color: #f7f9fb;
- }
- /* Material Icons font is usually loaded globally, but if missing, fallback to text */
- @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap');
- .material-symbols-outlined {
- font-family: 'Material Symbols Outlined';
- font-weight: normal;
- font-style: normal;
- font-size: 24px;
- display: inline-block;
- line-height: 1;
- text-transform: none;
- letter-spacing: normal;
- word-wrap: normal;
- white-space: nowrap;
- direction: ltr;
- }
- /* Base tailwind classes simulation */
- .flex { display: flex; }
- .flex-1 { flex: 1 1 0%; }
- .flex-col { flex-direction: column; }
- .items-center { align-items: center; }
- .justify-between { justify-content: space-between; }
- .justify-center { justify-content: center; }
- .relative { position: relative; }
- .absolute { position: absolute; }
- .w-full { width: 100%; }
- .h-full { height: 100%; }
- .h-screen { height: 100vh; }
- .overflow-hidden { overflow: hidden; }
- .overflow-y-auto { overflow-y: auto; }
- .grid-2-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
- .space-x-2 > * + * { margin-left: 0.5rem; }
- .space-x-3 > * + * { margin-left: 0.75rem; }
- .space-x-8 > * + * { margin-left: 2rem; }
- .space-y-2 > * + * { margin-top: 0.5rem; }
- .space-y-3 > * + * { margin-top: 0.75rem; }
- .space-y-4 > * + * { margin-top: 1rem; }
- .space-y-6 > * + * { margin-top: 1.5rem; }
- .space-y-8 > * + * { margin-top: 2rem; }
- .gap-1 { gap: 0.25rem; }
- .gap-2 { gap: 0.5rem; }
- .gap-3 { gap: 0.75rem; }
- .gap-4 { gap: 1rem; }
- .gap-6 { gap: 1.5rem; }
- .gap-y-2 { row-gap: 0.5rem; }
- /* Tailwind Utilities */
- .bg-surface-container-lowest { background-color: #ffffff; }
- .bg-white { background-color: #ffffff; }
- .bg-gray-50 { background-color: #f9fafb; }
- .bg-gray-100 { background-color: #f3f4f6; }
- .bg-gray-200 { background-color: #e5e7eb; }
- .bg-blue-50 { background-color: #eff6ff; }
- .bg-blue-300 { background-color: #93c5fd; }
- .bg-blue-500 { background-color: #3b82f6; }
- .bg-blue-600 { background-color: #2563eb; }
- .bg-blue-700 { background-color: #1d4ed8; }
- .bg-green-50 { background-color: #f0fdf4; }
- .text-white { color: #ffffff; }
- .text-gray-300 { color: #d1d5db; }
- .text-gray-400 { color: #9ca3af; }
- .text-gray-500 { color: #6b7280; }
- .text-gray-600 { color: #4b5563; }
- .text-gray-800 { color: #1f2937; }
- .text-gray-900 { color: #111827; }
- .text-blue-500 { color: #3b82f6; }
- .text-blue-600 { color: #2563eb; }
- .text-red-500 { color: #ef4444; }
- .text-green-500 { color: #22c55e; }
- .text-green-700 { color: #15803d; }
- .text-green-800 { color: #166534; }
- .text-on-surface-variant { color: #424753; }
- .border { border-width: 1px; }
- .border-t { border-top-width: 1px; }
- .border-l { border-left-width: 1px; }
- .border-gray-100 { border-color: #f3f4f6; }
- .border-gray-200 { border-color: #e5e7eb; }
- .border-gray-300 { border-color: #d1d5db; }
- .border-blue-300 { border-color: #93c5fd; }
- .border-blue-500 { border-color: #3b82f6; }
- .border-green-100 { border-color: #dcfce3; }
- .border-green-500 { border-color: #22c55e; }
- .rounded { border-radius: 0.25rem; }
- .rounded-lg { border-radius: 0.5rem; }
- .rounded-xl { border-radius: 0.75rem; }
- .rounded-2xl { border-radius: 1rem; }
- .rounded-full { border-radius: 9999px; }
- .shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); }
- .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
- .p-2 { padding: 0.5rem; }
- .p-3 { padding: 0.75rem; }
- .p-4 { padding: 1rem; }
- .p-5 { padding: 1.25rem; }
- .p-6 { padding: 1.5rem; }
- .p-8 { padding: 2rem; }
- .p-10 { padding: 2.5rem; }
- .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
- .px-4 { padding-left: 1rem; padding-right: 1rem; }
- .px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
- .px-8 { padding-left: 2rem; padding-right: 2rem; }
- .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
- .py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
- .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
- .py-2\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
- .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
- .py-4 { padding-top: 1rem; padding-bottom: 1rem; }
- .py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
- .pt-6 { padding-top: 1.5rem; }
- .pt-8 { padding-top: 2rem; }
- .pb-20 { padding-bottom: 5rem; }
- .pb-32 { padding-bottom: 8rem; }
- .mb-2 { margin-bottom: 0.5rem; }
- .mb-3 { margin-bottom: 0.75rem; }
- .mb-4 { margin-bottom: 1rem; }
- .mb-6 { margin-bottom: 1.5rem; }
- .mb-8 { margin-bottom: 2rem; }
- .mt-1 { margin-top: 0.25rem; }
- .mt-2 { margin-top: 0.5rem; }
- .mt-3 { margin-top: 0.75rem; }
- .mt-4 { margin-top: 1rem; }
- .mt-8 { margin-top: 2rem; }
- .ml-3\.5 { margin-left: 0.875rem; }
- .mr-2 { margin-right: 0.5rem; }
- .w-1\.5 { width: 0.375rem; }
- .w-2 { width: 0.5rem; }
- .w-4 { width: 1rem; }
- .w-6 { width: 1.5rem; }
- .w-12 { width: 3rem; }
- .w-\[214px\] { width: 214px; }
- .h-1\.5 { height: 0.375rem; }
- .h-2 { height: 0.5rem; }
- .h-4 { height: 1rem; }
- .h-6 { height: 1.5rem; }
- .h-8 { height: 2rem; }
- .h-12 { height: 3rem; }
- .h-48 { height: 12rem; }
- .min-h-screen { min-height: 100vh; }
- .min-w-\[80px\] { min-width: 80px; }
- .max-w-5xl { max-width: 64rem; }
- .mx-auto { margin-left: auto; margin-right: auto; }
- .text-\[10px\] { font-size: 10px; }
- .text-xs { font-size: 0.75rem; line-height: 1rem; }
- .text-sm { font-size: 0.875rem; line-height: 1.25rem; }
- .text-\[15px\] { font-size: 15px; }
- .text-base { font-size: 1rem; line-height: 1.5rem; }
- .text-lg { font-size: 1.125rem; line-height: 1.75rem; }
- .text-2xl { font-size: 1.5rem; line-height: 2rem; }
- .text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
- .font-normal { font-weight: 400; }
- .font-medium { font-weight: 500; }
- .font-bold { font-weight: 700; }
- .italic { font-style: italic; }
- .uppercase { text-transform: uppercase; }
- .tracking-wider { letter-spacing: 0.05em; }
- .leading-relaxed { line-height: 1.625; }
- .text-center { text-align: center; }
- .cursor-pointer { cursor: pointer; }
- .cursor-not-allowed { cursor: not-allowed; }
- .transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
- .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; }
- .outline-none { outline: 2px solid transparent; outline-offset: 2px; }
- .resize-none { resize: none; }
- .z-30 { z-index: 30; }
- .z-40 { z-index: 40; }
- .opacity-40 { opacity: 0.4; }
- .opacity-60 { opacity: 0.6; }
- .backdrop-blur-md { backdrop-filter: blur(12px); }
- /* Hover states */
- .hover\:bg-gray-50:hover { background-color: #f9fafb; }
- .hover\:bg-blue-700:hover { background-color: #1d4ed8; }
- .hover\:bg-green-50:hover { background-color: #f0fdf4; }
- .hover\:border-blue-300:hover { border-color: #93c5fd; }
- .hover\:text-red-500:hover { color: #ef4444; }
- /* Group hover */
- .group:hover .group-hover\:text-blue-500 { color: #3b82f6; }
- /* Focus states */
- .focus\:border-blue-500:focus { border-color: #3b82f6; }
- .focus\:ring-2:focus { box-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); }
- .focus\:ring-blue-100:focus { --tw-ring-color: #dbeafe; }
- /* Custom Slider */
- .custom-slider-container {
- position: relative;
- height: 4px;
- width: 100%;
- background-color: #e0e3e5;
- border-radius: 9999px;
- margin-top: 12px;
- margin-bottom: 8px;
- }
- .slider-track {
- position: absolute;
- top: 0;
- left: 0;
- height: 100%;
- width: 100%;
- border-radius: 9999px;
- pointer-events: none;
- }
- .slider-fill {
- height: 100%;
- background-color: #0058bd;
- border-radius: 9999px;
- }
- .custom-slider {
- position: absolute;
- top: -8px;
- left: 0;
- width: 100%;
- height: 20px;
- opacity: 0;
- cursor: pointer;
- z-index: 10;
- }
- .custom-slider::-webkit-slider-thumb {
- width: 16px;
- height: 16px;
- appearance: none;
- }
- /* Thumb indicator (visual only) */
- .custom-slider-container::after {
- content: '';
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- width: 16px;
- height: 16px;
- background-color: #0058bd;
- border: 2px solid white;
- border-radius: 50%;
- box-shadow: 0 1px 3px rgba(0,0,0,0.3);
- pointer-events: none;
- left: var(--thumb-pos, 0%);
- margin-left: -8px;
- }
- /* History list (from original) */
- .history-sidebar {
- width: 260px;
- background: #fff;
- border-right: 1px solid #e2e8f0;
- display: flex;
- flex-direction: column;
- }
- .history-header {
- padding: 20px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid #f1f5f9;
- }
- .section-title {
- font-size: 16px;
- font-weight: 600;
- color: #1e293b;
- }
- .new-chat-btn {
- width: 24px;
- height: 24px;
- cursor: pointer;
- transition: transform 0.2s;
- }
- .new-chat-btn:hover {
- transform: scale(1.1);
- }
- .history-list {
- flex: 1;
- overflow-y: auto;
- padding: 12px;
- }
- .history-item {
- padding: 12px;
- border-radius: 8px;
- margin-bottom: 8px;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .history-item:hover {
- background: #f8fafc;
- }
- .history-item.active {
- background: #eff6ff;
- }
- .history-content {
- flex: 1;
- overflow: hidden;
- }
- .history-title {
- font-size: 14px;
- color: #334155;
- margin-bottom: 4px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .history-time {
- font-size: 12px;
- color: #94a3b8;
- }
- .delete-btn {
- padding: 4px;
- opacity: 0;
- transition: opacity 0.2s;
- }
- .history-item:hover .delete-btn, .delete-btn.always-visible {
- opacity: 1;
- }
- .delete-icon {
- width: 16px;
- height: 16px;
- }
- .empty-history {
- text-align: center;
- padding: 40px 0;
- }
- .empty-icon {
- width: 64px;
- margin-bottom: 12px;
- }
- .empty-text {
- color: #94a3b8;
- font-size: 14px;
- }
- .history-loading {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 0;
- color: #64748b;
- font-size: 14px;
- }
- .loading-spinner {
- width: 24px;
- height: 24px;
- border: 2px solid #e2e8f0;
- border-top-color: #3b82f6;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin-bottom: 12px;
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- </style>
- """
- # Strip old styles
- new_rest_without_style = re.sub(r'<style[\s\S]*?</style>', '', new_rest)
- final_content = new_template + "\n" + new_rest_without_style + "\n" + css
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(final_content)
|