Files
sanbuphy 0eba9e87e9 fix(eslint): reduce warnings in GitHub Actions deployment
- Disable formatting rules (handled by Prettier)
- Relaxed strict Vue/JS rules for demo code compatibility
- Fix syntax errors in ApiPlayground and VoiceCloningDemo
- Fix duplicate else-if condition in ApiPlayground
- Fix Promise executor async pattern in AutoregressiveAudioDemo
- Add TypeScript file support to ESLint config

Warnings reduced from 295 to 251 problems.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-18 17:38:10 +08:00

629 lines
18 KiB
Vue
Raw Permalink 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.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<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"
style="width: 160px"
size="large"
@change="reset"
>
<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"
icon="Minus"
@click="downgrade"
>
降级
</el-button>
<el-button
type="primary"
:disabled="level === 2"
icon="Plus"
@click="upgrade"
>
升级
</el-button>
</el-button-group>
</div>
</div>
<el-button
type="primary"
size="large"
:loading="isRunning"
style="width: 100%; font-weight: bold; font-size: 1.1rem;"
@click="run"
>
{{ isRunning ? '生成中...' : '🚀 发送给 AI' }}
</el-button>
</div>
<!-- 右侧AI 模拟输出 -->
<div class="chat-preview">
<div class="section-title">
<span>AI 回复 (Output)</span>
<!-- 历史记录切换 -->
<div
v-if="hasAnyHistory"
class="history-tabs"
>
<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"
style="padding: 0; vertical-align: baseline;"
@click="viewLevel = level"
>
回到当前
</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 />
</div>
<div
v-else
class="markdown-body"
v-html="renderMarkdown(getOutputForLevel(viewLevel))"
/>
</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>
</el-card>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const tasks = [
{ id: 'copy', label: '写小红书文案' },
{ id: 'summary', label: '总结会议纪要' },
{ id: 'code', label: '写代码函数' }
]
const taskId = ref('copy')
const level = ref(0) // 0: vague, 1: clear, 2: pro
const isRunning = ref(false)
const hasRun = ref(false)
const displayedOutput = ref('')
// 存储历史输出:{ 0: "...", 1: "..." }
const outputs = ref({})
const viewLevel = ref(0) // 当前查阅的 Level
const hasAnyHistory = computed(() => Object.keys(outputs.value).length > 0)
const availableLevels = computed(() => Object.keys(outputs.value).map(Number).sort())
const reset = () => {
level.value = 0
hasRun.value = false
displayedOutput.value = ''
outputs.value = {}
viewLevel.value = 0
}
const upgrade = () => {
if (level.value < 2) level.value++
hasRun.value = false
viewLevel.value = level.value // 切换到新等级时,视角跟随
}
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+ 示例:附带一个测试用例'
}
}
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-start-demo-container {
margin: 24px 0;
}
.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;
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);
}
.controls {
display: flex;
align-items: center;
gap: 8px;
}
.label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.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: 6px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
align-items: flex-start;
}
.block.base {
border-left: 3px solid var(--vp-c-text-2);
}
.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;
}
.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: 6px;
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;
}
.message-bubble.typing .content {
min-width: 60px;
}
.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;
}
.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); }
}
.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: 6px;
margin-top: auto;
}
.feedback-title {
font-weight: 700;
color: var(--vp-c-yellow-1);
margin-bottom: 4px;
font-size: 0.9rem;
}
.feedback-text {
font-size: 0.9rem;
color: var(--vp-c-text-1);
line-height: 1.4;
}
.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>