This commit is contained in:
@@ -114,6 +114,7 @@
|
||||
- Phase 53 manual sample candidate queue approval:新增 `/api/market_intel/manual_sample_review/candidate_queue_approval` POST 與 UI 送審 gate 按鈕,將 queue draft row preview 對齊既有 `market_alert_review_queue` 契約,檢查必填欄位、寫入 flags、備份與人工批准 gate;不建立 approval record、不寫 review queue、不開 DB transaction、不掛 scheduler;版本同步至 V10.225。
|
||||
- V10.226 補 PPT 視覺 QA runtime checklist:`/observability/ppt_audit_history` 在視覺模型未就緒時顯示 Feature Flag、LibreOffice、Vision Model 三段檢查與下一步操作,避免只看到「停用」而不知道卡在哪。
|
||||
- Phase 54 manual sample candidate queue transaction:新增 `/api/market_intel/manual_sample_review/candidate_queue_transaction` POST 與 UI transaction preview 按鈕,將 queue row preview 轉成 `market_alert_review_queue` idempotent insert statement、payload hash 與 rollback plan;不開 DB connection、不開 transaction、不 commit、不建立 approval record;版本同步至 V10.227。
|
||||
- V10.228 補 PPT 視覺 QA 背景狀態卡:新增 `/observability/ppt_audit/vision_status` 與頁面 Vision QA 狀態卡,讓立即視覺 QA 排入後可看 queued/running/completed/error 與最近審核摘要,不必刷新猜測。
|
||||
- Schema smoke:`tests/test_market_intel_skeleton.py` 檢查 `Base.metadata` 內含 ADR-035 八張 `market_*` tables。
|
||||
- Desktop UI QA:本機只註冊 `market_intel_bp` 的 Flask harness 載入 `/market_intel`,確認 Phase 15、候選預覽、writer preview、安全 flags、點陣暖紙視覺正常,console error 0。
|
||||
- API QA:`/api/market_intel/schema_smoke` 通過 7 張表與 `market_platforms` 必要欄位檢查;`/api/market_intel/platform_seed_writer_plan` 回傳 4 筆 dry-run upsert preview,`writes_executed=false`,四平台皆 `blocked_dry_run_only`。
|
||||
|
||||
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.227"
|
||||
SYSTEM_VERSION = "V10.228"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -1973,6 +1973,18 @@ def ppt_audit_run_vision():
|
||||
return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500
|
||||
|
||||
|
||||
@admin_observability_bp.route('/ppt_audit/vision_status')
|
||||
@login_required
|
||||
def ppt_audit_vision_status():
|
||||
"""Expose current/last background PPT vision audit status for the admin UI."""
|
||||
try:
|
||||
from services.ppt_vision_service import get_ppt_vision_audit_status
|
||||
|
||||
return jsonify(get_ppt_vision_audit_status())
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500
|
||||
|
||||
|
||||
def _resolve_ppt_report_path(filename: str):
|
||||
"""在 REPORTS_DIR 內解析簡報檔名,並阻擋路徑逃逸。"""
|
||||
import os
|
||||
@@ -3137,11 +3149,20 @@ def ppt_audit_history():
|
||||
'next_actions': ['確認 ppt_vision_service import 與 runtime 設定後重新整理此頁。'],
|
||||
}
|
||||
try:
|
||||
from services.ppt_vision_service import get_ppt_vision_runtime_status
|
||||
from services.ppt_vision_service import get_ppt_vision_audit_status, get_ppt_vision_runtime_status
|
||||
vision_status = get_ppt_vision_runtime_status()
|
||||
vision_enabled = bool(vision_status.get('enabled'))
|
||||
vision_audit_status = get_ppt_vision_audit_status()
|
||||
except Exception:
|
||||
vision_enabled = False
|
||||
vision_audit_status = {
|
||||
'ok': False,
|
||||
'running': False,
|
||||
'status': 'unknown',
|
||||
'status_label': '讀取失敗',
|
||||
'message': '最近視覺 QA 狀態讀取失敗。',
|
||||
'last_run': None,
|
||||
}
|
||||
|
||||
# Phase 47 K-6: 月報表統計 + top failure files
|
||||
audit_30d_stats = {}
|
||||
@@ -3321,6 +3342,7 @@ def ppt_audit_history():
|
||||
top_failure_files=top_failure_files,
|
||||
vision_enabled=vision_enabled,
|
||||
vision_status=vision_status,
|
||||
vision_audit_status=vision_audit_status,
|
||||
auto_generation=auto_generation,
|
||||
auto_generation_items=auto_generation_items,
|
||||
auto_generation_missing_report_types=auto_generation.get('missing_report_types', []),
|
||||
|
||||
@@ -106,6 +106,75 @@ def get_ppt_vision_runtime_status() -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _public_audit_run_payload(run: Dict[str, Any] | None) -> Dict[str, Any] | None:
|
||||
if not run:
|
||||
return None
|
||||
summary = run.get('summary') or {}
|
||||
audited_files = []
|
||||
for item in summary.get('audited_files') or []:
|
||||
path = item.get('path') or ''
|
||||
audited_files.append({
|
||||
'filename': os.path.basename(path) if path else '',
|
||||
'slides_checked': int(item.get('slides_checked') or 0),
|
||||
'issues': int(item.get('issues') or 0),
|
||||
'error': item.get('error') or '',
|
||||
})
|
||||
errors = [str(error)[:160] for error in (summary.get('errors') or [])[:3]]
|
||||
payload = {
|
||||
'ok': bool(run.get('ok')),
|
||||
'status': run.get('status') or 'unknown',
|
||||
'queued_at': run.get('queued_at') or '',
|
||||
'started_at': run.get('started_at') or '',
|
||||
'finished_at': run.get('finished_at') or '',
|
||||
'filenames': [
|
||||
os.path.basename(str(name))
|
||||
for name in (run.get('filenames') or [])
|
||||
if str(name).lower().endswith('.pptx')
|
||||
],
|
||||
'max_files': run.get('max_files'),
|
||||
'error': run.get('error') or '',
|
||||
'summary': {
|
||||
'audited_count': len(audited_files),
|
||||
'total_issues': int(summary.get('total_issues') or 0),
|
||||
'error_count': len(summary.get('errors') or []),
|
||||
'errors': errors,
|
||||
'files': audited_files[:5],
|
||||
},
|
||||
}
|
||||
return payload
|
||||
|
||||
|
||||
def get_ppt_vision_audit_status() -> Dict[str, Any]:
|
||||
"""Return the current/last background visual QA run without touching DB."""
|
||||
running = _AUDIT_LOCK.locked()
|
||||
last_run = _public_audit_run_payload(_LAST_AUDIT_RUN)
|
||||
if running:
|
||||
status = 'running'
|
||||
status_label = '執行中'
|
||||
message = '視覺 QA 正在背景審核簡報。'
|
||||
elif last_run:
|
||||
status = last_run.get('status') or 'unknown'
|
||||
status_label = {
|
||||
'queued': '已排入',
|
||||
'running': '執行中',
|
||||
'completed': '已完成',
|
||||
'error': '錯誤',
|
||||
}.get(status, status)
|
||||
message = '最近一次視覺 QA 已完成。' if status == 'completed' else '最近一次視覺 QA 狀態可查。'
|
||||
else:
|
||||
status = 'idle'
|
||||
status_label = '待命'
|
||||
message = '尚未有背景視覺 QA 執行紀錄。'
|
||||
return {
|
||||
'ok': True,
|
||||
'running': running,
|
||||
'status': status,
|
||||
'status_label': status_label,
|
||||
'message': message,
|
||||
'last_run': last_run,
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 結果容器
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -557,7 +626,7 @@ def start_ppt_vision_audit_background(
|
||||
'ok': True,
|
||||
'status': 'already_running',
|
||||
'message': 'PPT vision audit is already running.',
|
||||
'last_run': _LAST_AUDIT_RUN,
|
||||
'last_run': _public_audit_run_payload(_LAST_AUDIT_RUN),
|
||||
}
|
||||
|
||||
clean_filenames = [
|
||||
@@ -565,11 +634,27 @@ def start_ppt_vision_audit_background(
|
||||
for name in (filenames or [])
|
||||
if str(name).lower().endswith('.pptx')
|
||||
]
|
||||
queued_at = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
_LAST_AUDIT_RUN = {
|
||||
'ok': True,
|
||||
'status': 'queued',
|
||||
'queued_at': queued_at,
|
||||
'filenames': clean_filenames,
|
||||
'max_files': max_files,
|
||||
}
|
||||
|
||||
def _run():
|
||||
global _LAST_AUDIT_RUN
|
||||
with _AUDIT_LOCK:
|
||||
started_at = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
_LAST_AUDIT_RUN = {
|
||||
'ok': True,
|
||||
'status': 'running',
|
||||
'queued_at': queued_at,
|
||||
'started_at': started_at,
|
||||
'filenames': clean_filenames,
|
||||
'max_files': max_files,
|
||||
}
|
||||
try:
|
||||
summary = audit_recent_ppts(
|
||||
reports_dir=reports_dir,
|
||||
@@ -580,16 +665,22 @@ def start_ppt_vision_audit_background(
|
||||
_LAST_AUDIT_RUN = {
|
||||
'ok': True,
|
||||
'status': 'completed',
|
||||
'queued_at': queued_at,
|
||||
'started_at': started_at,
|
||||
'finished_at': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'filenames': clean_filenames,
|
||||
'max_files': max_files,
|
||||
'summary': summary,
|
||||
}
|
||||
except Exception as exc:
|
||||
_LAST_AUDIT_RUN = {
|
||||
'ok': False,
|
||||
'status': 'error',
|
||||
'queued_at': queued_at,
|
||||
'started_at': started_at,
|
||||
'finished_at': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'filenames': clean_filenames,
|
||||
'max_files': max_files,
|
||||
'error': f'{type(exc).__name__}: {str(exc)[:200]}',
|
||||
}
|
||||
logger.error("[PPTVision] background audit failed: %s", exc, exc_info=True)
|
||||
@@ -641,6 +732,7 @@ __all__ = [
|
||||
'ppt_vision_service',
|
||||
'is_ppt_vision_enabled',
|
||||
'get_ppt_vision_runtime_status',
|
||||
'get_ppt_vision_audit_status',
|
||||
'PPT_VISION_SYSTEM_PROMPT',
|
||||
'audit_recent_ppts',
|
||||
'start_ppt_vision_audit_background',
|
||||
|
||||
@@ -96,6 +96,33 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ppt-vision-status is-{{ vision_audit_status.status }}" data-ppt-vision-status aria-live="polite">
|
||||
<div class="ppt-vision-status-main">
|
||||
<span class="ppt-run-status is-{{ 'ready' if vision_audit_status.status == 'completed' else 'planned' if vision_audit_status.status in ['idle', 'queued', 'running'] else 'error' }}">
|
||||
<i class="fas fa-eye me-1" aria-hidden="true"></i>Vision QA
|
||||
</span>
|
||||
<div>
|
||||
<strong data-ppt-vision-status-title>{{ vision_audit_status.status_label }}</strong>
|
||||
<small data-ppt-vision-status-meta>{{ vision_audit_status.message }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ppt-vision-status-list" data-ppt-vision-status-list>
|
||||
{% if vision_audit_status.last_run %}
|
||||
<div class="ppt-vision-job">
|
||||
<span>{{ vision_audit_status.last_run.finished_at or vision_audit_status.last_run.started_at or vision_audit_status.last_run.queued_at }}</span>
|
||||
<strong>{{ vision_audit_status.last_run.summary.audited_count }} 份 / {{ vision_audit_status.last_run.summary.total_issues }} 問題</strong>
|
||||
<small>{% if vision_audit_status.last_run.summary.error_count %}錯誤 {{ vision_audit_status.last_run.summary.error_count }}{% else %}無 runtime error{% endif %}</small>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="ppt-vision-job">
|
||||
<span>尚無紀錄</span>
|
||||
<strong>待命</strong>
|
||||
<small>按下「立即視覺 QA」後會在這裡顯示背景任務狀態。</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ppt-toolbar">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('admin_observability.ppt_audit_history', month=prev_month_label, report_type=report_type) }}">
|
||||
|
||||
@@ -696,6 +696,93 @@ def test_ppt_audit_run_vision_queues_background_audit(client, monkeypatch):
|
||||
assert captured['max_files'] == 1
|
||||
|
||||
|
||||
def test_ppt_vision_audit_status_sanitizes_last_run(monkeypatch):
|
||||
"""背景視覺 QA 狀態只回檔名與摘要,不把 reports_dir 絕對路徑曝露到頁面。"""
|
||||
from services import ppt_vision_service as svc
|
||||
|
||||
monkeypatch.setattr(svc, '_LAST_AUDIT_RUN', {
|
||||
'ok': True,
|
||||
'status': 'completed',
|
||||
'queued_at': '2026-05-19 12:00:00',
|
||||
'started_at': '2026-05-19 12:00:01',
|
||||
'finished_at': '2026-05-19 12:00:05',
|
||||
'filenames': ['/app/data/reports/ocbot_daily_20260518.pptx'],
|
||||
'max_files': 1,
|
||||
'summary': {
|
||||
'audited_files': [{
|
||||
'path': '/app/data/reports/ocbot_daily_20260518.pptx',
|
||||
'slides_checked': 1,
|
||||
'issues': 0,
|
||||
'error': None,
|
||||
}],
|
||||
'total_issues': 0,
|
||||
'errors': [],
|
||||
},
|
||||
})
|
||||
|
||||
status = svc.get_ppt_vision_audit_status()
|
||||
|
||||
assert status['ok'] is True
|
||||
assert status['status'] == 'completed'
|
||||
assert status['status_label'] == '已完成'
|
||||
assert status['last_run']['filenames'] == ['ocbot_daily_20260518.pptx']
|
||||
assert status['last_run']['summary']['audited_count'] == 1
|
||||
assert status['last_run']['summary']['files'][0]['filename'] == 'ocbot_daily_20260518.pptx'
|
||||
assert '/app/data/reports' not in str(status)
|
||||
|
||||
|
||||
def test_ppt_audit_vision_status_route_returns_json(client, monkeypatch):
|
||||
"""頁面輪詢用 status endpoint 要能回最近一次背景視覺 QA 狀態。"""
|
||||
from services import ppt_vision_service as svc
|
||||
|
||||
monkeypatch.setattr(svc, 'get_ppt_vision_audit_status', lambda: {
|
||||
'ok': True,
|
||||
'running': False,
|
||||
'status': 'completed',
|
||||
'status_label': '已完成',
|
||||
'message': '最近一次視覺 QA 已完成。',
|
||||
'last_run': {
|
||||
'summary': {'audited_count': 2, 'total_issues': 1, 'error_count': 0, 'errors': [], 'files': []},
|
||||
},
|
||||
})
|
||||
|
||||
r = client.get('/observability/ppt_audit/vision_status')
|
||||
data = r.get_json()
|
||||
|
||||
assert r.status_code == 200
|
||||
assert data['ok'] is True
|
||||
assert data['status'] == 'completed'
|
||||
assert data['last_run']['summary']['audited_count'] == 2
|
||||
|
||||
|
||||
def test_ppt_audit_history_renders_last_vision_status(client, monkeypatch):
|
||||
"""PPT 產線頁要在按下立即 QA 前後都看得到背景狀態卡。"""
|
||||
from services import ppt_vision_service as svc
|
||||
|
||||
monkeypatch.setattr(svc, 'get_ppt_vision_audit_status', lambda: {
|
||||
'ok': True,
|
||||
'running': False,
|
||||
'status': 'completed',
|
||||
'status_label': '已完成',
|
||||
'message': '最近一次視覺 QA 已完成。',
|
||||
'last_run': {
|
||||
'queued_at': '2026-05-19 12:00:00',
|
||||
'started_at': '2026-05-19 12:00:01',
|
||||
'finished_at': '2026-05-19 12:00:05',
|
||||
'summary': {'audited_count': 2, 'total_issues': 1, 'error_count': 0, 'errors': [], 'files': []},
|
||||
},
|
||||
})
|
||||
|
||||
r = client.get('/observability/ppt_audit_history')
|
||||
html = r.get_data(as_text=True)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert 'data-ppt-vision-status' in html
|
||||
assert 'data-ppt-vision-status-title' in html
|
||||
assert '最近一次視覺 QA 已完成。' in html
|
||||
assert '2 份 / 1 問題' in html
|
||||
|
||||
|
||||
def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch):
|
||||
"""視覺 QA failed 常只有 issues_found;AiderHeal 應可吃診斷摘要派工。"""
|
||||
from routes import admin_observability_routes as mod
|
||||
|
||||
@@ -205,6 +205,83 @@
|
||||
font-size: var(--momo-text-caption, 12px);
|
||||
}
|
||||
|
||||
.ppt-vision-status {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 0.36fr) minmax(0, 0.64fr);
|
||||
gap: var(--momo-space-3, 12px);
|
||||
align-items: stretch;
|
||||
margin-top: var(--momo-space-3, 12px);
|
||||
padding: var(--momo-space-3, 12px);
|
||||
border: 1px solid rgba(72, 108, 149, 0.24);
|
||||
border-radius: var(--momo-radius-lg, 8px);
|
||||
background:
|
||||
radial-gradient(circle, rgba(72, 108, 149, 0.08) 1px, transparent 1.2px),
|
||||
rgba(255, 255, 255, 0.58);
|
||||
background-size: 10px 10px, auto;
|
||||
}
|
||||
|
||||
.ppt-vision-status.is-running,
|
||||
.ppt-vision-status.is-queued {
|
||||
border-color: rgba(184, 121, 47, 0.32);
|
||||
}
|
||||
|
||||
.ppt-vision-status.is-error {
|
||||
border-color: rgba(196, 84, 75, 0.32);
|
||||
}
|
||||
|
||||
.ppt-vision-status-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--momo-space-3, 12px);
|
||||
}
|
||||
|
||||
.ppt-vision-status-main strong,
|
||||
.ppt-vision-status-main small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ppt-vision-status-main strong {
|
||||
color: var(--obs-ink);
|
||||
font-size: var(--momo-text-body, 14px);
|
||||
font-weight: var(--momo-font-weight-black, 800);
|
||||
}
|
||||
|
||||
.ppt-vision-status-main small,
|
||||
.ppt-vision-job span,
|
||||
.ppt-vision-job small {
|
||||
color: var(--obs-muted);
|
||||
font-size: var(--momo-text-caption, 12px);
|
||||
}
|
||||
|
||||
.ppt-vision-status-list {
|
||||
display: grid;
|
||||
gap: var(--momo-space-2, 8px);
|
||||
}
|
||||
|
||||
.ppt-vision-job {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(130px, 0.28fr) minmax(150px, 0.28fr) minmax(0, 0.44fr);
|
||||
gap: var(--momo-space-2, 8px);
|
||||
align-items: center;
|
||||
min-height: 42px;
|
||||
padding: var(--momo-space-2, 8px);
|
||||
border: 1px solid rgba(86, 64, 48, 0.12);
|
||||
border-radius: var(--momo-radius-md, 6px);
|
||||
background: rgba(255, 255, 255, 0.54);
|
||||
}
|
||||
|
||||
.ppt-vision-job strong,
|
||||
.ppt-vision-job span,
|
||||
.ppt-vision-job small {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ppt-vision-job strong {
|
||||
color: var(--obs-ink);
|
||||
font-size: var(--momo-text-body, 14px);
|
||||
}
|
||||
|
||||
.ppt-command {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
@@ -1151,6 +1228,7 @@ body.ppt-preview-open {
|
||||
|
||||
.ppt-diagnostic-strip,
|
||||
.ppt-aider-status,
|
||||
.ppt-vision-status,
|
||||
.ppt-health-board,
|
||||
.ppt-pipeline-layout {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1262,7 +1340,8 @@ body.ppt-preview-open {
|
||||
min-height: calc(100vh - 168px);
|
||||
}
|
||||
|
||||
.ppt-aider-job {
|
||||
.ppt-aider-job,
|
||||
.ppt-vision-job {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,6 +659,57 @@
|
||||
}
|
||||
};
|
||||
|
||||
function renderPptVisionStatus(payload) {
|
||||
const panel = document.querySelector('[data-ppt-vision-status]');
|
||||
if (!panel) return;
|
||||
const status = (payload && payload.status) || 'unknown';
|
||||
panel.className = panel.className.replace(/\bis-[a-z_]+\b/g, '').trim();
|
||||
panel.classList.add(`is-${status}`);
|
||||
const title = panel.querySelector('[data-ppt-vision-status-title]');
|
||||
const meta = panel.querySelector('[data-ppt-vision-status-meta]');
|
||||
const list = panel.querySelector('[data-ppt-vision-status-list]');
|
||||
if (title) title.textContent = (payload && payload.status_label) || '狀態未知';
|
||||
if (meta) meta.textContent = (payload && payload.message) || '最近視覺 QA 狀態無法讀取。';
|
||||
if (!list) return;
|
||||
const lastRun = payload && payload.last_run;
|
||||
if (!lastRun) {
|
||||
list.innerHTML = `
|
||||
<div class="ppt-vision-job">
|
||||
<span>尚無紀錄</span>
|
||||
<strong>待命</strong>
|
||||
<small>按下「立即視覺 QA」後會在這裡顯示背景任務狀態。</small>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
const summary = lastRun.summary || {};
|
||||
const timestamp = lastRun.finished_at || lastRun.started_at || lastRun.queued_at || '';
|
||||
const issueText = `${Number(summary.audited_count || 0)} 份 / ${Number(summary.total_issues || 0)} 問題`;
|
||||
const errorText = Number(summary.error_count || 0) > 0
|
||||
? `錯誤 ${Number(summary.error_count || 0)}`
|
||||
: '無 runtime error';
|
||||
list.innerHTML = `
|
||||
<div class="ppt-vision-job">
|
||||
<span>${escapeHtml(timestamp)}</span>
|
||||
<strong>${escapeHtml(issueText)}</strong>
|
||||
<small>${escapeHtml(errorText)}</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
window.refreshPptVisionStatus = async function refreshPptVisionStatus() {
|
||||
try {
|
||||
const response = await fetch('/observability/ppt_audit/vision_status', {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.ok) return;
|
||||
renderPptVisionStatus(data);
|
||||
} catch (error) {
|
||||
console.warn('ppt_vision_status_failed', error);
|
||||
}
|
||||
};
|
||||
|
||||
function initPptAutoGeneration() {
|
||||
const panel = document.querySelector('[data-ppt-auto-generation]');
|
||||
const pageStatus = document.querySelector('[data-ppt-auto-status]');
|
||||
@@ -671,6 +722,9 @@
|
||||
const previewLoading = previewModal ? previewModal.querySelector('[data-ppt-preview-loading]') : null;
|
||||
const previewFrameWrap = previewModal ? previewModal.querySelector('.ppt-preview-frame-wrap') : null;
|
||||
const visionAuditFilenames = readJson('obs-ppt-audit-filenames', []);
|
||||
if (document.querySelector('[data-ppt-vision-status]') && window.refreshPptVisionStatus) {
|
||||
window.refreshPptVisionStatus();
|
||||
}
|
||||
|
||||
function closePreviewModal() {
|
||||
if (!previewModal) return;
|
||||
@@ -772,6 +826,9 @@
|
||||
? '視覺 QA 已在執行中,請稍後重新整理查看資料庫結果。'
|
||||
: `視覺 QA 已排入 ${filenames.length} 份簡報;審核結果會寫入 ppt_audit_results。`;
|
||||
}
|
||||
if (window.refreshPptVisionStatus) {
|
||||
window.refreshPptVisionStatus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('ppt_vision_audit_queue_failed', error);
|
||||
buttons.forEach(item => {
|
||||
|
||||
Reference in New Issue
Block a user