mirror of
https://github.com/OpenBMB/ChatDev.git
synced 2026-04-25 11:18:06 +00:00
411 lines
10 KiB
Vue
Executable File
411 lines
10 KiB
Vue
Executable File
<script setup>
|
|
import { ref, onMounted, nextTick } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import MarkdownIt from 'markdown-it'
|
|
import markdownItAnchor from 'markdown-it-anchor'
|
|
|
|
const route = useRoute()
|
|
|
|
const renderedContent = ref('')
|
|
const currentLang = ref('en') // 'zh' for Chinese, 'en' for English
|
|
const markdownBody = ref(null)
|
|
const md = new MarkdownIt({
|
|
html: true,
|
|
linkify: true,
|
|
typographer: true
|
|
})
|
|
md.use(markdownItAnchor, {
|
|
permalink: markdownItAnchor.permalink.ariaHidden({
|
|
placement: 'before',
|
|
symbol: '#',
|
|
space: true,
|
|
class: 'header-anchor'
|
|
}),
|
|
slugify: s =>
|
|
s
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\u4e00-\u9fa5\s-]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
})
|
|
|
|
const getTutorialFile = () => (currentLang.value === 'en' ? '/tutorial-en.md' : '/tutorial-zh.md')
|
|
|
|
const scrollToHash = () => {
|
|
// Wait for DOM to update, then scroll to hash if present
|
|
nextTick(() => {
|
|
if (route.hash) {
|
|
// Remove the '#' from the hash
|
|
const targetId = route.hash.slice(1)
|
|
const targetElement = document.getElementById(targetId)
|
|
|
|
if (targetElement) {
|
|
setTimeout(() => {
|
|
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
}, 100)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const addCopyButtons = () => {
|
|
nextTick(() => {
|
|
const container = markdownBody.value
|
|
if (!container) return
|
|
const blocks = container.querySelectorAll('pre')
|
|
blocks.forEach((block) => {
|
|
if (block.querySelector('.copy-code-btn')) return
|
|
const button = document.createElement('button')
|
|
button.type = 'button'
|
|
button.className = 'copy-code-btn'
|
|
button.textContent = 'Copy'
|
|
button.addEventListener('click', async () => {
|
|
const code = block.querySelector('code')
|
|
const text = code ? code.innerText : block.innerText
|
|
try {
|
|
await navigator.clipboard.writeText(text)
|
|
button.textContent = 'Copied'
|
|
setTimeout(() => {
|
|
button.textContent = 'Copy'
|
|
}, 1200)
|
|
} catch (error) {
|
|
console.error('Failed to copy code: ', error)
|
|
}
|
|
})
|
|
block.classList.add('has-copy-button')
|
|
block.appendChild(button)
|
|
})
|
|
})
|
|
}
|
|
|
|
const loadTutorial = async () => {
|
|
try {
|
|
const response = await fetch(getTutorialFile())
|
|
if (response.ok) {
|
|
let text = await response.text()
|
|
// Fix media paths to point to /media/ (absolute path from public root)
|
|
// For markdown images:  -> 
|
|
text = text.replace(/\]\(media\//g, '](/media/')
|
|
// For HTML image tags: src="media/file.gif" -> src="/media/file.gif"
|
|
text = text.replace(/src="media\//g, 'src="/media/')
|
|
|
|
renderedContent.value = md.render(text)
|
|
addCopyButtons()
|
|
scrollToHash()
|
|
} else {
|
|
console.error('Failed to fetch tutorial markdown')
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load tutorial:', error)
|
|
}
|
|
}
|
|
|
|
const switchLang = (lang) => {
|
|
if (currentLang.value !== lang) {
|
|
currentLang.value = lang
|
|
loadTutorial()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadTutorial()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="tutorial-view">
|
|
<div class="lang-switch">
|
|
<button :class="{ active: currentLang === 'zh' }" @click="switchLang('zh')">中文</button>
|
|
<button :class="{ active: currentLang === 'en' }" @click="switchLang('en')">English</button>
|
|
</div>
|
|
<div ref="markdownBody" class="markdown-body" v-html="renderedContent"></div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.tutorial-view {
|
|
padding: 40px;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
margin: 0 auto;
|
|
height: calc(100vh - 55px); /* Adjust the height of the .sidebar */
|
|
background: linear-gradient(135deg, #232526 0%, #1e1e1e 100%);
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
box-sizing: border-box;
|
|
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
/* Glowing border and card shadow */
|
|
border-radius: 18px;
|
|
box-shadow: 0 4px 32px 0 rgba(0, 255, 255, 0.08), 0 0 0 2px #00eaff33;
|
|
border: 1.5px solid #00eaff33;
|
|
transition: box-shadow 0.3s, height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
:global(body.nav-hidden .tutorial-view) {
|
|
height: 100vh;
|
|
}
|
|
|
|
.lang-switch {
|
|
position: absolute;
|
|
top: 80px; /* 55px nav + 25px spacing */
|
|
right: 48px;
|
|
z-index: 10;
|
|
display: flex;
|
|
gap: 12px;
|
|
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
:global(body.nav-hidden .lang-switch) {
|
|
top: 25px; /* Adjust to 25px spacing from top of screen when nav is hidden */
|
|
}
|
|
.lang-switch button {
|
|
background: linear-gradient(90deg, #232526 60%, #00eaff22 100%);
|
|
color: #00eaff;
|
|
border: 1.5px solid #00eaff33;
|
|
border-radius: 8px;
|
|
padding: 6px 18px;
|
|
font-size: 1em;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
transition: background 0.2s, color 0.2s, border 0.2s, box-shadow 0.2s;
|
|
box-shadow: 0 2px 8px #00eaff11;
|
|
}
|
|
.lang-switch button.active,
|
|
.lang-switch button:hover {
|
|
background: linear-gradient(90deg, #00eaff 0%, #232526 100%);
|
|
color: #fff;
|
|
border: 1.5px solid #00eaff;
|
|
box-shadow: 0 0 12px #00eaff44;
|
|
}
|
|
|
|
:deep(.markdown-body) {
|
|
max-width: 980px;
|
|
margin: 0 auto;
|
|
color: #e0e0e0;
|
|
font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', 'monospace', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
line-height: 1.7;
|
|
letter-spacing: 0.01em;
|
|
background: rgba(30,34,40,0.85);
|
|
border-radius: 14px;
|
|
box-shadow: 0 2px 16px 0 rgba(0,255,255,0.04);
|
|
padding: 32px 36px 32px 36px;
|
|
border: 1px solid #00eaff22;
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
|
|
:deep(.markdown-body h1),
|
|
:deep(.markdown-body h2),
|
|
:deep(.markdown-body h3),
|
|
:deep(.markdown-body h4),
|
|
:deep(.markdown-body h5),
|
|
:deep(.markdown-body h6) {
|
|
margin-top: 24px;
|
|
margin-bottom: 16px;
|
|
font-weight: 700;
|
|
line-height: 1.25;
|
|
color: #00eaff;
|
|
text-shadow: 0 0 8px #00eaff44;
|
|
letter-spacing: 0.02em;
|
|
scroll-margin-top: 20px; /* Offset for hash scroll target */
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
/* Highlight targeted heading */
|
|
:deep(.markdown-body h1:target),
|
|
:deep(.markdown-body h2:target),
|
|
:deep(.markdown-body h3:target),
|
|
:deep(.markdown-body h4:target),
|
|
:deep(.markdown-body h5:target),
|
|
:deep(.markdown-body h6:target) {
|
|
background: rgba(0, 234, 255, 0.15);
|
|
padding: 8px 12px;
|
|
margin-left: -12px;
|
|
margin-right: -12px;
|
|
border-radius: 6px;
|
|
box-shadow: 0 0 16px rgba(0, 234, 255, 0.3);
|
|
}
|
|
|
|
:deep(.markdown-body h1) { font-size: 2.2em; border-bottom: 1px solid #00eaff33; padding-bottom: 0.3em; }
|
|
:deep(.markdown-body h2) { font-size: 1.6em; border-bottom: 1px solid #00eaff22; padding-bottom: 0.3em; }
|
|
:deep(.markdown-body h3) { font-size: 1.3em; }
|
|
|
|
:deep(.markdown-body p) {
|
|
margin-top: 0;
|
|
margin-bottom: 16px;
|
|
color: #b8eaff;
|
|
font-size: 1.08em;
|
|
}
|
|
|
|
:deep(.markdown-body a) {
|
|
color: #00eaff;
|
|
text-decoration: none;
|
|
border-bottom: 1px dashed #00eaff99;
|
|
transition: color 0.2s, border-bottom 0.2s;
|
|
}
|
|
:deep(.markdown-body a:hover) {
|
|
color: #fff;
|
|
border-bottom: 1px solid #00eaff;
|
|
text-shadow: 0 0 6px #00eaff99;
|
|
}
|
|
|
|
:deep(.markdown-body img) {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
box-sizing: border-box;
|
|
background-color: transparent;
|
|
border-radius: 10px;
|
|
border: 1.5px solid #00eaff33;
|
|
box-shadow: 0 2px 16px 0 #00eaff22;
|
|
margin: 16px 0;
|
|
transition: box-shadow 0.2s;
|
|
}
|
|
:deep(.markdown-body img:hover) {
|
|
box-shadow: 0 0 24px 2px #00eaff88;
|
|
}
|
|
|
|
:deep(.markdown-body video) {
|
|
max-width: 100%;
|
|
border-radius: 10px;
|
|
border: 1.5px solid #00eaff33;
|
|
margin-bottom: 16px;
|
|
box-shadow: 0 2px 16px 0 #00eaff22;
|
|
}
|
|
|
|
:deep(.markdown-body code) {
|
|
padding: 0.2em 0.5em;
|
|
margin: 0;
|
|
font-size: 90%;
|
|
background: linear-gradient(90deg, #232526 60%, #00eaff22 100%);
|
|
border-radius: 6px;
|
|
color: #00eaff;
|
|
font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', 'monospace';
|
|
border: 1px solid #00eaff33;
|
|
box-shadow: 0 0 8px #00eaff22;
|
|
}
|
|
|
|
:deep(.markdown-body pre) {
|
|
padding: 18px 20px;
|
|
overflow: auto;
|
|
font-size: 95%;
|
|
line-height: 1.55;
|
|
background: linear-gradient(90deg, #161b22 80%, #00eaff11 100%);
|
|
border-radius: 10px;
|
|
margin-bottom: 18px;
|
|
border: 1.5px solid #00eaff33;
|
|
box-shadow: 0 0 16px #00eaff11;
|
|
position: relative;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
:deep(.markdown-body pre code) {
|
|
display: inline;
|
|
padding: 0;
|
|
margin: 0;
|
|
overflow: visible;
|
|
line-height: inherit;
|
|
word-wrap: normal;
|
|
background: transparent;
|
|
border: 0;
|
|
color: #00eaff;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
:deep(.markdown-body pre.has-copy-button) {
|
|
padding-top: 44px;
|
|
}
|
|
|
|
:deep(.markdown-body .copy-code-btn) {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: linear-gradient(90deg, #232526 60%, #00eaff22 100%);
|
|
color: #00eaff;
|
|
border: 1px solid #00eaff44;
|
|
border-radius: 6px;
|
|
padding: 4px 10px;
|
|
font-size: 0.85em;
|
|
cursor: pointer;
|
|
transition: background 0.2s, color 0.2s, border 0.2s, box-shadow 0.2s;
|
|
box-shadow: 0 2px 8px #00eaff11;
|
|
}
|
|
|
|
:deep(.markdown-body .copy-code-btn:hover) {
|
|
background: linear-gradient(90deg, #00eaff 0%, #232526 100%);
|
|
color: #fff;
|
|
border: 1px solid #00eaff;
|
|
box-shadow: 0 0 12px #00eaff44;
|
|
}
|
|
|
|
:deep(.markdown-body blockquote) {
|
|
padding: 0 1em;
|
|
color: #8bffe6;
|
|
border-left: 0.25em solid #00eaff99;
|
|
margin: 0 0 16px 0;
|
|
background: rgba(0,234,255,0.04);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
:deep(.markdown-body hr) {
|
|
height: 0.25em;
|
|
padding: 0;
|
|
margin: 24px 0;
|
|
background: linear-gradient(90deg, #00eaff33 0%, #232526 100%);
|
|
border: 0;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
:deep(.markdown-body table) {
|
|
border-spacing: 0;
|
|
border-collapse: collapse;
|
|
margin-top: 0;
|
|
margin-bottom: 16px;
|
|
width: 100%;
|
|
overflow: auto;
|
|
background: rgba(0,234,255,0.02);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
:deep(.markdown-body table th),
|
|
:deep(.markdown-body table td) {
|
|
padding: 8px 15px;
|
|
border: 1px solid #00eaff22;
|
|
}
|
|
|
|
:deep(.markdown-body table th) {
|
|
font-weight: 700;
|
|
background: rgba(0,234,255,0.08);
|
|
color: #00eaff;
|
|
}
|
|
|
|
:deep(.markdown-body table tr) {
|
|
background: rgba(30,34,40,0.7);
|
|
border-top: 1px solid #00eaff11;
|
|
}
|
|
|
|
:deep(.markdown-body table tr:nth-child(2n)) {
|
|
background: rgba(0,234,255,0.03);
|
|
}
|
|
|
|
/* Custom Scrollbar */
|
|
.tutorial-view::-webkit-scrollbar {
|
|
width: 10px;
|
|
background: #232526;
|
|
}
|
|
.tutorial-view::-webkit-scrollbar-thumb {
|
|
background: linear-gradient(135deg, #00eaff 0%, #232526 100%);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
:deep(.markdown-body::-webkit-scrollbar) {
|
|
height: 8px;
|
|
background: #232526;
|
|
}
|
|
:deep(.markdown-body::-webkit-scrollbar-thumb) {
|
|
background: linear-gradient(90deg, #00eaff 0%, #232526 100%);
|
|
border-radius: 8px;
|
|
}
|
|
</style>
|