Files
Open-AutoGLM/dashboard/static/video-learning.html
let5sne.win10 b97d3f3a9f Improve Video Learning Agent with action-based detection and analysis toggle
- Change video detection from screenshot hash to action-based (Swipe detection)
- Add enable_analysis toggle to disable VLM screenshot analysis
- Improve task prompt to prevent VLM from stopping prematurely
- Add debug logging for action detection troubleshooting
- Fix ModelResponse attribute error (content -> raw_content)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-10 01:47:09 +08:00

434 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Learning - AutoGLM Dashboard</title>
<!-- Vue.js 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Axios for API requests -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- CSS -->
<link rel="stylesheet" href="/static/css/dashboard.css">
<link rel="stylesheet" href="/static/css/video-learning.css">
</head>
<body>
<div id="app">
<!-- Header -->
<header class="header">
<div class="header-content">
<h1>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="23 7 16 12 23 17 23 7"></polygon>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
</svg>
Video Learning Agent
</h1>
<div class="stats">
<span class="stat" title="Session Status">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
{{ sessionStatus ? sessionStatus.status : 'No Session' }}
</span>
<span class="stat" v-if="sessionStatus" title="Progress">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
{{ sessionStatus.watched_count }} / {{ sessionStatus.target_count }}
</span>
</div>
</div>
<div class="header-actions">
<button @click="goBack" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back
</button>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<!-- Configuration Section -->
<section class="config-section" v-if="!currentSessionId">
<h2>Create Learning Session</h2>
<div class="config-form">
<div class="form-group">
<label>Device</label>
<select v-model="config.deviceId" :disabled="loading">
<option value="">Select a device...</option>
<option v-for="device in devices" :key="device.device_id" :value="device.device_id"
:disabled="!device.is_connected || device.status === 'busy'">
{{ device.device_id }}
{{ !device.is_connected ? '(Disconnected)' : '' }}
{{ device.status === 'busy' ? '(Busy)' : '' }}
</option>
</select>
</div>
<div class="form-group">
<label>Platform</label>
<select v-model="config.platform" :disabled="loading">
<option value="douyin">Douyin (抖音)</option>
<option value="kuaishou">Kuaishou (快手)</option>
<option value="tiktok">TikTok</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>Target Videos</label>
<input type="number" v-model.number="config.targetCount" min="1" max="100" :disabled="loading">
</div>
<div class="form-group">
<label>Watch Duration (s)</label>
<input type="number" v-model.number="config.watchDuration" min="1" max="30" step="0.5" :disabled="loading">
</div>
</div>
<div class="form-group">
<label>Category (Optional)</label>
<input type="text" v-model="config.category" placeholder="e.g., 美食, 旅行, 搞笑" :disabled="loading">
<small>Leave empty to watch recommended videos</small>
</div>
<div class="form-group checkbox-group">
<label>
<input type="checkbox" v-model="config.enableAnalysis" :disabled="loading">
<span>Enable Screenshot Analysis</span>
</label>
<small>Analyze video content using VLM to extract description, likes, comments, tags, etc.</small>
</div>
<button @click="createAndStartSession" class="btn btn-primary" :disabled="loading || !config.deviceId">
<svg v-if="loading" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spinning">
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
</svg>
{{ loading ? 'Creating...' : 'Start Learning' }}
</button>
</div>
</section>
<!-- Session Control Section -->
<section class="session-section" v-if="currentSessionId && sessionStatus">
<div class="session-header">
<h2>Session: {{ currentSessionId }}</h2>
<div class="session-controls">
<button v-if="sessionStatus.is_paused" @click="resumeSession" class="btn btn-primary btn-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
Resume
</button>
<button v-else-if="sessionStatus.is_active" @click="pauseSession" class="btn btn-secondary btn-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="4" width="4" height="16"></rect>
<rect x="14" y="4" width="4" height="16"></rect>
</svg>
Pause
</button>
<button v-if="sessionStatus.is_active" @click="stopSession" class="btn btn-danger btn-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="6" width="12" height="12"></rect>
</svg>
Stop
</button>
</div>
</div>
<!-- Progress Bar -->
<div class="progress-section" v-if="sessionStatus.is_active || sessionStatus.is_paused">
<div class="progress-info">
<span>Progress: {{ sessionStatus.watched_count }} / {{ sessionStatus.target_count }}</span>
<span>{{ Math.round(sessionStatus.progress_percent) }}%</span>
</div>
<div class="progress-bar-large">
<div class="progress-fill" :style="{ width: sessionStatus.progress_percent + '%' }"></div>
</div>
<div class="progress-stats">
<span>Total Duration: {{ formatDuration(sessionStatus.total_duration) }}</span>
</div>
</div>
<!-- Current Video -->
<div class="current-video" v-if="sessionStatus.current_video">
<h3>Current Video</h3>
<div class="video-card">
<div class="video-screenshot" v-if="sessionStatus.current_video.screenshot_path">
<img :src="sessionStatus.current_video.screenshot_path" alt="Current video">
</div>
<div class="video-info">
<div class="video-id">#{{ sessionStatus.current_video.sequence_id }}</div>
<div class="video-description" v-if="sessionStatus.current_video.description">
{{ sessionStatus.current_video.description }}
</div>
<div class="video-stats">
<span v-if="sessionStatus.current_video.likes">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
{{ formatNumber(sessionStatus.current_video.likes) }}
</span>
<span v-if="sessionStatus.current_video.comments">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
</svg>
{{ formatNumber(sessionStatus.current_video.comments) }}
</span>
</div>
</div>
</div>
</div>
<!-- Session Complete -->
<div class="session-complete" v-if="!sessionStatus.is_active && currentSessionId">
<div class="complete-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</div>
<h3>Session Complete!</h3>
<p>Watched {{ sessionStatus.watched_count }} videos in {{ formatDuration(sessionStatus.total_duration) }}</p>
<button @click="resetSession" class="btn btn-primary">Start New Session</button>
</div>
</section>
<!-- Video History -->
<section class="history-section" v-if="videos.length > 0">
<h2>Watched Videos</h2>
<div class="video-grid">
<div v-for="video in videos" :key="video.sequence_id" class="video-card">
<div class="video-screenshot" v-if="video.screenshot_path">
<img :src="video.screenshot_path" :alt="'Video ' + video.sequence_id">
</div>
<div class="video-placeholder" v-else>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect>
<line x1="7" y1="2" x2="7" y2="22"></line>
<line x1="17" y1="2" x2="17" y2="22"></line>
<line x1="2" y1="12" x2="22" y2="12"></line>
<line x1="2" y1="7" x2="7" y2="7"></line>
<line x1="2" y1="17" x2="7" y2="17"></line>
<line x1="17" y1="17" x2="22" y2="17"></line>
<line x1="17" y1="7" x2="22" y2="7"></line>
</svg>
</div>
<div class="video-info">
<div class="video-id">#{{ video.sequence_id }}</div>
<div class="video-description" v-if="video.description">{{ video.description }}</div>
<div class="video-stats">
<span v-if="video.likes">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
{{ formatNumber(video.likes) }}
</span>
<span v-if="video.comments">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
</svg>
{{ formatNumber(video.comments) }}
</span>
</div>
<div class="video-category" v-if="video.category">
<span class="category-badge">{{ video.category }}</span>
</div>
<div class="video-tags" v-if="video.tags && video.tags.length > 0">
<span class="tag" v-for="tag in video.tags" :key="tag">#{{ tag }}</span>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Toast notifications -->
<div class="toast-container">
<div v-for="toast in toasts" :key="toast.id" class="toast" :class="toast.type">
{{ toast.message }}
</div>
</div>
</div>
<script src="/static/js/video-learning.js"></script>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
devices: [],
currentSessionId: null,
sessionStatus: null,
videos: [],
loading: false,
toasts: [],
toastIdCounter: 0,
config: {
deviceId: '',
platform: 'douyin',
targetCount: 10,
category: '',
watchDuration: 3.0,
enableAnalysis: true,
},
};
},
mounted() {
this.loadDevices();
this.setupVideoLearningEvents();
},
methods: {
async loadDevices() {
try {
const response = await axios.get('/api/devices');
this.devices = response.data;
} catch (error) {
this.showToast('Failed to load devices', 'error');
}
},
async createAndStartSession() {
if (!this.config.deviceId) {
this.showToast('Please select a device', 'error');
return;
}
this.loading = true;
try {
// Create session
const createResult = await VideoLearningModule.createSession(
this.config.deviceId,
{
platform: this.config.platform,
targetCount: this.config.targetCount,
category: this.config.category || null,
watchDuration: this.config.watchDuration,
enableAnalysis: this.config.enableAnalysis,
}
);
this.currentSessionId = createResult.session_id;
this.showToast('Session created! Starting...', 'success');
// Start session
await VideoLearningModule.startSession(this.currentSessionId);
this.showToast('Learning session started!', 'success');
// Initial status update
await this.updateSessionStatus();
} catch (error) {
this.showToast('Failed to create session: ' + error.message, 'error');
} finally {
this.loading = false;
}
},
async pauseSession() {
if (!this.currentSessionId) return;
try {
await VideoLearningModule.controlSession(this.currentSessionId, 'pause');
await this.updateSessionStatus();
this.showToast('Session paused', 'info');
} catch (error) {
this.showToast('Failed to pause session', 'error');
}
},
async resumeSession() {
if (!this.currentSessionId) return;
try {
await VideoLearningModule.controlSession(this.currentSessionId, 'resume');
await this.updateSessionStatus();
this.showToast('Session resumed', 'info');
} catch (error) {
this.showToast('Failed to resume session', 'error');
}
},
async stopSession() {
if (!this.currentSessionId) return;
if (!confirm('Are you sure you want to stop this session?')) return;
try {
await VideoLearningModule.controlSession(this.currentSessionId, 'stop');
// Stop polling first
VideoLearningModule.stopPolling();
// Mark session as stopped in UI
if (this.sessionStatus) {
this.sessionStatus.is_active = false;
}
this.showToast('Session stopped', 'info');
} catch (error) {
this.showToast('Failed to stop session', 'error');
}
},
async updateSessionStatus() {
if (!this.currentSessionId) return;
try {
this.sessionStatus = await VideoLearningModule.getSessionStatus(this.currentSessionId);
this.videos = await VideoLearningModule.getSessionVideos(this.currentSessionId);
} catch (error) {
console.error('Error updating session status:', error);
}
},
setupVideoLearningEvents() {
window.addEventListener('videoLearningUpdate', (event) => {
const { status, videos } = event.detail;
this.sessionStatus = status;
this.videos = videos;
});
},
resetSession() {
this.currentSessionId = null;
this.sessionStatus = null;
this.videos = [];
VideoLearningModule.stopPolling();
},
goBack() {
window.location.href = '/';
},
formatDuration(seconds) {
return VideoLearningModule.formatDuration(seconds);
},
formatNumber(num) {
return VideoLearningModule.formatNumber(num);
},
showToast(message, type = 'info') {
const id = this.toastIdCounter++;
this.toasts.push({ id, message, type });
setTimeout(() => {
this.toasts = this.toasts.filter(t => t.id !== id);
}, 3000);
},
},
beforeUnmount() {
VideoLearningModule.stopPolling();
},
}).mount('#app');
</script>
</body>
</html>