index.html 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>多模态内容分析平台 | AI智能分析</title>
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  9. <style>
  10. @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
  11. body {
  12. font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  13. }
  14. .gradient-bg {
  15. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  16. }
  17. .card-hover {
  18. transition: all 0.3s ease;
  19. }
  20. .card-hover:hover {
  21. transform: translateY(-4px);
  22. box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  23. }
  24. .loading-spinner {
  25. border: 3px solid #f3f3f3;
  26. border-top: 3px solid #667eea;
  27. border-radius: 50%;
  28. width: 40px;
  29. height: 40px;
  30. animation: spin 1s linear infinite;
  31. }
  32. @keyframes spin {
  33. 0% { transform: rotate(0deg); }
  34. 100% { transform: rotate(360deg); }
  35. }
  36. .fade-in {
  37. animation: fadeIn 0.5s ease-in;
  38. }
  39. @keyframes fadeIn {
  40. from { opacity: 0; transform: translateY(10px); }
  41. to { opacity: 1; transform: translateY(0); }
  42. }
  43. .scene-card {
  44. background: linear-gradient(to bottom right, #f8fafc, #f1f5f9);
  45. }
  46. .result-container {
  47. background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
  48. }
  49. .tab-active {
  50. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  51. color: white;
  52. }
  53. .upload-zone {
  54. border: 2px dashed #cbd5e1;
  55. transition: all 0.3s ease;
  56. }
  57. .upload-zone:hover {
  58. border-color: #667eea;
  59. background-color: #f8fafc;
  60. }
  61. .upload-zone.dragover {
  62. border-color: #667eea;
  63. background-color: #eef2ff;
  64. }
  65. /* 三栏布局样式 */
  66. .sidebar {
  67. width: 280px;
  68. height: calc(100vh - 120px);
  69. overflow-y: auto;
  70. }
  71. .main-content {
  72. flex: 1;
  73. height: calc(100vh - 120px);
  74. overflow-y: auto;
  75. }
  76. .config-panel {
  77. width: 320px;
  78. height: calc(100vh - 120px);
  79. overflow-y: auto;
  80. }
  81. .tree-item {
  82. cursor: pointer;
  83. transition: all 0.2s ease;
  84. }
  85. .tree-item:hover {
  86. background-color: #f3f4f6;
  87. }
  88. .tree-item.active {
  89. background-color: #eef2ff;
  90. border-left: 3px solid #667eea;
  91. }
  92. .bucket-header {
  93. cursor: pointer;
  94. transition: all 0.2s ease;
  95. }
  96. .bucket-header:hover {
  97. background-color: #f9fafb;
  98. }
  99. </style>
  100. </head>
  101. <body class="bg-gray-50 min-h-screen">
  102. <!-- Header -->
  103. <header class="gradient-bg text-white shadow-lg">
  104. <div class="container mx-auto px-6 py-6">
  105. <div class="flex items-center justify-between">
  106. <div class="flex items-center space-x-4">
  107. <div class="bg-white bg-opacity-20 backdrop-blur-sm rounded-xl p-3">
  108. <i class="fas fa-brain text-2xl"></i>
  109. </div>
  110. <div>
  111. <h1 class="text-3xl font-bold">多模态智能分析平台</h1>
  112. <p class="text-sm text-purple-100 mt-1">AI-Powered Multi-Modal Content Analysis</p>
  113. </div>
  114. </div>
  115. <div class="hidden md:flex items-center space-x-6 text-sm">
  116. <div class="bg-white bg-opacity-20 backdrop-blur-sm rounded-lg px-4 py-2">
  117. <i class="fas fa-text-width mr-2"></i>文本分析
  118. </div>
  119. <div class="bg-white bg-opacity-20 backdrop-blur-sm rounded-lg px-4 py-2">
  120. <i class="fas fa-image mr-2"></i>图像识别
  121. </div>
  122. <div class="bg-white bg-opacity-20 backdrop-blur-sm rounded-lg px-4 py-2">
  123. <i class="fas fa-video mr-2"></i>视频解析
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. </header>
  129. <!-- Main Content - Three Column Layout -->
  130. <main class="flex h-screen">
  131. <!-- Left Sidebar - File Navigator -->
  132. <aside class="sidebar bg-white border-r border-gray-200 p-4">
  133. <div class="mb-4">
  134. <h2 class="text-lg font-bold text-gray-800 flex items-center">
  135. <i class="fas fa-folder-tree mr-2 text-purple-600"></i>
  136. 文件浏览器
  137. </h2>
  138. <button onclick="loadAllBuckets()" class="mt-2 w-full px-3 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 transition-all">
  139. <i class="fas fa-sync-alt mr-2"></i>刷新
  140. </button>
  141. </div>
  142. <!-- Loading State -->
  143. <div id="sidebar-loading" class="hidden text-center py-8">
  144. <div class="loading-spinner mx-auto mb-2" style="width: 30px; height: 30px;"></div>
  145. <p class="text-sm text-gray-600">加载中...</p>
  146. </div>
  147. <!-- Bucket Tree -->
  148. <div id="bucket-tree" class="space-y-2">
  149. <!-- 动态生成桶和文件 -->
  150. </div>
  151. <!-- Empty State -->
  152. <div id="sidebar-empty" class="hidden text-center py-8">
  153. <i class="fas fa-folder-open text-4xl text-gray-300 mb-2"></i>
  154. <p class="text-sm text-gray-500">暂无文件</p>
  155. </div>
  156. </aside>
  157. <!-- Center - Preview & Results -->
  158. <div class="main-content bg-gray-50 p-6">
  159. <!-- File Preview -->
  160. <div id="preview-section" class="hidden bg-white rounded-xl shadow-lg p-6 mb-6 fade-in">
  161. <div class="flex items-center justify-between mb-4">
  162. <h3 class="text-xl font-bold text-gray-800">
  163. <i class="fas fa-image mr-2 text-purple-600"></i>文件预览
  164. </h3>
  165. <button onclick="clearPreview()" class="text-gray-500 hover:text-red-500">
  166. <i class="fas fa-times-circle text-xl"></i>
  167. </button>
  168. </div>
  169. <div class="bg-gray-100 rounded-lg p-4 flex items-center justify-center" style="min-height: 300px;">
  170. <img id="preview-img" class="max-w-full h-auto rounded-lg" style="max-height: 500px;">
  171. <div id="preview-video-placeholder" class="hidden text-center">
  172. <i class="fas fa-video text-6xl text-gray-400 mb-4"></i>
  173. <p class="text-gray-600">视频文件</p>
  174. <p id="preview-filename" class="text-sm text-gray-500 mt-2"></p>
  175. </div>
  176. </div>
  177. </div>
  178. <!-- Loading State -->
  179. <div id="loading" class="hidden bg-white rounded-xl shadow-lg p-12 text-center mb-6">
  180. <div class="loading-spinner mx-auto mb-4"></div>
  181. <p class="text-lg font-semibold text-gray-700">AI正在分析中...</p>
  182. <p class="text-sm text-gray-500 mt-2">这可能需要几秒到几分钟,请耐心等待</p>
  183. </div>
  184. <!-- Results Section -->
  185. <div id="results" class="hidden bg-white rounded-xl shadow-lg p-6 fade-in">
  186. <!-- Content Type Badge -->
  187. <div class="mb-6">
  188. <span id="content-type-badge" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-gradient-to-r from-purple-500 to-indigo-600 text-white shadow-lg">
  189. <i class="fas fa-check-circle mr-2"></i>分析完成
  190. </span>
  191. </div>
  192. <!-- Main Result -->
  193. <div class="result-container rounded-2xl shadow-xl p-8 mb-8">
  194. <div class="flex items-center mb-4">
  195. <div class="bg-purple-100 rounded-full p-3 mr-4">
  196. <i class="fas fa-sparkles text-purple-600 text-xl"></i>
  197. </div>
  198. <h2 class="text-2xl font-bold text-gray-800">分析结果</h2>
  199. </div>
  200. <div id="main-result" class="text-gray-700 leading-relaxed text-lg whitespace-pre-wrap"></div>
  201. </div>
  202. <!-- Scene Descriptions (for videos) -->
  203. <div id="scenes-container" class="hidden">
  204. <div class="flex items-center mb-6">
  205. <div class="bg-indigo-100 rounded-full p-3 mr-4">
  206. <i class="fas fa-film text-indigo-600 text-xl"></i>
  207. </div>
  208. <h2 class="text-2xl font-bold text-gray-800">镜头分析</h2>
  209. </div>
  210. <div id="scenes-list" class="grid grid-cols-1 md:grid-cols-2 gap-6"></div>
  211. </div>
  212. <!-- Actions -->
  213. <div class="mt-8 flex space-x-4">
  214. <button
  215. onclick="copyResult()"
  216. class="flex-1 bg-white border-2 border-purple-600 text-purple-600 font-semibold py-3 rounded-lg hover:bg-purple-50 transition-all"
  217. >
  218. <i class="fas fa-copy mr-2"></i>复制结果
  219. </button>
  220. <button
  221. onclick="resetForm()"
  222. class="flex-1 gradient-bg text-white font-semibold py-3 rounded-lg hover:opacity-90 transition-all"
  223. >
  224. <i class="fas fa-redo mr-2"></i>新建分析
  225. </button>
  226. </div>
  227. </div>
  228. </div>
  229. <!-- Right Sidebar - Configuration Panel -->
  230. <aside class="config-panel bg-white border-l border-gray-200 p-4">
  231. <div class="mb-4">
  232. <h2 class="text-lg font-bold text-gray-800 flex items-center">
  233. <i class="fas fa-sliders-h mr-2 text-purple-600"></i>
  234. 配置面板
  235. </h2>
  236. </div>
  237. <!-- 视频处理参数 -->
  238. <div class="mb-6 bg-purple-50 rounded-lg p-4">
  239. <h3 class="font-semibold text-gray-700 mb-3 flex items-center">
  240. <i class="fas fa-video mr-2 text-purple-600"></i>视频参数
  241. </h3>
  242. <div class="space-y-3">
  243. <div>
  244. <label class="block text-xs text-gray-600 mb-1">每镜头抽帧数</label>
  245. <input type="number" id="keyframes-per-scene" value="3" min="1" max="10"
  246. class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-sm">
  247. </div>
  248. <div>
  249. <label class="block text-xs text-gray-600 mb-1">抽帧间隔(秒)</label>
  250. <input type="number" id="extract-interval" value="3" min="1" max="10"
  251. class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-sm">
  252. </div>
  253. </div>
  254. </div>
  255. <!-- 提示词配置 -->
  256. <div class="bg-indigo-50 rounded-lg p-4">
  257. <h3 class="font-semibold text-gray-700 mb-3 flex items-center">
  258. <i class="fas fa-comments mr-2 text-indigo-600"></i>提示词
  259. </h3>
  260. <div class="space-y-3">
  261. <div>
  262. <label class="block text-xs text-gray-600 mb-1">文本提示词</label>
  263. <input type="text" id="prompt-text" placeholder="请回答用户的问题。"
  264. class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-xs">
  265. </div>
  266. <div>
  267. <label class="block text-xs text-gray-600 mb-1">图片提示词</label>
  268. <input type="text" id="prompt-image" placeholder="请描述图像中的主要内容..."
  269. class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-xs">
  270. </div>
  271. <div>
  272. <label class="block text-xs text-gray-600 mb-1">视频提示词</label>
  273. <input type="text" id="prompt-video" placeholder="请描述镜头中的主要内容..."
  274. class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-xs">
  275. </div>
  276. </div>
  277. </div>
  278. <!-- 分析按钮 -->
  279. <button
  280. id="analyze-btn"
  281. onclick="analyzeSelectedFile()"
  282. class="w-full mt-6 gradient-bg text-white font-semibold py-3 rounded-lg hover:opacity-90 transition-all shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
  283. disabled
  284. >
  285. <i class="fas fa-robot mr-2"></i>开始分析
  286. </button>
  287. </aside>
  288. </main>
  289. <script>
  290. let selectedFile = null;
  291. let selectedFileUrl = null;
  292. let allFiles = [];
  293. const API_BASE_URL = 'http://127.0.0.1:8000';
  294. // 页面加载时自动加载文件
  295. window.onload = function() {
  296. loadAllBuckets();
  297. };
  298. // 切换URL输入区域
  299. function toggleUrlInput() {
  300. const content = document.getElementById('url-input-content');
  301. const icon = document.getElementById('url-input-icon');
  302. content.classList.toggle('hidden');
  303. if (content.classList.contains('hidden')) {
  304. icon.classList.remove('fa-chevron-up');
  305. icon.classList.add('fa-chevron-down');
  306. } else {
  307. icon.classList.remove('fa-chevron-down');
  308. icon.classList.add('fa-chevron-up');
  309. }
  310. }
  311. // 加载所有桶和文件
  312. async function loadAllBuckets() {
  313. const loading = document.getElementById('sidebar-loading');
  314. const tree = document.getElementById('bucket-tree');
  315. const empty = document.getElementById('sidebar-empty');
  316. loading.classList.remove('hidden');
  317. tree.innerHTML = '';
  318. empty.classList.add('hidden');
  319. try {
  320. const response = await fetch(`${API_BASE_URL}/minio/files`);
  321. if (!response.ok) throw new Error('加载失败');
  322. const data = await response.json();
  323. loading.classList.add('hidden');
  324. if (data.files && data.files.length > 0) {
  325. allFiles = data.files;
  326. // 按桶分组(这里目前只有一个桶,但结构支持多桶)
  327. const buckets = groupFilesByBucket(data.files);
  328. Object.keys(buckets).forEach(bucketName => {
  329. const bucketElement = createBucketTree(bucketName, buckets[bucketName]);
  330. tree.appendChild(bucketElement);
  331. });
  332. } else {
  333. empty.classList.remove('hidden');
  334. }
  335. } catch (error) {
  336. loading.classList.add('hidden');
  337. alert('加载文件失败: ' + error.message);
  338. }
  339. }
  340. // 按桶分组文件
  341. function groupFilesByBucket(files) {
  342. const buckets = {};
  343. files.forEach(file => {
  344. // 从 URL 中提取 bucket 名称
  345. const match = file.url.match(/\/\/[^\/]+\/([^\/]+)\//);
  346. const bucketName = match ? match[1] : 'default';
  347. if (!buckets[bucketName]) {
  348. buckets[bucketName] = [];
  349. }
  350. buckets[bucketName].push(file);
  351. });
  352. return buckets;
  353. }
  354. // 创建桶的树形结构
  355. function createBucketTree(bucketName, files) {
  356. const bucketDiv = document.createElement('div');
  357. bucketDiv.className = 'mb-2';
  358. // 桶头部
  359. const header = document.createElement('div');
  360. header.className = 'bucket-header flex items-center justify-between p-2 rounded-lg';
  361. header.onclick = () => toggleBucket(bucketName);
  362. header.innerHTML = `
  363. <div class="flex items-center">
  364. <i id="bucket-icon-${bucketName}" class="fas fa-chevron-down mr-2 text-gray-500 text-sm transition-transform"></i>
  365. <i class="fas fa-folder text-yellow-500 mr-2"></i>
  366. <span class="font-semibold text-sm text-gray-700">${bucketName}</span>
  367. </div>
  368. <span class="text-xs text-gray-500">${files.length}</span>
  369. `;
  370. // 文件列表
  371. const fileList = document.createElement('div');
  372. fileList.id = `bucket-files-${bucketName}`;
  373. fileList.className = 'ml-4 mt-1 space-y-1';
  374. files.forEach(file => {
  375. const fileItem = createFileTreeItem(file);
  376. fileList.appendChild(fileItem);
  377. });
  378. bucketDiv.appendChild(header);
  379. bucketDiv.appendChild(fileList);
  380. return bucketDiv;
  381. }
  382. // 创建文件树项
  383. function createFileTreeItem(file) {
  384. const item = document.createElement('div');
  385. item.className = 'tree-item flex items-center p-2 rounded text-sm';
  386. item.onclick = () => selectFileFromTree(file, item);
  387. const icon = file.type === 'image' ? 'fa-image' : 'fa-video';
  388. const iconColor = file.type === 'image' ? 'text-blue-500' : 'text-green-500';
  389. item.innerHTML = `
  390. <i class="fas ${icon} ${iconColor} mr-2 text-xs"></i>
  391. <span class="truncate text-gray-700" title="${file.name}">${file.name}</span>
  392. `;
  393. return item;
  394. }
  395. // 切换桶的展开/折叠
  396. function toggleBucket(bucketName) {
  397. const fileList = document.getElementById(`bucket-files-${bucketName}`);
  398. const icon = document.getElementById(`bucket-icon-${bucketName}`);
  399. fileList.classList.toggle('hidden');
  400. if (fileList.classList.contains('hidden')) {
  401. icon.classList.remove('fa-chevron-down');
  402. icon.classList.add('fa-chevron-right');
  403. } else {
  404. icon.classList.remove('fa-chevron-right');
  405. icon.classList.add('fa-chevron-down');
  406. }
  407. }
  408. // 从树形列表选择文件
  409. function selectFileFromTree(file, itemElement) {
  410. // 清除所有active状态
  411. document.querySelectorAll('.tree-item').forEach(item => {
  412. item.classList.remove('active');
  413. });
  414. // 添加active状态
  415. itemElement.classList.add('active');
  416. selectedFile = file;
  417. selectedFileUrl = file.url;
  418. // 显示预览
  419. const previewSection = document.getElementById('preview-section');
  420. const previewImg = document.getElementById('preview-img');
  421. const videoPlaceholder = document.getElementById('preview-video-placeholder');
  422. const previewFilename = document.getElementById('preview-filename');
  423. previewSection.classList.remove('hidden');
  424. if (file.type === 'image') {
  425. previewImg.src = file.url;
  426. previewImg.classList.remove('hidden');
  427. videoPlaceholder.classList.add('hidden');
  428. } else {
  429. previewImg.classList.add('hidden');
  430. videoPlaceholder.classList.remove('hidden');
  431. previewFilename.textContent = file.name;
  432. }
  433. // 启用分析按钮
  434. document.getElementById('analyze-btn').disabled = false;
  435. }
  436. // 清除预览
  437. function clearPreview() {
  438. selectedFile = null;
  439. selectedFileUrl = null;
  440. document.getElementById('preview-section').classList.add('hidden');
  441. document.getElementById('analyze-btn').disabled = true;
  442. // 清除active状态
  443. document.querySelectorAll('.tree-item').forEach(item => {
  444. item.classList.remove('active');
  445. });
  446. }
  447. function formatFileSize(bytes) {
  448. if (bytes < 1024) return bytes + ' B';
  449. if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
  450. return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
  451. }
  452. // API calls
  453. async function analyzeContent() {
  454. const content = document.getElementById('content-input').value.trim();
  455. if (!content) {
  456. alert('请输入内容');
  457. return;
  458. }
  459. // 获取配置参数
  460. const keyframesPerScene = parseInt(document.getElementById('keyframes-per-scene').value) || 3;
  461. const extractInterval = parseInt(document.getElementById('extract-interval').value) || 3;
  462. const promptText = document.getElementById('prompt-text').value.trim() || null;
  463. const promptImage = document.getElementById('prompt-image').value.trim() || null;
  464. const promptVideo = document.getElementById('prompt-video').value.trim() || null;
  465. showLoading();
  466. try {
  467. const response = await fetch(`${API_BASE_URL}/analyze`, {
  468. method: 'POST',
  469. headers: {
  470. 'Content-Type': 'application/json',
  471. },
  472. body: JSON.stringify({
  473. content: content,
  474. // 不发送 content_type,强制后端自动检测
  475. keyframes_per_scene: keyframesPerScene,
  476. extract_interval: extractInterval,
  477. prompt_text: promptText,
  478. prompt_image: promptImage,
  479. prompt_video: promptVideo
  480. })
  481. });
  482. if (!response.ok) {
  483. throw new Error('分析失败');
  484. }
  485. const data = await response.json();
  486. displayResults(data);
  487. } catch (error) {
  488. alert('分析出错: ' + error.message);
  489. hideLoading();
  490. }
  491. }
  492. // 分析选中文件
  493. async function analyzeSelectedFile() {
  494. if (!selectedFileUrl) {
  495. alert('请选择文件');
  496. return;
  497. }
  498. // 从右侧配置面板获取参数
  499. const keyframesPerScene = parseInt(document.getElementById('keyframes-per-scene').value) || 3;
  500. const extractInterval = parseInt(document.getElementById('extract-interval').value) || 3;
  501. const promptText = document.getElementById('prompt-text').value.trim() || null;
  502. const promptImage = document.getElementById('prompt-image').value.trim() || null;
  503. const promptVideo = document.getElementById('prompt-video').value.trim() || null;
  504. showLoading();
  505. try {
  506. const response = await fetch(`${API_BASE_URL}/analyze`, {
  507. method: 'POST',
  508. headers: {
  509. 'Content-Type': 'application/json',
  510. },
  511. body: JSON.stringify({
  512. content: selectedFileUrl,
  513. // 不发送 content_type,强制后端自动检测
  514. keyframes_per_scene: keyframesPerScene,
  515. extract_interval: extractInterval,
  516. prompt_text: promptText,
  517. prompt_image: promptImage,
  518. prompt_video: promptVideo
  519. })
  520. });
  521. if (!response.ok) {
  522. throw new Error('分析失败');
  523. }
  524. const data = await response.json();
  525. displayResults(data);
  526. } catch (error) {
  527. alert('分析出错: ' + error.message);
  528. hideLoading();
  529. }
  530. }
  531. // UI updates
  532. function showLoading() {
  533. document.getElementById('results').classList.add('hidden');
  534. document.getElementById('loading').classList.remove('hidden');
  535. }
  536. function hideLoading() {
  537. document.getElementById('loading').classList.add('hidden');
  538. }
  539. function displayResults(data) {
  540. hideLoading();
  541. // Update badge
  542. const badge = document.getElementById('content-type-badge');
  543. const typeIcons = {
  544. 'text': 'fa-text-width',
  545. 'image': 'fa-image',
  546. 'video': 'fa-video'
  547. };
  548. badge.innerHTML = `<i class="fas ${typeIcons[data.content_type]} mr-2"></i>${data.content_type.toUpperCase()} 分析完成`;
  549. // Display main result
  550. document.getElementById('main-result').textContent = data.result;
  551. // Display scenes if video
  552. const scenesContainer = document.getElementById('scenes-container');
  553. if (data.content_type === 'video' && data.scenes && data.scenes.length > 0) {
  554. scenesContainer.classList.remove('hidden');
  555. const scenesList = document.getElementById('scenes-list');
  556. scenesList.innerHTML = '';
  557. data.scenes.forEach((scene, index) => {
  558. const sceneCard = document.createElement('div');
  559. sceneCard.className = 'scene-card rounded-xl p-6 shadow-lg';
  560. sceneCard.innerHTML = `
  561. <div class="flex items-center mb-3">
  562. <div class="bg-indigo-500 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold mr-3">
  563. ${index + 1}
  564. </div>
  565. <h3 class="font-bold text-gray-800">镜头 ${index + 1}</h3>
  566. </div>
  567. <p class="text-gray-700 leading-relaxed">${scene}</p>
  568. `;
  569. scenesList.appendChild(sceneCard);
  570. });
  571. } else {
  572. scenesContainer.classList.add('hidden');
  573. }
  574. document.getElementById('results').classList.remove('hidden');
  575. }
  576. function copyResult() {
  577. const mainResult = document.getElementById('main-result').textContent;
  578. const scenes = document.getElementById('scenes-container').classList.contains('hidden')
  579. ? ''
  580. : '\n\n镜头分析:\n' + Array.from(document.querySelectorAll('#scenes-list .scene-card p'))
  581. .map((p, i) => `镜头${i+1}: ${p.textContent}`).join('\n');
  582. navigator.clipboard.writeText(mainResult + scenes).then(() => {
  583. alert('结果已复制到剪贴板');
  584. });
  585. }
  586. function resetForm() {
  587. document.getElementById('content-input').value = '';
  588. clearPreview();
  589. document.getElementById('results').classList.add('hidden');
  590. }
  591. </script>
  592. </body>
  593. </html>