LingTropy/lingtropy-client/src/renderer/screens/TextRewritingTool.vue

1179 lines
25 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="text-rewriter-container">
<div class="text-rewriter-layout">
<!-- 左侧输入区域 -->
<div class="input-section">
<HeaderComponent v-model:tokenvalue="currentKey" />
<SelectModel v-model:modelvalue="currentModel" />
<div class="rewrite-mode-section">
<h2 class="section-title">
<span class="section-icon"></span>
改写模式
</h2>
<div class="mode-tabs">
<button
:class="{ active: mode === 'single' }"
@click="switchMode('single')"
class="mode-tab"
>
<span class="mode-icon">🔄</span>
单次改写
</button>
<button
:class="{ active: mode === 'batch' }"
@click="switchMode('batch')"
class="mode-tab"
>
<span class="mode-icon">🔀</span>
批量改写
</button>
</div>
<transition name="fade">
<div v-if="mode === 'batch'" class="generation-count">
<label for="count-input">生成数量:</label>
<div class="count-control">
<button
:disabled="generationCount <= 1"
@click="decrementCount"
class="count-button"
>-</button
>
<input
id="count-input"
v-model="generationCount"
@input="validateInput"
type="number"
class="count-input"
/>
<button
:disabled="generationCount >= ARTICLE_MAX_COUNT"
@click="incrementCount"
class="count-button"
>+</button
>
</div>
</div>
</transition>
</div>
<div class="original-text-section">
<h2 class="section-title">
<span class="section-icon">📝</span>
原始文案
<span v-if="originalText" class="char-count">{{ originalText.length }} 字</span>
</h2>
<textarea
v-model="originalText"
class="text-input"
placeholder="请在此输入需要改写的文本..."
></textarea>
</div>
<button :disabled="isGenerating" @click="rewriteText" class="rewrite-button">
<span v-if="isGenerating" class="loading-spinner"></span>
<span v-else>{{ mode === 'single' ? '生成改写' : '批量生成' }}</span>
</button>
</div>
<!-- 右侧结果区域 -->
<transition name="fade">
<div v-if="rewrittenText.length > 0 || isGenerating" class="result-section">
<div class="result-header-bar">
<h2 class="section-title">
<span class="section-icon">✨</span>
改写结果
</h2>
<div class="result-stats">
<div class="layout-toggle">
<button
@click="toggleLayout('list')"
:class="{ active: layoutMode === 'list' }"
class="layout-button"
>
<span class="layout-icon">📋</span>
</button>
<button
@click="toggleLayout('grid')"
:class="{ active: layoutMode === 'grid' }"
class="layout-button"
>
<span class="layout-icon">📊</span>
</button>
</div>
<span class="model-badge">{{ currentModel }}</span>
<span class="count-badge">{{ rewrittenText.length }} 个结果</span>
<button
@click="toggleLayout"
class="layout-toggle-button"
:title="settings.layoutMode === 'list' ? '切换到网格视图' : '切换到列表视图'"
>
<span v-if="settings.layoutMode === 'list'" >▦</span>
<span v-else>☰</span>
</button>
</div>
</div>
<div v-if="isGenerating && rewrittenText.length === 0" class="generating-placeholder">
<div class="generating-animation">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<p>正在使用 {{ currentModel }} 生成改写结果...</p>
</div>
<div v-else class="results-container">
<div :class="['results-list', layoutMode === 'grid' ? 'grid-layout' : '']">
<div
v-for="(text, index) in rewrittenText"
:key="index"
:class="{
'expanded-result': expandedResults[index],
'grid-item': layoutMode === 'grid'
}"
:style="{ animationDelay: `${index * 0.1}s` }"
class="result-block"
>
<div class="result-header">
<div class="result-number">
结果 #{{ index + 1 }}
<span class="char-count">{{ text.length }} 字</span>
</div>
<div class="result-actions">
<button @click="copyText(text, index)" class="action-button copy-button">
<span v-if="copiedIndex === index">已复制!</span>
<span v-else>复制</span>
</button>
<button @click="toggleExpand(index)" class="action-button expand-button">
{{ expandedResults[index] ? '收起' : '展开' }}
</button>
</div>
</div>
<div :class="{ expanded: expandedResults[index] }" class="result-content">
{{ text }}
</div>
</div>
</div>
</div>
</div>
<div v-else class="empty-result-section">
<div class="empty-state">
<div class="empty-icon"></div>
<h3>等待生成结果</h3>
<p>请在左侧输入文本并点击生成按钮</p>
</div>
</div>
</transition>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ARTICLE_MAX_COUNT } from '../constants/constants'
import HeaderComponent from '../components/HeaderComponent.vue'
import SelectModel from '../components/SelectModelComponent.vue'
// 基础状态
const mode = ref('single')
const originalText = ref('')
const rewrittenText = ref([])
const generationCount = ref(3)
const expandedResults = reactive({})
const isGenerating = ref(false)
const copiedIndex = ref(null)
const baseUrl = ref('https://www.apimini2.org/v1/')
const currentModel = ref('gpt-4o')
const currentKey = ref('')
const layoutMode = ref('list')
// 生成数量限制
const validateInput = () => {
if (generationCount.value < 1) {
setGenerationCount(1)
} else if (generationCount.value > ARTICLE_MAX_COUNT) {
setGenerationCount(ARTICLE_MAX_COUNT)
}
}
const setGenerationCount = async (value) => {
generationCount.value = value
await window.mainApi.invoke('store-set', 'generationCount', value)
}
// 新增:切换布局模式
const toggleLayout = (newLayout) => {
layoutMode.value = newLayout
// 可选:保存用户偏好
window.mainApi.invoke('store-set', 'layoutMode', newLayout).catch((err) => {
console.error('保存布局偏好失败:', err)
})
}
onMounted(async () => {
const count = await window.mainApi.invoke('store-get', 'generationCount')
generationCount.value = count || 3
console.log('generationCount', generationCount.value)
// 新增:获取保存的布局偏好
try {
const savedLayout = await window.mainApi.invoke('store-get', 'layoutMode')
if (savedLayout) {
layoutMode.value = savedLayout
}
} catch (err) {
console.error('获取布局偏好失败:', err)
}
baseUrl.value = await window.mainApi.invoke('getBaseUrl')
console.log('baseUrl', baseUrl.value)
// currentKey.value =
// console.log("currentKey", currentKey.value)
})
// 其他方法
const switchMode = (newMode) => {
if (mode.value !== newMode) {
mode.value = newMode
}
}
const incrementCount = () => {
if (generationCount.value < ARTICLE_MAX_COUNT) {
// generationCount.value++
setGenerationCount(generationCount.value + 1)
}
}
const decrementCount = () => {
if (generationCount.value > 1) {
setGenerationCount(generationCount.value - 1)
// generationCount.value--
}
}
function callOpenAI(apiKey, model, count, rawArticle) {
window.mainApi.send('call-openai', baseUrl, apiKey, model, count, rawArticle)
}
const rewriteText = () => {
if (!originalText.value.trim()) {
showToast('请输入需要改写的文本')
return
}
// 清空之前的改写结果
Object.keys(expandedResults).forEach((key) => {
delete expandedResults[key]
})
console.log(`userToekn:${currentKey.value}`)
isGenerating.value = true
rewrittenText.value = []
// 调用 OpenAI API
callOpenAI(
currentKey.value, // 替换为你的 API 密钥
currentModel.value, // 使用的模型
mode.value === 'single' ? 1 : generationCount.value, // 请求次数
originalText.value // 原始文本
)
// 监听部分结果
window.mainApi.on('openai-partial-response', (_, { index, content }) => {
// console.log(`请求 ${index + 1} 的部分结果:`, content);
// 将部分结果渲染到页面
if (!rewrittenText.value[index]) {
rewrittenText.value[index] = ''
}
rewrittenText.value[index] += content
})
// 监听最终结果
window.mainApi
.invoke(
'call-openai',
baseUrl.value,
currentKey.value,
currentModel.value,
mode.value === 'single' ? 1 : generationCount.value,
originalText.value
)
.then((results) => {
console.log('所有请求的最终结果:', results)
// 将最终结果渲染到页面
results.forEach(({ index, response }) => {
rewrittenText.value[index] = response
})
isGenerating.value = false
})
.catch((error) => {
console.error('调用 API 失败:', error)
showToast('调用 API 失败,请稍后重试')
isGenerating.value = false
})
}
const copyText = (text, index) => {
navigator.clipboard
.writeText(text)
.then(() => {
copiedIndex.value = index
setTimeout(() => {
copiedIndex.value = null
}, 2000)
})
.catch((err) => {
console.error('复制失败:', err)
showToast('复制失败,请重试')
})
}
const toggleExpand = (index) => {
expandedResults[index] = !expandedResults[index]
}
const showToast = (message) => {
const toast = document.createElement('div')
toast.className = 'toast-message'
toast.textContent = message
document.body.appendChild(toast)
setTimeout(() => {
toast.classList.add('show')
setTimeout(() => {
toast.classList.remove('show')
setTimeout(() => {
document.body.removeChild(toast)
}, 300)
}, 2000)
}, 10)
}
</script>
<style scoped>
/* 基础布局 */
.text-rewriter-container {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
/* 背景装饰 */
.background-decoration {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: -1;
overflow: hidden;
}
.bg-circle {
position: absolute;
border-radius: 50%;
opacity: 0.5;
filter: blur(60px);
}
.bg-circle-1 {
width: 400px;
height: 400px;
background: linear-gradient(45deg, rgba(46, 204, 113, 0.3), rgba(52, 152, 219, 0.3));
top: -100px;
left: -100px;
animation: float 15s ease-in-out infinite alternate;
}
.bg-circle-2 {
width: 600px;
height: 600px;
background: linear-gradient(45deg, rgba(155, 89, 182, 0.2), rgba(41, 128, 185, 0.2));
bottom: -200px;
right: -200px;
animation: float 20s ease-in-out infinite alternate-reverse;
}
.bg-circle-3 {
width: 300px;
height: 300px;
background: linear-gradient(45deg, rgba(241, 196, 15, 0.2), rgba(231, 76, 60, 0.2));
top: 40%;
left: 60%;
animation: float 18s ease-in-out infinite alternate;
}
.bg-dots {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: radial-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px);
background-size: 30px 30px;
opacity: 0.3;
}
@keyframes float {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
}
.text-rewriter-layout {
display: flex;
gap: 24px;
width: 100%;
max-width: 1800px;
height: calc(100vh - 40px);
position: relative;
z-index: 1;
}
/* 左侧输入区域 */
.input-section {
flex: 0 0 40%;
max-width: 600px;
min-width: 320px;
background-color: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 28px;
display: flex;
flex-direction: column;
overflow-y: auto;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.95);
}
/* 右侧结果区域 */
.result-section,
.empty-result-section {
flex: 1;
background-color: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 28px;
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.95);
}
.result-header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.result-stats {
display: flex;
gap: 12px;
align-items: center;
}
.model-badge,
.count-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
.model-badge {
background-color: #e8f5e9;
color: #2e7d32;
}
/* 新增:布局切换按钮样式 */
.layout-toggle {
display: flex;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e0e0e0;
}
.layout-button {
background-color: white;
border: none;
padding: 6px 10px;
cursor: pointer;
transition: all 0.2s;
}
.layout-button.active {
background-color: #f0f0f0;
}
.layout-icon {
font-size: 16px;
}
.count-badge {
background-color: #e3f2fd;
color: #1565c0;
}
/* 标题样式 */
.title {
font-size: 24px;
font-weight: 700;
margin: 0 0 4px 0;
background: linear-gradient(90deg, #2ecc71, #27ae60);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.subtitle {
color: #666;
margin: 0;
font-size: 14px;
}
.section-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 16px 0;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.section-icon {
font-size: 20px;
}
/* 字数统计 */
.char-count {
font-size: 14px;
color: #666;
font-weight: normal;
margin-left: auto;
background-color: #f0f0f0;
padding: 2px 8px;
border-radius: 12px;
}
/* 生成数量控制样式 */
.generation-count {
display: flex;
align-items: center;
margin-bottom: 24px;
gap: 16px;
padding: 16px;
background-color: #f9f9f9;
border-radius: 10px;
border-left: 4px solid #2ecc71;
}
.count-control {
display: flex;
align-items: center;
}
.count-button {
width: 36px;
height: 36px;
border: 1px solid #e0e0e0;
background-color: white;
color: #333;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.count-button:first-child {
border-radius: 8px 0 0 8px;
}
.count-button:last-child {
border-radius: 0 8px 8px 0;
}
.count-button:hover:not(:disabled) {
background-color: #f0f0f0;
}
.count-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.count-input {
border: 1px solid #e0e0e0;
border-left: none;
border-right: none;
padding: 8px 0;
width: 50px;
font-size: 16px;
text-align: center;
height: 36px;
}
/* 输入区域样式 */
.original-text-section {
margin-bottom: 24px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.text-input {
border: 1px solid #e0e0e0;
border-radius: 10px;
font-family: inherit;
font-size: 16px;
padding: 16px;
resize: none;
width: 100%;
flex-grow: 1;
min-height: 150px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
transition:
border-color 0.3s,
box-shadow 0.3s;
background-color: #f9f9f9;
}
.text-input:focus {
outline: none;
border-color: #2ecc71;
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.2);
background-color: white;
}
/* 按钮样式 */
.rewrite-button {
background: linear-gradient(135deg, #2ecc71, #27ae60);
border: none;
border-radius: 10px;
color: white;
cursor: pointer;
font-size: 16px;
font-weight: 600;
margin-top: 16px;
padding: 16px;
transition: all 0.3s ease;
width: 100%;
position: relative;
overflow: hidden;
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
}
.rewrite-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(46, 204, 113, 0.4);
}
.rewrite-button:active:not(:disabled) {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(46, 204, 113, 0.3);
}
.rewrite-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* 加载动画 */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 结果区域样式 */
.results-container {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.results-list {
display: flex;
flex-direction: column;
gap: 20px;
padding: 4px;
}
/* 新增:网格布局样式 */
.results-list.grid-layout {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.result-block {
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
border: 1px solid #e8e8e8;
width: 100%;
transition: all 0.3s ease;
animation: fadeIn 0.5s ease forwards;
opacity: 0;
transform: translateY(20px);
display: flex;
flex-direction: column;
}
/* 新增:网格布局中的项目样式 */
.result-block.grid-item {
height: 250px;
}
.result-block:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
.result-header {
background-color: #f9f9f9;
padding: 14px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e8e8e8;
}
.result-number {
font-weight: 600;
color: #2ecc71;
display: flex;
align-items: center;
gap: 10px;
}
.result-number::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
background-color: #2ecc71;
border-radius: 50%;
}
.result-actions {
display: flex;
gap: 8px;
}
.action-button {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
position: relative;
min-width: 60px;
text-align: center;
}
.action-button:hover {
background-color: #f0f0f0;
border-color: #ccc;
}
.copy-button {
color: #2ecc71;
border-color: #2ecc71;
}
.copy-button:hover {
background-color: rgba(46, 204, 113, 0.1);
border-color: #2ecc71;
}
.expand-button {
color: #666;
}
.result-content {
padding: 16px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.6;
max-height: 200px;
overflow: hidden;
position: relative;
text-align: justify;
hyphens: auto;
transition: max-height 0.5s ease;
flex: 1;
}
.result-content:not(.expanded)::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 60px;
background: linear-gradient(transparent, white);
pointer-events: none;
transition: opacity 0.3s;
}
.result-content.expanded {
max-height: none;
}
/* 生成中占位符 */
.generating-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
color: #666;
}
.generating-placeholder p {
margin-top: 20px;
font-size: 16px;
}
.generating-animation {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.dot {
width: 12px;
height: 12px;
background-color: #2ecc71;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
/* 空状态 */
.empty-result-section {
display: flex;
align-items: center;
justify-content: center;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
color: #ccc;
}
.empty-state h3 {
font-size: 20px;
margin-bottom: 8px;
color: #333;
}
.empty-state p {
font-size: 16px;
color: #666;
}
/* 动画效果 */
@keyframes fadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInOut {
0% {
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.fade-enter-active,
.fade-leave-active {
transition:
opacity 0.3s,
transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(20px);
}
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from {
opacity: 0;
transform: translateY(30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 提示框样式 */
:global(.toast-message) {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 1000;
transition: transform 0.3s ease;
}
:global(.toast-message.show) {
transform: translateX(-50%) translateY(0);
}
/* 布局切换按钮 */
.layout-toggle-button {
background-color: #f0f0f0;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.layout-toggle-button:hover {
background-color: #e0e0e0;
transform: scale(1.1);
}
/* 响应式设计 */
@media (max-width: 1200px) {
.text-rewriter-layout {
flex-direction: column;
}
.input-section {
flex: none;
width: 100%;
max-width: 100%;
}
.result-section,
.empty-result-section {
width: 100%;
}
/* 网格布局在小屏幕上调整 */
.results-list.grid-layout {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.text-rewriter-container {
padding: 12px;
}
/* 网格布局在平板上调整 */
.results-list.grid-layout {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
.text-rewriter-layout {
gap: 16px;
}
.input-section,
.result-section,
.empty-result-section {
padding: 20px;
border-radius: 12px;
}
.mode-tabs {
flex-direction: column;
}
.generation-count {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
/* .header-section {
flex-direction: column;
text-align: center;
}
.settings-button {
position: absolute;
top: 0;
right: 0;
} */
}
@media (max-width: 480px) {
.text-rewriter-container {
padding: 8px;
}
.input-section,
.result-section,
.empty-result-section {
padding: 16px;
}
.result-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.result-actions {
width: 100%;
justify-content: space-between;
}
/* 在手机上强制使用列表布局 */
.results-list.grid-layout {
display: flex;
flex-direction: column;
}
.result-block.grid-item {
height: auto;
}
}
/* 模式选择样式 */
.rewrite-mode-section {
margin-bottom: 24px;
}
.mode-tabs {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.mode-tab {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 10px;
color: #333;
cursor: pointer;
flex: 1;
font-size: 16px;
padding: 14px;
text-align: center;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.mode-tab:hover {
border-color: #2ecc71;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.mode-tab.active {
background: linear-gradient(135deg, #2ecc71, #27ae60);
border-color: transparent;
color: white;
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
}
.mode-icon {
font-size: 18px;
}
</style>