feat(docs): add NavGrid/NavCard components and restructure stage pages

- Add NavGrid.vue and NavCard.vue components for better navigation layout
- Restructure stage-0 index pages across languages into intro.md with new navigation components
- Remove old stage-0 index.md files and update stage-3 pages similarly
- Add new dependencies 'claude' and 'codex' to package.json
- Improve code formatting in multiple Vue components for better readability
- Update documentation content and structure for better user experience
This commit is contained in:
sanbuphy
2026-02-01 23:42:12 +08:00
parent a9a5c5c8a7
commit ad95658a11
171 changed files with 16366 additions and 7946 deletions
@@ -1,57 +1,151 @@
<!--
PromptQuickStartDemo.vue
提示词先玩后讲快速体验同一任务切换提示词写法看输出质量变化
交互
- 选择任务写文案/总结/写代码
- 选择提示词等级随口一句 / 清晰版 / 专业版
- 展示你写的提示词AI 输出并提示改进点
-->
<template>
<div class="quick">
<div class="header">
<div>
<div class="title">先玩一下同一个需求换一种说法</div>
<div class="subtitle">你改的不是字数而是边界标准</div>
</div>
<div class="controls">
<select v-model="taskId">
<option v-for="t in tasks" :key="t.id" :value="t.id">
{{ t.label }}
</option>
</select>
<div class="levels">
<button
v-for="l in levels"
:key="l.id"
:class="['level', { active: levelId === l.id }]"
@click="levelId = l.id"
<div class="quick-start-demo-container">
<el-card class="quick-start-card" shadow="hover">
<template #header>
<div class="header-content">
<div class="title-group">
<div class="title">🕹 互动体验提示词进化论</div>
<div class="subtitle">不要一次性写好试着像搭积木一样优化你的指令</div>
</div>
<div class="controls">
<span class="label">选择任务</span>
<el-select v-model="taskId" @change="reset" style="width: 160px" size="large">
<el-option
v-for="t in tasks"
:key="t.id"
:label="t.label"
:value="t.id"
/>
</el-select>
</div>
</div>
</template>
<!-- 游戏区 -->
<div class="game-area">
<!-- 左侧提示词构建 -->
<div class="prompt-builder">
<div class="section-title">你的指令 (Prompt)</div>
<div class="prompt-box">
<!-- 基础层 -->
<div class="block base" :class="{ active: true }">
<span class="icon">📝</span>
<span class="text">{{ basePrompt }}</span>
</div>
<!-- 进阶层清晰指令 -->
<div v-if="level >= 1" class="block clear animate-in">
<span class="icon">🎯</span>
<span class="text">{{ clearPromptAddon }}</span>
</div>
<!-- 专家层结构化 -->
<div v-if="level >= 2" class="block pro animate-in">
<span class="icon">🧠</span>
<span class="text">{{ proPromptAddon }}</span>
</div>
</div>
<!-- 升级按钮 -->
<div class="upgrade-controls">
<div class="level-info">
<el-tag :type="levelColor" effect="dark" size="small" style="margin-bottom: 4px;">Level {{ level }}</el-tag>
<span class="level-desc" :style="{ color: levelColorCode }">{{ levelLabel }}</span>
</div>
<div class="actions">
<el-button-group>
<el-button
:disabled="level === 0"
@click="downgrade"
icon="Minus"
>
降级
</el-button>
<el-button
type="primary"
:disabled="level === 2"
@click="upgrade"
icon="Plus"
>
升级
</el-button>
</el-button-group>
</div>
</div>
<el-button
type="primary"
size="large"
:loading="isRunning"
@click="run"
style="width: 100%; font-weight: bold; font-size: 1.1rem;"
>
{{ l.label }}
</button>
{{ isRunning ? '生成中...' : '🚀 发送给 AI' }}
</el-button>
</div>
<!-- 右侧AI 模拟输出 -->
<div class="chat-preview">
<div class="section-title">
<span>AI 回复 (Output)</span>
<!-- 历史记录切换 -->
<div class="history-tabs" v-if="hasAnyHistory">
<el-radio-group v-model="viewLevel" size="small">
<el-radio-button
v-for="l in availableLevels"
:key="l"
:label="l"
>
L{{ l }}
</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="chat-window">
<!-- 空状态 -->
<div v-if="!hasRun && !hasAnyHistory" class="empty-state">
<el-empty description="点击左侧“发送”按钮,看看 AI 会怎么回。" :image-size="100" />
</div>
<!-- 内容区域 -->
<div v-else>
<!-- 比较模式提示 -->
<el-alert
v-if="viewLevel !== level"
type="info"
show-icon
:closable="false"
style="margin-bottom: 12px;"
>
<template #title>
正在查看 Level {{ viewLevel }} 的历史记录 (当前是 L{{ level }})
<el-button link type="primary" @click="viewLevel = level" style="padding: 0; vertical-align: baseline;">回到当前</el-button>
</template>
</el-alert>
<div class="message-bubble" :class="{ typing: isRunning && viewLevel === level }">
<div class="avatar">🤖</div>
<div class="content">
<div v-if="isRunning && viewLevel === level" class="typing-indicator">
<span></span><span></span><span></span>
</div>
<div v-else class="markdown-body" v-html="renderMarkdown(getOutputForLevel(viewLevel))"></div>
</div>
</div>
<!-- 点评气泡 -->
<div v-if="(!isRunning || viewLevel !== level) && getOutputForLevel(viewLevel)" class="feedback-bubble animate-pop">
<div class="feedback-title">💡 {{ getFeedbackForLevel(viewLevel).title }}</div>
<div class="feedback-text">{{ getFeedbackForLevel(viewLevel).text }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="grid">
<div class="panel">
<div class="panel-title">提示词 / Prompt</div>
<pre><code>{{ prompt }}</code></pre>
<div class="hint">{{ promptHint }}</div>
</div>
<div class="panel">
<div class="panel-title">AI 输出 / Output示意</div>
<div class="output">{{ output }}</div>
<div class="hint">{{ outputHint }}</div>
</div>
</div>
<div class="tips">
<div class="tip" v-for="t in tips" :key="t.title">
<div class="tip-title">{{ t.title }}</div>
<div class="tip-body">{{ t.body }}</div>
</div>
</div>
</el-card>
</div>
</template>
@@ -59,210 +153,406 @@
import { computed, ref } from 'vue'
const tasks = [
{ id: 'copy', label: '写一段小红书文案' },
{ id: 'summary', label: '把一段文字总结成要点' },
{ id: 'code', label: '写一个小函数' }
]
const levels = [
{ id: 'vague', label: '随口一句' },
{ id: 'clear', label: '清晰版' },
{ id: 'pro', label: '专业版' }
{ id: 'copy', label: '写小红书文案' },
{ id: 'summary', label: '总结会议纪要' },
{ id: 'code', label: '写代码函数' }
]
const taskId = ref('copy')
const levelId = ref('vague')
const level = ref(0) // 0: vague, 1: clear, 2: pro
const isRunning = ref(false)
const hasRun = ref(false)
const displayedOutput = ref('')
const prompt = computed(() => {
if (taskId.value === 'copy') {
if (levelId.value === 'vague') return '写个咖啡杯文案'
if (levelId.value === 'clear')
return '写一段小红书风格文案,主题:保温咖啡杯。语气:轻松。长度:120-160 字。'
return `你是小红书资深种草博主。\n任务:写一段保温咖啡杯的种草文案。\n受众:通勤上班族。\n要求:\n- 120-160 字\n- 3 个卖点(颜值/密封/保温)\n- 结尾加一句行动号召\n输出:一段中文文案,不要标题。`
}
if (taskId.value === 'summary') {
if (levelId.value === 'vague') return '帮我总结一下这段文字'
if (levelId.value === 'clear')
return '把下面内容总结成 3-5 个要点,每点不超过 15 个字。'
return `任务:把输入文本总结成要点。\n要求:\n- 5 个以内\n- 每点 <= 15 字\n- 只输出要点列表,不要解释\n格式:Markdown 无序列表`
}
// code
if (levelId.value === 'vague') return '写个排序函数'
if (levelId.value === 'clear')
return '用 JavaScript 写一个快速排序函数,并给一个使用示例。'
return `你是资深前端工程师。\n任务:实现 quickSort(arr)。\n要求:\n- 纯函数(不修改原数组)\n- 处理重复值\n- 代码加简短注释\n- 给一个示例输入输出\n输出:只给 JS 代码块`
})
// 存储历史输出:{ 0: "...", 1: "..." }
const outputs = ref({})
const viewLevel = ref(0) // 当前查阅的 Level
const output = computed(() => {
if (taskId.value === 'copy') {
if (levelId.value === 'vague')
return '这是一款很好用的咖啡杯,适合日常使用...'
if (levelId.value === 'clear')
return '早八通勤救星!这只保温杯颜值在线,放包里不漏,热咖啡到下午还温温的...'
return '通勤党必备!奶油配色超耐看,密封圈一拧就稳,放包里也不怕洒;保温够久,早上冲的拿铁下午还是温热...想要链接评论区见~'
}
if (taskId.value === 'summary') {
if (levelId.value === 'vague') return '这段文字主要讲了……(可能很长)'
if (levelId.value === 'clear')
return '- 核心观点 1\n- 核心观点 2\n- 核心观点 3'
return '- 关键结论\n- 主要原因\n- 影响范围\n- 建议行动'
}
// code
if (levelId.value === 'vague') return 'function sort(arr) { /* ... */ }'
if (levelId.value === 'clear')
return 'function quickSort(arr) { /* ... */ }\nconsole.log(quickSort([3,1,2]))'
return `function quickSort(arr) {\n const a = [...arr]\n if (a.length <= 1) return a\n const pivot = a[0]\n const left = a.slice(1).filter(x => x < pivot)\n const right = a.slice(1).filter(x => x >= pivot)\n return [...quickSort(left), pivot, ...quickSort(right)]\n}\n\nconsole.log(quickSort([3, 1, 2, 2])) // [1,2,2,3]`
})
const hasAnyHistory = computed(() => Object.keys(outputs.value).length > 0)
const availableLevels = computed(() => Object.keys(outputs.value).map(Number).sort())
const promptHint = computed(() => {
if (levelId.value === 'vague') return '问题:AI 不知道你要什么标准。'
if (levelId.value === 'clear')
return '好一点:有风格/长度,但仍缺少“检查标准”。'
return '最好:角色 + 任务 + 要求 + 输出格式,AI 很难跑偏。'
})
const reset = () => {
level.value = 0
hasRun.value = false
displayedOutput.value = ''
outputs.value = {}
viewLevel.value = 0
}
const outputHint = computed(() => {
if (levelId.value === 'vague')
return '常见结果:泛泛而谈、风格不稳、格式不对。'
if (levelId.value === 'clear')
return '常见结果:更像你要的,但细节/格式可能还会飘。'
return '常见结果:风格稳定、结构清晰、可直接复制使用。'
})
const upgrade = () => {
if (level.value < 2) level.value++
hasRun.value = false
viewLevel.value = level.value // 切换到新等级时,视角跟随
}
const tips = computed(() => {
if (levelId.value === 'vague') {
return [
{ title: '先补 3 件事', body: '你要做什么?给谁看?最后要什么格式?' },
{ title: '别怕写长', body: '长不是目的,“可检查”才是目的。' }
]
const downgrade = () => {
if (level.value > 0) level.value--
hasRun.value = false
viewLevel.value = level.value
}
const levelLabel = computed(() => ['随口一说', '清晰指令', '结构化 Prompt'][level.value])
const levelColor = computed(() => ['info', 'warning', 'success'][level.value])
const levelColorCode = computed(() => ['#909399', '#e6a23c', '#67c23a'][level.value])
// Prompt 内容配置
const promptConfig = {
copy: {
base: '写个咖啡杯文案',
clear: '+ 风格:小红书,轻松活泼。长度:100字左右。卖点:颜值高、保温好。',
pro: '+ 角色:资深种草博主\n+ 结构:痛点 -> 卖点 -> 场景 -> 结尾互动\n+ 格式:多用 Emoji,分段清晰'
},
summary: {
base: '帮我总结一下这段文字',
clear: '+ 要求:提炼 3 个核心要点,每点不超过 20 字。',
pro: '+ 角色:专业秘书\n+ 格式:Markdown 无序列表\n+ 排除:不要客套话,只要干货'
},
code: {
base: '写个排序函数',
clear: '+ 语言:JavaScript (ES6)。要求:快速排序,带注释。',
pro: '+ 角色:资深前端架构师\n+ 健壮性:处理边界情况(空数组、非数组)\n+ 示例:附带一个测试用例'
}
if (levelId.value === 'clear') {
return [
{ title: '再加一条', body: '加“输出格式”或“要点数量”,能明显更稳。' },
{ title: '再加一个例子', body: '给 1 个示例,AI 会更像你的口吻。' }
]
}
return [
{ title: '记住模板', body: '角色 / 任务 / 输入 / 要求 / 输出格式。' },
{ title: '写完就测', body: '同一输入跑 2-3 次,看是否稳定。' }
}
const basePrompt = computed(() => promptConfig[taskId.value].base)
const clearPromptAddon = computed(() => promptConfig[taskId.value].clear)
const proPromptAddon = computed(() => promptConfig[taskId.value].pro)
// 模拟输出内容
const outputConfig = {
copy: [
'这个咖啡杯真的很好用,推荐给大家。它颜色很好看,而且保温效果也不错。快去买吧。',
'✨ 早八人必备!这个保温杯颜值真的绝绝子!💖 拿在手里超有质感,而且保温效果超级好,早上装的咖啡下午还是热的!☕️ 放在包里也不漏水,集美们冲鸭!',
'👋 还在为冷咖啡烦恼?\n\n😫 **痛点**:早起冲的咖啡,还没到公司就凉了?\n\n🌟 **安利**:这款“拿铁杯”必须拥有!\n1️⃣ **颜值主义**:奶油白配色,随手一拍就是大片 📸\n2️⃣ **硬核保温**:实测 6 小时依然烫嘴 🔥\n3️⃣ **办公绝配**:密封圈设计,随便塞包里不漏洒 🎒\n\n👇 评论区告诉我,你最喜欢哪个颜色?'
],
summary: [
'这段文字主要讲了关于...(此处省略500字流水账)...总之就是这些内容。',
'- 核心观点:用户增长放缓\n- 主要原因:市场竞争加剧\n- 建议:加大投放力度',
'### 📝 会议核心摘要\n\n* **📉 现状**Q3 用户增长率下降 15%\n* **🔍 原因**:竞品推出低价策略,分流明显\n* **🚀 行动**:下周启动“老用户回馈”专项活动'
],
code: [
'function sort(arr) { return arr.sort() } // 没写快排,或者写了但没注释',
'// 快速排序\nconst quickSort = (arr) => {\n if (arr.length <= 1) return arr;\n const p = arr[0];\n const left = arr.slice(1).filter(x => x < p);\n const right = arr.slice(1).filter(x => x >= p);\n return [...quickSort(left), p, ...quickSort(right)];\n}',
'/**\n * 快速排序 (ES6+)\n * @param {Array} arr - 输入数组\n * @returns {Array} - 排序后的新数组\n */\nconst quickSort = (arr) => {\n // 🛡️ 边界检查\n if (!Array.isArray(arr)) throw new Error("Input must be an array");\n if (arr.length <= 1) return arr;\n\n const pivot = arr[0];\n const left = [];\n const right = [];\n\n // 分区\n for (let i = 1; i < arr.length; i++) {\n arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i]);\n }\n\n return [...quickSort(left), pivot, ...quickSort(right)];\n};\n\n// ✅ 测试用例\nconsole.log(quickSort([3, 1, 4, 1, 5, 9])); // [1, 1, 3, 4, 5, 9]'
]
})
}
const feedbackConfig = {
copy: [
{ title: '太泛了', text: 'AI 不知道你要什么风格,只能给你“说明书”式的文案。' },
{ title: '好多了', text: '有了风格和卖点,AI 知道怎么“说话”了,但结构还不够抓人。' },
{ title: '专业级', text: '指定了角色和结构(痛点-卖点),输出逻辑清晰,转化率更高。' }
],
summary: [
{ title: '抓不住重点', text: '没有字数和格式限制,AI 可能会罗嗦一大堆。' },
{ title: '清晰明了', text: '限制了字数和要点数量,可读性大幅提升。' },
{ title: '结构化交付', text: '指定 Markdown 格式和角色,直接可用,无需二次编辑。' }
],
code: [
{ title: '不可用', text: '可能偷懒用内置函数,或者缺少注释,难以维护。' },
{ title: '可用', text: '代码正确,有基本注释,但缺乏健壮性考虑。' },
{ title: '生产级', text: '考虑了边界情况和类型检查,直接复制就能进项目。' }
]
}
const getFeedbackForLevel = (l) => feedbackConfig[taskId.value][l]
// 获取某等级的输出(如果是当前等级正在运行,显示实时打字内容;否则显示历史记录)
const getOutputForLevel = (l) => {
if (l === level.value && isRunning.value) return displayedOutput.value
return outputs.value[l] || ''
}
const renderMarkdown = (text) => {
if (!text) return ''
// 1. HTML Escape (Basic)
let html = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
// 2. Bold: **text** -> <strong>text</strong>
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
return html
}
const run = () => {
if (isRunning.value) return
// 直接显示结果,不进行模拟等待
hasRun.value = true
viewLevel.value = level.value // 强制看当前
const fullText = outputConfig[taskId.value][level.value]
displayedOutput.value = fullText
outputs.value[level.value] = fullText
isRunning.value = false
}
</script>
<style scoped>
.quick {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 16px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 12px;
.quick-start-demo-container {
margin: 24px 0;
}
.header {
.quick-start-card {
border-radius: 12px;
overflow: visible; /* Allow selects to overflow if needed, though el-select uses popper */
}
.header-content {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
flex-wrap: wrap;
gap: 16px;
}
.title {
font-size: 1.2rem;
font-weight: 800;
margin-bottom: 4px;
background: linear-gradient(120deg, var(--vp-c-brand) 30%, var(--vp-c-brand-dark));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 0.9rem;
color: var(--vp-c-text-2);
font-size: 13px;
}
.controls {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
select {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 8px 10px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.levels {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.level {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 8px 12px;
border-radius: 999px;
cursor: pointer;
}
.level.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
.label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.panel {
.game-area {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.game-area {
grid-template-columns: 1fr;
}
}
/* 左侧构建区 */
.prompt-builder {
display: flex;
flex-direction: column;
gap: 16px;
}
.section-title {
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
color: var(--vp-c-text-2);
letter-spacing: 0.5px;
display: flex;
justify-content: space-between;
align-items: center;
}
.prompt-box {
background: var(--vp-c-bg-alt);
border: 2px dashed var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
min-height: 140px;
display: flex;
flex-direction: column;
gap: 8px;
transition: all 0.3s;
}
.block {
display: flex;
gap: 10px;
padding: 10px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 12px;
align-items: flex-start;
}
.panel-title {
font-weight: 700;
margin-bottom: 6px;
.block.base {
border-left: 3px solid var(--vp-c-text-2);
}
pre {
margin: 0;
background: #0b1221;
color: #e5e7eb;
border-radius: 8px;
padding: 12px;
font-family: var(--vp-font-family-mono);
font-size: 13px;
overflow-x: auto;
.block.clear {
background: rgba(var(--vp-c-brand-rgb), 0.05);
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.2);
border-left: 3px solid var(--vp-c-brand);
}
.block.pro {
background: rgba(100, 108, 255, 0.05); /* Indigo-ish */
border: 1px solid rgba(100, 108, 255, 0.2);
border-left: 3px solid #646cff;
}
.block .icon {
font-size: 1.2rem;
}
.block .text {
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
}
.output {
color: var(--vp-c-text-1);
.animate-in {
animation: slideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.upgrade-controls {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--vp-c-bg-alt);
padding: 12px 16px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.level-info {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.level-desc {
font-size: 0.9rem;
font-weight: 700;
}
/* 右侧预览区 */
.chat-preview {
display: flex;
flex-direction: column;
gap: 16px;
}
.chat-window {
background: var(--vp-c-bg-alt);
border-radius: 12px;
padding: 20px;
min-height: 300px;
display: flex;
flex-direction: column;
gap: 16px;
border: 1px solid var(--vp-c-divider);
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 200px;
}
.message-bubble {
display: flex;
gap: 12px;
align-items: flex-start;
}
.avatar {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
flex-shrink: 0;
}
.content {
background: var(--vp-c-bg);
padding: 12px 16px;
border-radius: 0 12px 12px 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
max-width: 100%;
position: relative;
}
.markdown-body {
white-space: pre-wrap;
line-height: 1.6;
}
.hint {
margin-top: 6px;
color: var(--vp-c-text-2);
font-size: 13px;
.message-bubble.typing .content {
min-width: 60px;
}
.tips {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
.typing-indicator span {
display: inline-block;
width: 6px;
height: 6px;
background: var(--vp-c-text-2);
border-radius: 50%;
margin: 0 2px;
animation: bounce 1.4s infinite ease-in-out both;
}
.tip {
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 10px;
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.tip-title {
.feedback-bubble {
background: rgba(var(--vp-c-yellow-rgb), 0.1);
border: 1px solid rgba(var(--vp-c-yellow-rgb), 0.3);
padding: 12px;
border-radius: 8px;
margin-top: auto;
}
.feedback-title {
font-weight: 700;
color: var(--vp-c-yellow-1);
margin-bottom: 4px;
font-size: 0.9rem;
}
.tip-body {
color: var(--vp-c-text-2);
font-size: 13px;
line-height: 1.5;
.feedback-text {
font-size: 0.9rem;
color: var(--vp-c-text-1);
line-height: 1.4;
}
</style>
.animate-pop {
animation: popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes popIn {
from { opacity: 0; transform: scale(0.9) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
</style>