Unify API port to 3002 and add video playback to search page

- Change localhost:3003 to localhost:3002 in demo pages
- Add video playback modal to page-api-demo-query.php
- Enhance search results with clickable video chunks
- Update search functions to support identity and universal search
This commit is contained in:
OpenCode
2026-05-30 19:32:46 +08:00
parent 48ef06a07a
commit 39b9fd7da8
4 changed files with 335 additions and 22 deletions

View File

@@ -165,6 +165,24 @@ get_header();
</div>
</div>
<!-- 视频播放模态窗口 -->
<div id="video-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<span class="close-btn">&times;</span>
</div>
<div class="modal-body">
<video id="video-player" controls width="100%">
<source src="" type="video/mp4">
</video>
<div class="video-info">
<div class="chunk-time"></div>
<div class="chunk-text"></div>
</div>
</div>
</div>
</div>
<style>
.workflow-container {
margin-top: 1rem;
@@ -337,10 +355,68 @@ get_header();
padding: 1rem;
border-radius: 4px;
}
/* 视频播放模态窗口 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
}
.modal-content {
margin: 5% auto;
width: 80%;
max-width: 1200px;
background: #1a1a1a;
border-radius: 8px;
overflow: hidden;
}
.modal-header {
padding: 15px;
background: #2d2d2d;
display: flex;
justify-content: space-between;
}
.close-btn {
color: #fff;
font-size: 28px;
cursor: pointer;
}
#video-player {
width: 100%;
max-height: 70vh;
}
.video-info {
padding: 15px;
background: #2d2d2d;
color: #fff;
}
.chunk-item.clickable {
cursor: pointer;
transition: background 0.2s;
}
.chunk-item.clickable:hover {
background: #dbeafe;
}
.play-btn {
margin-top: 8px;
padding: 4px 12px;
background: var(--primary-color);
color: white;
border-radius: 4px;
font-size: 0.8rem;
border: none;
cursor: pointer;
}
</style>
<script>
const API_BASE = 'http://localhost:3003/api/v1';
const API_BASE = 'http://localhost:3002/api/v1';
const API_KEY = 'muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69';
let workflowFileUuid = null;
function showResult(elementId, data) {
@@ -349,6 +425,60 @@ function showResult(elementId, data) {
el.classList.add('has-content');
}
function playVideoChunk(fileUuid, startTime, endTime, text) {
const videoUrl = `${API_BASE}/file/${fileUuid}/video?api_key=${API_KEY}&start_time=${startTime}&end_time=${endTime}`;
const modal = document.getElementById('video-modal');
const video = document.getElementById('video-player');
video.pause();
video.src = videoUrl;
video.load();
document.querySelector('.chunk-time').textContent =
`${startTime.toFixed(1)}s - ${endTime.toFixed(1)}s`;
document.querySelector('.chunk-text').textContent = text || '';
modal.style.display = 'block';
video.onloadeddata = () => {
video.play().catch(e => console.warn('playVideoChunk: autoplay blocked', e));
};
video.onerror = () => {
console.error('playVideoChunk: video error', video.error);
};
}
function closeVideoModal() {
const video = document.getElementById('video-player');
document.getElementById('video-modal').style.display = 'none';
video.pause();
video.removeAttribute('src');
video.load();
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('#video-modal .close-btn').onclick = closeVideoModal;
window.onclick = (event) => {
if (event.target === document.getElementById('video-modal')) {
closeVideoModal();
}
};
document.getElementById('search_details_content').addEventListener('click', function(e) {
const item = e.target.closest('.chunk-item.clickable');
if (!item) return;
const fu = item.dataset.fileUuid;
const st = parseFloat(item.dataset.startTime);
const et = parseFloat(item.dataset.endTime);
const txt = item.dataset.text || '';
if (fu && !isNaN(st) && !isNaN(et)) {
playVideoChunk(fu, st, et, txt);
}
});
});
function updateStep(step, status, isCompleted = false) {
const stepEl = document.getElementById(`step${step}`);
const statusEl = document.getElementById(`step${step}_status`);
@@ -569,10 +699,10 @@ async function searchWorkflow() {
if (!query) return alert('請輸入搜尋查詢');
try {
const res = await fetch(`${API_BASE}/search`, {
const res = await fetch(`${API_BASE}/search/universal?api_key=${API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, type: 'chunk' })
body: JSON.stringify({ query, types: ['chunk'] })
});
const data = await res.json();
@@ -582,13 +712,19 @@ async function searchWorkflow() {
<p>涵蓋 ${data.files_count || 0} 個檔案</p>
`;
document.getElementById('search_details_content').innerHTML = (data.results || [])
.map(r => `
<div class="chunk-item">
<div class="chunk-time">${r.file_name} | ${r.start_time?.toFixed(1)}s - ${r.end_time?.toFixed(1)}s</div>
<div>${r.text_content || r.description || '無內容'}</div>
<div class="badge">相似度: ${(r.similarity * 100).toFixed(1)}%</div>
</div>
`).join('') || '<p>沒有結果</p>';
.map(r => {
const textAttr = (r.text || '').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return `<div class="chunk-item clickable"
data-file-uuid="${r.file_uuid}"
data-start-time="${r.start_time || 0}"
data-end-time="${r.end_time || (r.start_time || 0) + 10}"
data-text="${textAttr}">
<div class="chunk-time">${r.file_uuid} | ${(r.start_time || 0).toFixed(1)}s - ${(r.end_time || (r.start_time || 0) + 10).toFixed(1)}s</div>
<div>${r.text || '無內容'}</div>
<div class="badge">相似度: ${(r.score * 100).toFixed(1)}%</div>
<button class="play-btn">▶ 播放片段</button>
</div>`;
}).join('') || '<p>沒有結果</p>';
} catch (e) {
alert('搜尋失敗: ' + e.message);
}