Skip to content

Commit 2c67ddc

Browse files
sunweiyuecursoragent
andcommitted
feat(demo): add anchor links and copy-link button for each case
- Each case has id="case-{caseId}" anchor target - Copy link button copies full URL with #hash to clipboard - Tab click updates URL hash via replaceState - Page load / hashchange auto-navigates to target case - Works on both localhost and deployed GitHub Pages Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b97bc8f commit 2c67ddc

File tree

1 file changed

+95
-2
lines changed

1 file changed

+95
-2
lines changed

minicpm-o-4_5/index.html

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,36 @@
385385
color: var(--text-secondary);
386386
}
387387

388+
/* === Copy Link Button === */
389+
.case-header {
390+
display: flex;
391+
justify-content: flex-end;
392+
margin-bottom: 4px;
393+
}
394+
395+
.copy-link-btn {
396+
background: none;
397+
border: 1px solid var(--border);
398+
border-radius: var(--radius-sm);
399+
padding: 3px 10px;
400+
font-size: 0.7rem;
401+
color: var(--text-muted);
402+
cursor: pointer;
403+
transition: all 0.15s;
404+
letter-spacing: 0.02em;
405+
}
406+
407+
.copy-link-btn:hover {
408+
color: var(--text-primary);
409+
border-color: var(--text-secondary);
410+
background: var(--bg-body);
411+
}
412+
413+
.copy-link-btn.copied {
414+
color: #22c55e;
415+
border-color: #22c55e;
416+
}
417+
388418
/* === Footer Disclaimer === */
389419
.footer-disclaimer {
390420
max-width: 900px;
@@ -492,7 +522,9 @@ <h1>MiniCPM-o 4.5 Audio Demo Page</h1>
492522
noData: '数据未加载',
493523
ensureFile: '请确保 data.js 文件存在',
494524
noCases: '暂无案例',
495-
addCases: '请在配置中添加案例'
525+
addCases: '请在配置中添加案例',
526+
copyLink: '🔗 复制链接',
527+
copied: '✓ 已复制'
496528
},
497529
en: {
498530
systemSetting: 'System Setting',
@@ -504,7 +536,9 @@ <h1>MiniCPM-o 4.5 Audio Demo Page</h1>
504536
noData: 'Data not loaded',
505537
ensureFile: 'Please ensure data.js file exists',
506538
noCases: 'No cases available',
507-
addCases: 'Please add cases in configuration'
539+
addCases: 'Please add cases in configuration',
540+
copyLink: '🔗 Link',
541+
copied: '✓ Copied'
508542
}
509543
};
510544

@@ -623,6 +657,7 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
623657
</div>
624658
${cases.map(c => `
625659
<div class="case-content ${c.id === activeCase ? 'active' : ''}"
660+
id="case-${c.id}"
626661
data-section="${sectionId}" data-case="${c.id}">
627662
${renderCaseContent(c)}
628663
</div>
@@ -635,6 +670,9 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
635670

636671
function renderCaseContent(caseData) {
637672
return `
673+
<div class="case-header">
674+
<button class="copy-link-btn" data-copy-case="${caseData.id}">${t('copyLink')}</button>
675+
</div>
638676
${renderSystemBlock(caseData.system)}
639677
${renderConversation(caseData.turns)}
640678
`;
@@ -731,6 +769,25 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
731769
document.querySelectorAll(`.case-content[data-section="${sectionId}"]`).forEach(c => {
732770
c.classList.toggle('active', c.dataset.case === caseId);
733771
});
772+
773+
// Update URL hash (不触发 scroll)
774+
history.replaceState(null, '', '#' + caseId);
775+
});
776+
});
777+
778+
// Copy link buttons
779+
document.querySelectorAll('.copy-link-btn[data-copy-case]').forEach(btn => {
780+
btn.addEventListener('click', () => {
781+
const caseId = btn.dataset.copyCase;
782+
const url = window.location.origin + window.location.pathname + '#' + caseId;
783+
navigator.clipboard.writeText(url).then(() => {
784+
btn.classList.add('copied');
785+
btn.textContent = t('copied');
786+
setTimeout(() => {
787+
btn.classList.remove('copied');
788+
btn.textContent = t('copyLink');
789+
}, 1500);
790+
});
734791
});
735792
});
736793

@@ -742,6 +799,30 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
742799
});
743800
}
744801

802+
// === Hash Navigation ===
803+
function navigateToCase(caseId) {
804+
const el = document.getElementById('case-' + caseId);
805+
if (!el) return false;
806+
807+
const sectionId = el.dataset.section;
808+
809+
// Activate tab
810+
state.activeCase[sectionId] = caseId;
811+
const tabGroup = document.querySelector(`.tab-group[data-section="${sectionId}"]`);
812+
if (tabGroup) {
813+
tabGroup.querySelectorAll('.tab-btn').forEach(b => {
814+
b.classList.toggle('active', b.dataset.case === caseId);
815+
});
816+
}
817+
document.querySelectorAll(`.case-content[data-section="${sectionId}"]`).forEach(c => {
818+
c.classList.toggle('active', c.dataset.case === caseId);
819+
});
820+
821+
// Scroll into view
822+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
823+
return true;
824+
}
825+
745826
// === Init ===
746827
document.addEventListener('DOMContentLoaded', () => {
747828
// Init lang buttons
@@ -750,6 +831,18 @@ <h3 class="subsection-title">${getText(sub.name)}</h3>
750831
});
751832
document.documentElement.lang = state.lang === 'zh' ? 'zh-CN' : 'en';
752833
render();
834+
835+
// Handle hash navigation on load
836+
if (window.location.hash) {
837+
const caseId = window.location.hash.slice(1);
838+
setTimeout(() => navigateToCase(caseId), 150);
839+
}
840+
});
841+
842+
// Handle hash changes (e.g. back/forward)
843+
window.addEventListener('hashchange', () => {
844+
const caseId = window.location.hash.slice(1);
845+
if (caseId) navigateToCase(caseId);
753846
});
754847
</script>
755848
</body>

0 commit comments

Comments
 (0)