- 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
735 lines
24 KiB
PHP
735 lines
24 KiB
PHP
<?php
|
|
/**
|
|
* Template Name: API Demo - 應用 (Application)
|
|
* Description: Demonstrates practical workflow scenarios combining multiple APIs
|
|
*/
|
|
get_header();
|
|
?>
|
|
|
|
<div class="site-content">
|
|
<h1>Momentry API Demo - 應用</h1>
|
|
<p class="page-description">示範結合多個 API 的實際應用場景與工作流程</p>
|
|
|
|
<div class="api-nav">
|
|
<a href="/api-demo-query/" class="nav-item">查詢</a>
|
|
<a href="/api-demo-display/" class="nav-item">展示</a>
|
|
<a href="/api-demo-operation/" class="nav-item">操作</a>
|
|
<a href="/api-demo-application/" class="nav-item active">應用</a>
|
|
</div>
|
|
|
|
<!-- 應用 1: 完整工作流程 -->
|
|
<div class="api-section">
|
|
<h2>1. 完整工作流程示範</h2>
|
|
<p>從檔案註冊到處理完成,展示完整的端到端工作流程。</p>
|
|
|
|
<div class="workflow-container">
|
|
<div class="workflow-step" id="step1">
|
|
<div class="step-header">
|
|
<span class="step-number">1</span>
|
|
<span class="step-title">註冊檔案</span>
|
|
<span class="step-status" id="step1_status">等待中</span>
|
|
</div>
|
|
<div class="step-content">
|
|
<input type="text" id="workflow_path" placeholder="輸入影片路徑">
|
|
<button class="btn btn-primary" onclick="workflowRegister()">執行</button>
|
|
</div>
|
|
</div>
|
|
<div class="workflow-step" id="step2">
|
|
<div class="step-header">
|
|
<span class="step-number">2</span>
|
|
<span class="step-title">查詢處理狀態</span>
|
|
<span class="step-status" id="step2_status">等待中</span>
|
|
</div>
|
|
<div class="step-content">
|
|
<button class="btn btn-secondary" onclick="workflowCheckStatus()" id="btn_step2" disabled>執行</button>
|
|
<span id="step2_progress"></span>
|
|
</div>
|
|
</div>
|
|
<div class="workflow-step" id="step3">
|
|
<div class="step-header">
|
|
<span class="step-number">3</span>
|
|
<span class="step-title">查詢檢測結果</span>
|
|
<span class="step-status" id="step3_status">等待中</span>
|
|
</div>
|
|
<div class="step-content">
|
|
<button class="btn btn-secondary" onclick="workflowCheckResults()" id="btn_step3" disabled>執行</button>
|
|
<div id="step3_results"></div>
|
|
</div>
|
|
</div>
|
|
<div class="workflow-step" id="step4">
|
|
<div class="step-header">
|
|
<span class="step-number">4</span>
|
|
<span class="step-title">搜尋身份</span>
|
|
<span class="step-status" id="step4_status">等待中</span>
|
|
</div>
|
|
<div class="step-content">
|
|
<button class="btn btn-secondary" onclick="workflowSearchIdentity()" id="btn_step4" disabled>執行</button>
|
|
<div id="step4_results"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 應用 2: 身份追蹤 -->
|
|
<div class="api-section">
|
|
<h2>2. 跨檔案身份追蹤</h2>
|
|
<p>追蹤特定身份在所有檔案中的出現情況,建立身份關聯圖。</p>
|
|
|
|
<div class="input-group">
|
|
<label for="track_uuid">Identity UUID:</label>
|
|
<input type="text" id="track_uuid" placeholder="輸入要追蹤的 identity_uuid">
|
|
<button class="btn btn-primary" onclick="trackIdentity()">開始追蹤</button>
|
|
</div>
|
|
|
|
<div id="tracking_results" class="tracking-results" style="display:none;">
|
|
<div class="tracking-header">
|
|
<h3 id="tracking_identity_name"></h3>
|
|
<span class="badge" id="tracking_file_count"></span>
|
|
</div>
|
|
<div class="tracking-timeline" id="tracking_timeline"></div>
|
|
<div class="tracking-stats" id="tracking_stats"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 應用 3: 檔案遷移示範 -->
|
|
<div class="api-section">
|
|
<h2>3. 檔案遷移與身份繼承示範</h2>
|
|
<p>演示檔案移動後重新註冊,系統如何建立新的 file_uuid 並保留身份綁定。</p>
|
|
|
|
<div class="migration-demo">
|
|
<div class="migration-step">
|
|
<h4>步驟 1: 原始註冊</h4>
|
|
<input type="text" id="migration_original_path" placeholder="原始路徑 /path/to/original.mp4">
|
|
<button class="btn btn-primary" onclick="migrationRegisterOriginal()">註冊原始檔案</button>
|
|
<div class="result-box" id="migration_original_result"></div>
|
|
</div>
|
|
<div class="migration-arrow">↓</div>
|
|
<div class="migration-step">
|
|
<h4>步驟 2: 模擬移動檔案</h4>
|
|
<input type="text" id="migration_new_path" placeholder="新路徑 /path/to/moved.mp4">
|
|
<button class="btn btn-secondary" onclick="migrationSimulateMove()">移動並重新註冊</button>
|
|
<div class="result-box" id="migration_new_result"></div>
|
|
</div>
|
|
<div class="migration-arrow">↓</div>
|
|
<div class="migration-step">
|
|
<h4>步驟 3: 查詢遷移歷史</h4>
|
|
<button class="btn btn-primary" onclick="migrationCheckHistory()">查看歷史</button>
|
|
<div class="result-box" id="migration_history_result"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 應用 4: 批次處理 -->
|
|
<div class="api-section">
|
|
<h2>4. 批次檔案處理</h2>
|
|
<p>一次註冊多個檔案,監控批次處理進度。</p>
|
|
|
|
<div class="batch-container">
|
|
<div class="input-group">
|
|
<label for="batch_paths">檔案路徑列表 (每行一個):</label>
|
|
<textarea id="batch_paths" rows="5" placeholder="/path/to/video1.mp4 /path/to/video2.mp4 /path/to/video3.mp4"></textarea>
|
|
<button class="btn btn-primary" onclick="batchRegister()">批次註冊</button>
|
|
</div>
|
|
<div class="batch-progress" id="batch_progress" style="display:none;">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="batch_progress_fill"></div>
|
|
</div>
|
|
<span id="batch_progress_text">0/0</span>
|
|
</div>
|
|
<div class="batch-results" id="batch_results"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 應用 5: 語義搜尋工作流 -->
|
|
<div class="api-section">
|
|
<h2>5. 語義搜尋與片段提取工作流</h2>
|
|
<p>使用語義搜尋找到相關片段,然後提取詳細資訊。</p>
|
|
|
|
<div class="search-workflow">
|
|
<div class="input-group">
|
|
<label for="semantic_query">搜尋查詢:</label>
|
|
<input type="text" id="semantic_query" placeholder="描述你要尋找的內容,例如: 戴眼鏡的男性在戶外說話">
|
|
<button class="btn btn-primary" onclick="searchWorkflow()">搜尋</button>
|
|
</div>
|
|
<div id="search_workflow_results" class="search-results-grid" style="display:none;">
|
|
<div class="search-summary">
|
|
<h3>搜尋結果摘要</h3>
|
|
<div id="search_summary_content"></div>
|
|
</div>
|
|
<div class="search-details">
|
|
<h3>詳細片段</h3>
|
|
<div id="search_details_content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 视频播放模态窗口 -->
|
|
<div id="video-modal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<span class="close-btn">×</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;
|
|
}
|
|
.workflow-step {
|
|
background: #f8fafc;
|
|
border-radius: var(--border-radius);
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
border-left: 4px solid #e2e8f0;
|
|
}
|
|
.workflow-step.active {
|
|
border-left-color: var(--primary-color);
|
|
background: #eff6ff;
|
|
}
|
|
.workflow-step.completed {
|
|
border-left-color: var(--success-color);
|
|
background: #f0fdf4;
|
|
}
|
|
.step-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.step-number {
|
|
width: 24px;
|
|
height: 24px;
|
|
background: var(--secondary-color);
|
|
color: white;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
.workflow-step.completed .step-number {
|
|
background: var(--success-color);
|
|
}
|
|
.step-title {
|
|
font-weight: 600;
|
|
flex: 1;
|
|
}
|
|
.step-status {
|
|
font-size: 0.75rem;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
background: #e2e8f0;
|
|
}
|
|
.step-content {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
}
|
|
.step-content input {
|
|
flex: 1;
|
|
padding: 0.5rem;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 4px;
|
|
}
|
|
.migration-demo {
|
|
margin-top: 1rem;
|
|
}
|
|
.migration-step {
|
|
background: #f8fafc;
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.migration-arrow {
|
|
text-align: center;
|
|
font-size: 1.5rem;
|
|
color: var(--secondary-color);
|
|
}
|
|
.result-box {
|
|
margin-top: 0.5rem;
|
|
padding: 0.75rem;
|
|
background: #1e293b;
|
|
color: #e2e8f0;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 0.75rem;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
display: none;
|
|
}
|
|
.result-box.has-content {
|
|
display: block;
|
|
}
|
|
.batch-container {
|
|
margin-top: 1rem;
|
|
}
|
|
.batch-container textarea {
|
|
width: 100%;
|
|
padding: 0.5rem;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 0.75rem;
|
|
resize: vertical;
|
|
}
|
|
.batch-progress {
|
|
margin: 1rem 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
.progress-bar {
|
|
flex: 1;
|
|
height: 8px;
|
|
background: #e2e8f0;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: var(--success-color);
|
|
width: 0%;
|
|
transition: width 0.3s;
|
|
}
|
|
.batch-results {
|
|
margin-top: 1rem;
|
|
}
|
|
.batch-item {
|
|
padding: 0.75rem;
|
|
background: #f8fafc;
|
|
border-radius: 4px;
|
|
margin-bottom: 0.5rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.tracking-results {
|
|
margin-top: 1rem;
|
|
}
|
|
.tracking-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.tracking-timeline {
|
|
border-left: 2px solid #e2e8f0;
|
|
padding-left: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.timeline-item {
|
|
margin-bottom: 1rem;
|
|
position: relative;
|
|
}
|
|
.timeline-item::before {
|
|
content: '';
|
|
width: 8px;
|
|
height: 8px;
|
|
background: var(--primary-color);
|
|
border-radius: 50%;
|
|
position: absolute;
|
|
left: -1.35rem;
|
|
top: 0.5rem;
|
|
}
|
|
.search-results-grid {
|
|
margin-top: 1rem;
|
|
display: grid;
|
|
grid-template-columns: 1fr 2fr;
|
|
gap: 1rem;
|
|
}
|
|
.search-summary, .search-details {
|
|
background: #f8fafc;
|
|
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:3002/api/v1';
|
|
const API_KEY = 'muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69';
|
|
let workflowFileUuid = null;
|
|
|
|
function showResult(elementId, data) {
|
|
const el = document.getElementById(elementId);
|
|
el.textContent = JSON.stringify(data, null, 2);
|
|
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`);
|
|
stepEl.classList.remove('active', 'completed');
|
|
if (isCompleted) {
|
|
stepEl.classList.add('completed');
|
|
statusEl.textContent = '完成';
|
|
statusEl.style.background = '#d1fae5';
|
|
statusEl.style.color = '#065f46';
|
|
} else if (status === 'active') {
|
|
stepEl.classList.add('active');
|
|
statusEl.textContent = '執行中';
|
|
statusEl.style.background = '#dbeafe';
|
|
statusEl.style.color = '#1e40af';
|
|
} else {
|
|
statusEl.textContent = status;
|
|
}
|
|
}
|
|
|
|
async function workflowRegister() {
|
|
const path = document.getElementById('workflow_path').value.trim();
|
|
if (!path) return alert('請輸入影片路徑');
|
|
|
|
updateStep(1, 'active');
|
|
try {
|
|
const res = await fetch(`${API_BASE}/register`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ file_path: path })
|
|
});
|
|
const data = await res.json();
|
|
showResult('step1_result', data);
|
|
workflowFileUuid = data.file_uuid || data.uuid;
|
|
updateStep(1, '完成', true);
|
|
document.getElementById('btn_step2').disabled = false;
|
|
} catch (e) {
|
|
updateStep(1, '失敗');
|
|
}
|
|
}
|
|
|
|
async function workflowCheckStatus() {
|
|
if (!workflowFileUuid) return;
|
|
updateStep(2, 'active');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/jobs/${workflowFileUuid}/status`);
|
|
const data = await res.json();
|
|
document.getElementById('step2_progress').textContent = `進度: ${data.progress || 0}%`;
|
|
|
|
if (data.status === 'completed') {
|
|
updateStep(2, '完成', true);
|
|
document.getElementById('btn_step3').disabled = false;
|
|
} else {
|
|
updateStep(2, `狀態: ${data.status || 'processing'}`);
|
|
setTimeout(() => workflowCheckStatus(), 3000);
|
|
}
|
|
} catch (e) {
|
|
updateStep(2, '失敗');
|
|
}
|
|
}
|
|
|
|
async function workflowCheckResults() {
|
|
if (!workflowFileUuid) return;
|
|
updateStep(3, 'active');
|
|
|
|
try {
|
|
const [identitiesRes, chunksRes] = await Promise.all([
|
|
fetch(`${API_BASE}/files/${workflowFileUuid}/identities`),
|
|
fetch(`${API_BASE}/files/${workflowFileUuid}/chunks`)
|
|
]);
|
|
const identities = await identitiesRes.json();
|
|
const chunks = await chunksRes.json();
|
|
|
|
document.getElementById('step3_results').innerHTML = `
|
|
<p>檢測到身份: ${(identities.identities || []).length}</p>
|
|
<p>語義片段: ${(chunks.chunks || []).length}</p>
|
|
`;
|
|
updateStep(3, '完成', true);
|
|
document.getElementById('btn_step4').disabled = false;
|
|
} catch (e) {
|
|
updateStep(3, '失敗');
|
|
}
|
|
}
|
|
|
|
async function workflowSearchIdentity() {
|
|
if (!workflowFileUuid) return;
|
|
updateStep(4, 'active');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/files/${workflowFileUuid}/identities`);
|
|
const data = await res.json();
|
|
document.getElementById('step4_results').innerHTML = (data.identities || [])
|
|
.map(i => `<div class="file-item"><span>${i.name || '未命名'}</span><span class="uuid">${i.uuid}</span></div>`)
|
|
.join('') || '<p>無身份</p>';
|
|
updateStep(4, '完成', true);
|
|
} catch (e) {
|
|
updateStep(4, '失敗');
|
|
}
|
|
}
|
|
|
|
async function trackIdentity() {
|
|
const uuid = document.getElementById('track_uuid').value.trim();
|
|
if (!uuid) return alert('請輸入 identity_uuid');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/identities/${uuid}/files`);
|
|
const data = await res.json();
|
|
|
|
document.getElementById('tracking_results').style.display = 'block';
|
|
document.getElementById('tracking_identity_name').textContent = data.name || '未命名身份';
|
|
document.getElementById('tracking_file_count').textContent = `${(data.files || []).length} 個檔案`;
|
|
|
|
document.getElementById('tracking_timeline').innerHTML = (data.files || [])
|
|
.map(f => `
|
|
<div class="timeline-item">
|
|
<strong>${f.file_name}</strong>
|
|
<div class="uuid">${f.file_uuid}</div>
|
|
<div class="chunk-time">時間: ${f.start_time?.toFixed(1) || '?'}s - ${f.end_time?.toFixed(1) || '?'}s</div>
|
|
</div>
|
|
`).join('') || '<p>無關聯檔案</p>';
|
|
|
|
document.getElementById('tracking_stats').innerHTML = `
|
|
<p>總檢測次數: ${data.total_detections || 0}</p>
|
|
<p>平均品質: ${(data.avg_quality || 0).toFixed(1)}</p>
|
|
<p>覆蓋角度: ${(data.angle_coverage || []).join(', ') || '無資料'}</p>
|
|
`;
|
|
} catch (e) {
|
|
alert('追蹤失敗: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function migrationRegisterOriginal() {
|
|
const path = document.getElementById('migration_original_path').value.trim();
|
|
if (!path) return alert('請輸入原始路徑');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/register`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ file_path: path })
|
|
});
|
|
const data = await res.json();
|
|
showResult('migration_original_result', data);
|
|
} catch (e) {
|
|
showResult('migration_original_result', { error: e.message });
|
|
}
|
|
}
|
|
|
|
async function migrationSimulateMove() {
|
|
const path = document.getElementById('migration_new_path').value.trim();
|
|
if (!path) return alert('請輸入新路徑');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/register`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ file_path: path })
|
|
});
|
|
const data = await res.json();
|
|
showResult('migration_new_result', data);
|
|
} catch (e) {
|
|
showResult('migration_new_result', { error: e.message });
|
|
}
|
|
}
|
|
|
|
async function migrationCheckHistory() {
|
|
const path = document.getElementById('migration_new_path').value.trim();
|
|
if (!path) return alert('請先輸入新路徑');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/files/${path}/history`);
|
|
const data = await res.json();
|
|
showResult('migration_history_result', data);
|
|
} catch (e) {
|
|
showResult('migration_history_result', { error: e.message });
|
|
}
|
|
}
|
|
|
|
async function batchRegister() {
|
|
const paths = document.getElementById('batch_paths').value.trim().split('\n').filter(p => p.trim());
|
|
if (paths.length === 0) return alert('請輸入至少一個檔案路徑');
|
|
|
|
document.getElementById('batch_progress').style.display = 'flex';
|
|
const resultsEl = document.getElementById('batch_results');
|
|
resultsEl.innerHTML = '';
|
|
|
|
let completed = 0;
|
|
for (const path of paths) {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/register`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ file_path: path.trim() })
|
|
});
|
|
const data = await res.json();
|
|
resultsEl.innerHTML += `
|
|
<div class="batch-item">
|
|
<span>${path.trim()}</span>
|
|
<span class="badge-status ${data.status || 'pending'}">${data.file_uuid || 'failed'}</span>
|
|
</div>
|
|
`;
|
|
} catch (e) {
|
|
resultsEl.innerHTML += `
|
|
<div class="batch-item">
|
|
<span>${path.trim()}</span>
|
|
<span class="badge-status error">失敗: ${e.message}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
completed++;
|
|
document.getElementById('batch_progress_fill').style.width = `${(completed / paths.length) * 100}%`;
|
|
document.getElementById('batch_progress_text').textContent = `${completed}/${paths.length}`;
|
|
}
|
|
}
|
|
|
|
async function searchWorkflow() {
|
|
const query = document.getElementById('semantic_query').value.trim();
|
|
if (!query) return alert('請輸入搜尋查詢');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/search/universal?api_key=${API_KEY}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query, types: ['chunk'] })
|
|
});
|
|
const data = await res.json();
|
|
|
|
document.getElementById('search_workflow_results').style.display = 'grid';
|
|
document.getElementById('search_summary_content').innerHTML = `
|
|
<p>找到 ${data.total || 0} 個結果</p>
|
|
<p>涵蓋 ${data.files_count || 0} 個檔案</p>
|
|
`;
|
|
document.getElementById('search_details_content').innerHTML = (data.results || [])
|
|
.map(r => {
|
|
const textAttr = (r.text || '').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
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);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<?php get_footer(); ?>
|