Features: - VideoLearningAgent for automated video watching on Douyin/Kuaishou/TikTok - Web dashboard UI for video learning sessions - Real-time progress tracking with screenshot capture - App detection using get_current_app() for accurate recording - Session management with pause/resume/stop controls Technical improvements: - Simplified video detection logic using direct app detection - Full base64 hash for sensitive screenshot change detection - Immediate stop when target video count is reached - Fixed circular import issues with ModelConfig Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
413 lines
20 KiB
HTML
413 lines
20 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>
|
|
|
|
<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>
|
|
</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,
|
|
},
|
|
};
|
|
},
|
|
|
|
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,
|
|
}
|
|
);
|
|
|
|
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');
|
|
await this.updateSessionStatus();
|
|
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>
|