|
|
@@ -0,0 +1,2710 @@
|
|
|
+<!doctype html>
|
|
|
+<html lang="zh-CN">
|
|
|
+
|
|
|
+<head>
|
|
|
+ <meta charset="utf-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
+ <meta name="referrer" content="no-referrer">
|
|
|
+ <title>新 Query 搜索与评估 · 案例总览</title>
|
|
|
+ <style>
|
|
|
+ :root {
|
|
|
+ --ink: #24211d;
|
|
|
+ --muted: #6f6961;
|
|
|
+ --line: #ded8ce;
|
|
|
+ --paper: #fbfaf7;
|
|
|
+ --panel: #ffffff;
|
|
|
+ --mint: #1f8a70;
|
|
|
+ --rose: #b24b63;
|
|
|
+ --amber: #b87918;
|
|
|
+ --cyan: #2a6f8f;
|
|
|
+ --soft-mint: #e9f5f0;
|
|
|
+ --soft-rose: #f8e9ee;
|
|
|
+ --soft-amber: #fff2d9;
|
|
|
+ --soft-cyan: #e7f2f7;
|
|
|
+ --shadow: 0 18px 45px rgba(41, 35, 28, .08);
|
|
|
+ }
|
|
|
+
|
|
|
+ * {
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ body {
|
|
|
+ margin: 0;
|
|
|
+ color: var(--ink);
|
|
|
+ background: var(--paper);
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
|
+ line-height: 1.55;
|
|
|
+ }
|
|
|
+
|
|
|
+ header {
|
|
|
+ padding: 30px 24px 18px;
|
|
|
+ border-bottom: 1px solid var(--line);
|
|
|
+ background: linear-gradient(180deg, #fff 0%, #fbfaf7 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .wrap {
|
|
|
+ max-width: 1440px;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .eyebrow {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+ color: var(--mint);
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ h1 {
|
|
|
+ margin: 8px 0 4px;
|
|
|
+ font-size: clamp(24px, 3.5vw, 36px);
|
|
|
+ line-height: 1.1;
|
|
|
+ letter-spacing: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .lede {
|
|
|
+ color: var(--muted);
|
|
|
+ font-size: 14.5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Layout for two-column structure */
|
|
|
+ .app-container {
|
|
|
+ max-width: 1440px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 24px;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 320px 1fr;
|
|
|
+ gap: 24px;
|
|
|
+ align-items: start;
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 1024px) {
|
|
|
+ .app-container {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Sidebar Styles */
|
|
|
+ .sidebar {
|
|
|
+ background: var(--panel);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ height: fit-content;
|
|
|
+ max-height: calc(100vh - 180px);
|
|
|
+ overflow-y: auto;
|
|
|
+ position: sticky;
|
|
|
+ top: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar h3 {
|
|
|
+ margin-top: 0;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ border-bottom: 1px solid var(--line);
|
|
|
+ padding-bottom: 10px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-item {
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ background: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.15s ease;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-item:hover {
|
|
|
+ border-color: #bbb;
|
|
|
+ background: #fafaf9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-item.active {
|
|
|
+ border-color: var(--mint);
|
|
|
+ background: var(--soft-mint);
|
|
|
+ color: var(--ink);
|
|
|
+ font-weight: 700;
|
|
|
+ box-shadow: inset 3px 0 0 var(--mint);
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-item .q-text {
|
|
|
+ word-break: break-all;
|
|
|
+ line-height: 1.35;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-item .meta {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-top: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sidebar-item .meta strong {
|
|
|
+ color: var(--mint);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Builder Card styling */
|
|
|
+ .builder-card {
|
|
|
+ background: var(--panel);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ }
|
|
|
+
|
|
|
+ .builder-card h3 {
|
|
|
+ margin-top: 0;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ border-bottom: 1px solid var(--line);
|
|
|
+ padding-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dimensions {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dim-group {
|
|
|
+ padding: 4px 0;
|
|
|
+ border-bottom: 1px solid #f2f2f2;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dim-group:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .level-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 7px 0;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .level-row.indent {
|
|
|
+ margin-left: 78px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dim-label {
|
|
|
+ font-size: 12.5px;
|
|
|
+ color: #999;
|
|
|
+ min-width: 68px;
|
|
|
+ padding-top: 6px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 3px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dim-label::before {
|
|
|
+ content: '·';
|
|
|
+ color: #bbb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sub-label {
|
|
|
+ font-size: 11.5px;
|
|
|
+ color: #bbb;
|
|
|
+ min-width: 56px;
|
|
|
+ padding-top: 6px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chips-wrap {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 7px;
|
|
|
+ align-items: center;
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chip {
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 20px;
|
|
|
+ font-size: 12.5px;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 1.5px solid #e2e2e2;
|
|
|
+ background: #fff;
|
|
|
+ color: #666;
|
|
|
+ transition: all .12s;
|
|
|
+ user-select: none;
|
|
|
+ white-space: nowrap;
|
|
|
+ line-height: 1.4;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chip:hover {
|
|
|
+ border-color: #c0c0c0;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chip.none-active {
|
|
|
+ background: #1a1a1a;
|
|
|
+ border-color: #1a1a1a;
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chip.none-active:hover {
|
|
|
+ background: #333;
|
|
|
+ border-color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chip.selected {
|
|
|
+ background: var(--mint);
|
|
|
+ border-color: var(--mint);
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chip.selected:hover {
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .placeholder-text {
|
|
|
+ font-size: 12.5px;
|
|
|
+ color: #ccc;
|
|
|
+ font-style: italic;
|
|
|
+ padding: 6px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-section {
|
|
|
+ margin-top: 18px;
|
|
|
+ padding: 14px 18px;
|
|
|
+ background: #fafafa;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1.5px solid #ebebeb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-header {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #aaa;
|
|
|
+ font-weight: 600;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.08em;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-tags {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 6px;
|
|
|
+ min-height: 34px;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-tag {
|
|
|
+ background: #1a1a1a;
|
|
|
+ color: #fff;
|
|
|
+ padding: 4px 11px;
|
|
|
+ border-radius: 7px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-sep {
|
|
|
+ color: #d0d0d0;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 300;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-empty {
|
|
|
+ color: #ccc;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-path {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #bbb;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ min-height: 18px;
|
|
|
+ font-family: monospace;
|
|
|
+ letter-spacing: 0.01em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn {
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ background: #fff;
|
|
|
+ color: var(--ink);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ font: inherit;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 13px;
|
|
|
+ transition: all 0.15s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn:hover {
|
|
|
+ background: #f5f5f5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn.active {
|
|
|
+ color: #fff;
|
|
|
+ background: var(--ink);
|
|
|
+ border-color: var(--ink);
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-dark {
|
|
|
+ background: #1a1a1a;
|
|
|
+ color: #fff;
|
|
|
+ border-color: #1a1a1a;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-dark:hover {
|
|
|
+ background: #333;
|
|
|
+ border-color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Console logger styling */
|
|
|
+ .exec-console {
|
|
|
+ background: #181512;
|
|
|
+ color: #dcd3c3;
|
|
|
+ border: 1px solid #3d352e;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 16px;
|
|
|
+ font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 320px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .exec-console-head {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ border-bottom: 1px solid #3d352e;
|
|
|
+ padding-bottom: 8px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #a3968d;
|
|
|
+ }
|
|
|
+
|
|
|
+ .exec-console-output {
|
|
|
+ flex: 1;
|
|
|
+ margin: 0;
|
|
|
+ padding: 4px;
|
|
|
+ overflow-y: auto;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-all;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.45;
|
|
|
+ color: #e5d8c5;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Stats container styling */
|
|
|
+ .stats {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ gap: 12px;
|
|
|
+ margin-top: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat {
|
|
|
+ background: var(--panel);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 14px;
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ min-height: 86px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat strong {
|
|
|
+ display: block;
|
|
|
+ font-size: 26px;
|
|
|
+ line-height: 1.1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat span {
|
|
|
+ color: var(--muted);
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Filters and Grid */
|
|
|
+ .toolbar {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-top: 20px;
|
|
|
+ margin-bottom: 18px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filters {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ select {
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ background: #fff;
|
|
|
+ color: var(--ink);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 9px 12px;
|
|
|
+ font: inherit;
|
|
|
+ min-width: 150px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 768px) {
|
|
|
+ .grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Results cards */
|
|
|
+ .result {
|
|
|
+ min-height: 480px;
|
|
|
+ background: var(--panel);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ position: relative;
|
|
|
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.25s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .result:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 22px 50px rgba(41, 35, 28, .12);
|
|
|
+ }
|
|
|
+
|
|
|
+ .result.discard {
|
|
|
+ border-color: rgba(178, 75, 99, 0.15);
|
|
|
+ }
|
|
|
+
|
|
|
+ .discard-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(251, 250, 247, 0.85);
|
|
|
+ backdrop-filter: blur(5px);
|
|
|
+ -webkit-backdrop-filter: blur(5px);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 20px;
|
|
|
+ text-align: center;
|
|
|
+ z-index: 5;
|
|
|
+ opacity: 1;
|
|
|
+ transition: opacity 0.25s ease;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .result.discard:hover .discard-overlay {
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .discard-badge {
|
|
|
+ background: var(--soft-rose);
|
|
|
+ color: var(--rose);
|
|
|
+ border: 1px solid rgba(178, 75, 99, 0.25);
|
|
|
+ padding: 6px 16px;
|
|
|
+ border-radius: 999px;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ box-shadow: 0 2px 8px rgba(178, 75, 99, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .discard-reason {
|
|
|
+ color: var(--muted);
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.6;
|
|
|
+ max-width: 90%;
|
|
|
+ margin: 0 auto;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 6;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ overflow: hidden;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .thumbs {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 2px;
|
|
|
+ height: 140px;
|
|
|
+ background: #eee7dc;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .thumbs img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ display: block;
|
|
|
+ background: #eee7dc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .thumbs img:first-child:nth-last-child(1) {
|
|
|
+ grid-column: 1 / -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .body {
|
|
|
+ padding: 15px;
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .meta {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 10px;
|
|
|
+ color: var(--muted);
|
|
|
+ font-size: 12px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .platform {
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 999px;
|
|
|
+ padding: 2px 8px;
|
|
|
+ font-weight: 700;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .p-xhs { background: var(--rose); }
|
|
|
+ .p-gzh { background: var(--mint); }
|
|
|
+ .p-zhihu { background: var(--cyan); }
|
|
|
+ .p-x { background: var(--cyan); }
|
|
|
+ .p-bili { background: #e06d93; }
|
|
|
+ .p-douyin { background: #24211d; }
|
|
|
+ .p-sph { background: #07c160; }
|
|
|
+ .p-youtube { background: #c4302b; }
|
|
|
+ .p-github { background: #24292e; }
|
|
|
+ .p-toutiao { background: #f04142; }
|
|
|
+ .p-weibo { background: #e6162d; }
|
|
|
+
|
|
|
+ h2 {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 17px;
|
|
|
+ line-height: 1.3;
|
|
|
+ letter-spacing: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .excerpt {
|
|
|
+ color: var(--muted);
|
|
|
+ font-size: 13px;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 4;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tags {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 6px;
|
|
|
+ margin: 10px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tag {
|
|
|
+ background: #f2eee7;
|
|
|
+ border-radius: 999px;
|
|
|
+ padding: 2px 8px;
|
|
|
+ font-size: 11.5px;
|
|
|
+ color: #514a42;
|
|
|
+ }
|
|
|
+
|
|
|
+ .scorebar {
|
|
|
+ margin-top: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .overall {
|
|
|
+ display: flex;
|
|
|
+ align-items: end;
|
|
|
+ justify-content: space-between;
|
|
|
+ border-top: 1px solid var(--line);
|
|
|
+ padding-top: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score {
|
|
|
+ font-size: 32px;
|
|
|
+ line-height: .9;
|
|
|
+ font-weight: 800;
|
|
|
+ }
|
|
|
+
|
|
|
+ .decision {
|
|
|
+ color: var(--mint);
|
|
|
+ font-weight: 800;
|
|
|
+ }
|
|
|
+
|
|
|
+ .decision.discard {
|
|
|
+ color: var(--amber);
|
|
|
+ }
|
|
|
+
|
|
|
+ .group-snapshot {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
|
|
+ gap: 5px;
|
|
|
+ margin-top: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .group-pill {
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 5px 6px;
|
|
|
+ background: #fff;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .group-pill span {
|
|
|
+ display: block;
|
|
|
+ color: var(--muted);
|
|
|
+ font-size: 11px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ .group-pill strong {
|
|
|
+ display: block;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .actions a,
|
|
|
+ .actions button {
|
|
|
+ flex: 1;
|
|
|
+ text-align: center;
|
|
|
+ text-decoration: none;
|
|
|
+ color: var(--ink);
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 8px 10px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Dialog styling */
|
|
|
+ dialog {
|
|
|
+ width: min(980px, calc(100vw - 28px));
|
|
|
+ max-height: calc(100vh - 32px);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 0;
|
|
|
+ box-shadow: 0 28px 90px rgba(0, 0, 0, .25);
|
|
|
+ }
|
|
|
+
|
|
|
+ dialog::backdrop {
|
|
|
+ background: rgba(38, 33, 27, .42);
|
|
|
+ }
|
|
|
+
|
|
|
+ dialog.fullscreen {
|
|
|
+ width: 100vw !important;
|
|
|
+ max-width: 100vw !important;
|
|
|
+ height: 100vh !important;
|
|
|
+ max-height: 100vh !important;
|
|
|
+ border: none !important;
|
|
|
+ border-radius: 0 !important;
|
|
|
+ margin: 0 !important;
|
|
|
+ top: 0 !important;
|
|
|
+ left: 0 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ dialog.fullscreen #modalContentProcedure {
|
|
|
+ height: calc(100vh - 120px) !important;
|
|
|
+ max-height: none !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ dialog.fullscreen #modalContentDetail {
|
|
|
+ height: calc(100vh - 120px) !important;
|
|
|
+ max-height: none !important;
|
|
|
+ overflow-y: auto !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-head {
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ background: #fff;
|
|
|
+ border-bottom: 1px solid var(--line);
|
|
|
+ padding: 16px;
|
|
|
+ z-index: 2;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 14px;
|
|
|
+ align-items: start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-head h3 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 20px;
|
|
|
+ line-height: 1.25;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-content {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1.1fr .9fr;
|
|
|
+ gap: 18px;
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-content > section,
|
|
|
+ .modal-content > aside {
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .section-title {
|
|
|
+ margin: 18px 0 8px;
|
|
|
+ font-weight: 800;
|
|
|
+ }
|
|
|
+
|
|
|
+ .raw {
|
|
|
+ white-space: pre-wrap;
|
|
|
+ background: #faf7f1;
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px;
|
|
|
+ max-height: 330px;
|
|
|
+ overflow: auto;
|
|
|
+ color: #3d3831;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .images {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .images img {
|
|
|
+ width: 100%;
|
|
|
+ max-height: 260px;
|
|
|
+ object-fit: contain;
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f1ece4;
|
|
|
+ }
|
|
|
+
|
|
|
+ .scores {
|
|
|
+ display: grid;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-group {
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #fff;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-group-head {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 10px;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 11px;
|
|
|
+ background: #faf7f1;
|
|
|
+ border-bottom: 1px solid var(--line);
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 800;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-group-head small {
|
|
|
+ color: var(--muted);
|
|
|
+ font-weight: 700;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-group-body {
|
|
|
+ display: grid;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 128px 1fr 34px;
|
|
|
+ gap: 10px;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-row.missing {
|
|
|
+ color: #a39b91;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-row.missing .meter span {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .meter {
|
|
|
+ height: 9px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #eee7dc;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .meter span {
|
|
|
+ display: block;
|
|
|
+ height: 100%;
|
|
|
+ width: calc(var(--v) * 20%);
|
|
|
+ background: var(--rose);
|
|
|
+ }
|
|
|
+
|
|
|
+ .rubric-note {
|
|
|
+ background: var(--soft-cyan);
|
|
|
+ border-left: 4px solid var(--cyan);
|
|
|
+ padding: 10px 12px;
|
|
|
+ color: #254c5d;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-tabs {
|
|
|
+ display: flex;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 0 16px;
|
|
|
+ border-bottom: 1px solid var(--line);
|
|
|
+ background: #faf7f1;
|
|
|
+ }
|
|
|
+ .modal-tab {
|
|
|
+ background: transparent;
|
|
|
+ border: none;
|
|
|
+ border-bottom: 3px solid transparent;
|
|
|
+ border-radius: 0;
|
|
|
+ padding: 10px 16px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--muted);
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ }
|
|
|
+ .modal-tab:hover {
|
|
|
+ color: var(--ink);
|
|
|
+ background: rgba(0, 0, 0, 0.02);
|
|
|
+ }
|
|
|
+ .modal-tab.active {
|
|
|
+ color: var(--mint);
|
|
|
+ border-bottom-color: var(--mint);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Rating card styling */
|
|
|
+ .sc-card {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 16px 20px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ box-shadow: 0 4px 15px rgba(0,0,0,0.02);
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-card-head {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 14px;
|
|
|
+ border-bottom: 1px solid #f3f0ea;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-card-head .title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-card-head .badge {
|
|
|
+ background: #eef2ff;
|
|
|
+ color: #2563eb;
|
|
|
+ font-size: 11px;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-card-head .avg-score {
|
|
|
+ font-size: 13.5px;
|
|
|
+ color: var(--muted);
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-card-head .avg-score strong {
|
|
|
+ font-size: 24px;
|
|
|
+ color: #2563eb;
|
|
|
+ font-weight: 800;
|
|
|
+ margin-left: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-sub-header {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--muted);
|
|
|
+ font-weight: 700;
|
|
|
+ margin: 14px 0 8px;
|
|
|
+ border-bottom: 1px dashed #f0ebd8;
|
|
|
+ padding-bottom: 4px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 6px 0;
|
|
|
+ font-size: 13px;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-row .label {
|
|
|
+ color: var(--ink);
|
|
|
+ font-weight: 500;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 100px;
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-row .bar-wrap {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ width: 160px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-row .bar {
|
|
|
+ height: 6px;
|
|
|
+ background: #eee7dc;
|
|
|
+ border-radius: 999px;
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-row .bar-fill {
|
|
|
+ height: 100%;
|
|
|
+ background: #2563eb;
|
|
|
+ border-radius: 999px;
|
|
|
+ width: calc(var(--v) * 10%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-row .value {
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 13px;
|
|
|
+ width: 20px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-row .info-icon {
|
|
|
+ cursor: pointer;
|
|
|
+ color: #9ca3af;
|
|
|
+ transition: color 0.15s ease;
|
|
|
+ font-size: 13px;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sc-row .info-icon:hover {
|
|
|
+ color: #3b82f6;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+
|
|
|
+<body>
|
|
|
+ <header>
|
|
|
+ <div class="wrap">
|
|
|
+ <div class="eyebrow">Content Search · runs_new/ 实时 · 维度构建器</div>
|
|
|
+ <h1>新 Query 搜索与评估</h1>
|
|
|
+ <p class="lede" id="lede">正在扫描 runs_new/ 目录...</p>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <main class="app-container">
|
|
|
+ <!-- Sidebar: previously searched queries -->
|
|
|
+ <aside class="sidebar">
|
|
|
+ <h3>
|
|
|
+ <span>已检索 Query 列表</span>
|
|
|
+ <button class="btn" onclick="loadData(true)" style="padding: 3px 8px; font-size: 11px;">刷新</button>
|
|
|
+ </h3>
|
|
|
+ <div class="sidebar-list" id="querySidebarList">
|
|
|
+ <!-- Dynamic sidebar items -->
|
|
|
+ </div>
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ <!-- Main Workspace -->
|
|
|
+ <div style="display: flex; flex-direction: column; gap: 24px; min-width: 0;">
|
|
|
+ <!-- Query Builder Panel -->
|
|
|
+ <section class="builder-card">
|
|
|
+ <h3>Query 词组织器</h3>
|
|
|
+ <div class="dimensions" id="dimensions"></div>
|
|
|
+
|
|
|
+ <div class="preview-section">
|
|
|
+ <div class="preview-header">Query 预览</div>
|
|
|
+ <div class="preview-tags" id="previewTags">
|
|
|
+ <span class="preview-empty">(请选择维度标签)</span>
|
|
|
+ </div>
|
|
|
+ <div class="preview-path" id="previewPath"></div>
|
|
|
+
|
|
|
+ <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; border-top: 1px solid var(--line); padding-top: 12px; margin-top: 12px;">
|
|
|
+ <div style="display: flex; flex-direction: column; gap: 8px; align-items: flex-start;">
|
|
|
+ <div style="display: flex; align-items: center; gap: 16px;">
|
|
|
+ <span style="font-size: 13.5px; font-weight: 700; color: var(--muted);">检索渠道:</span>
|
|
|
+ <div style="display: flex; gap: 10px; flex-wrap: wrap;" id="platformCheckboxes">
|
|
|
+ <!-- 小红书 -->
|
|
|
+ <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s;">
|
|
|
+ <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
|
|
|
+ <input type="checkbox" name="platform" value="xhs" checked onchange="togglePlatformInput(this)"> 小红书
|
|
|
+ </label>
|
|
|
+ <input type="number" name="platform_count" data-platform="xhs" min="1" max="100" value="20" style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink);">
|
|
|
+ </div>
|
|
|
+ <!-- 知乎 -->
|
|
|
+ <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s;">
|
|
|
+ <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
|
|
|
+ <input type="checkbox" name="platform" value="zhihu" checked onchange="togglePlatformInput(this)"> 知乎
|
|
|
+ </label>
|
|
|
+ <input type="number" name="platform_count" data-platform="zhihu" min="1" max="100" value="20" style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink);">
|
|
|
+ </div>
|
|
|
+ <!-- 公众号 -->
|
|
|
+ <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s; opacity: 0.6;">
|
|
|
+ <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
|
|
|
+ <input type="checkbox" name="platform" value="gzh" onchange="togglePlatformInput(this)"> 公众号
|
|
|
+ </label>
|
|
|
+ <input type="number" name="platform_count" data-platform="gzh" min="1" max="100" value="20" disabled style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink); opacity: 0.4;">
|
|
|
+ </div>
|
|
|
+ <!-- 抖音 -->
|
|
|
+ <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s; opacity: 0.6;">
|
|
|
+ <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
|
|
|
+ <input type="checkbox" name="platform" value="douyin" onchange="togglePlatformInput(this)"> 抖音
|
|
|
+ </label>
|
|
|
+ <input type="number" name="platform_count" data-platform="douyin" min="1" max="100" value="20" disabled style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink); opacity: 0.4;">
|
|
|
+ </div>
|
|
|
+ <!-- 视频号 -->
|
|
|
+ <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s; opacity: 0.6;">
|
|
|
+ <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
|
|
|
+ <input type="checkbox" name="platform" value="sph" onchange="togglePlatformInput(this)"> 视频号
|
|
|
+ </label>
|
|
|
+ <input type="number" name="platform_count" data-platform="sph" min="1" max="100" value="20" disabled style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink); opacity: 0.4;">
|
|
|
+ </div>
|
|
|
+ <!-- YouTube -->
|
|
|
+ <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s; opacity: 0.6;">
|
|
|
+ <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
|
|
|
+ <input type="checkbox" name="platform" value="youtube" onchange="togglePlatformInput(this)"> YouTube
|
|
|
+ </label>
|
|
|
+ <input type="number" name="platform_count" data-platform="youtube" min="1" max="100" value="20" disabled style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink); opacity: 0.4;">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div id="channelCountsRow" style="font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 8px; flex-wrap: wrap;"></div>
|
|
|
+ </div>
|
|
|
+ <div class="btn-row">
|
|
|
+ <button class="btn btn-dark" id="searchBtn" onclick="runSearchAndEvaluate()" style="background: var(--mint); color: #fff; border-color: var(--mint); font-weight: 700; padding: 6px 18px;">⚡ 搜索并评估</button>
|
|
|
+ <button class="btn" onclick="clearAll()">清空</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- Subprocess log card -->
|
|
|
+ <section class="exec-console" id="execConsoleCard" style="display: none;">
|
|
|
+ <div class="exec-console-head">
|
|
|
+ <span id="consoleTitle">🚀 搜索并评估任务控制台 - 准备就绪</span>
|
|
|
+ <span id="consoleStatus" style="font-weight: bold; color: var(--amber);">idle</span>
|
|
|
+ </div>
|
|
|
+ <pre class="exec-console-output" id="consoleOutput"></pre>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- Grid Cards Area -->
|
|
|
+ <div id="resultsArea" style="display: none;">
|
|
|
+ <div class="stats" id="stats"></div>
|
|
|
+
|
|
|
+ <div class="toolbar">
|
|
|
+ <div class="filters" id="platformFilterWrap"></div>
|
|
|
+
|
|
|
+ <div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
|
|
+ <div class="threshold-control">
|
|
|
+ <span style="color: var(--muted); font-size: 12px; font-weight: 700;">相关性过滤阈值:</span>
|
|
|
+ <input type="number" id="relThreshold" min="0" max="10" step="0.5" value="4.0"
|
|
|
+ oninput="renderGrid(); renderHead();"
|
|
|
+ style="width: 55px; border: 1px solid #d1d5db; border-radius: 4px; padding: 2px 6px; font-weight: 700; text-align: center; color: #2563eb; outline: none;">
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button id="reevalBtn" class="btn" onclick="reevalCurrentQuery()" style="background: #faf7f1; color: var(--amber); border-color: rgba(184, 121, 24, 0.3);">♻️ 重新评估当前结果</button>
|
|
|
+ <button id="editSpecBtn" class="btn" onclick="openSpecEditor()" style="background: #f0f7f6; color: var(--cyan); border-color: rgba(42, 111, 143, 0.3);">📝 编辑 Spec 提示词</button>
|
|
|
+
|
|
|
+ <div style="display: flex; align-items: center; gap: 4px; background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 4px 10px; font-size: 13px; font-weight: 600; box-shadow: var(--shadow); height: 38px;">
|
|
|
+ <span style="color: var(--muted); font-size: 12px; font-weight: 700;">提取并发:</span>
|
|
|
+ <input type="number" id="batchConcurrency" min="1" max="16" value="4"
|
|
|
+ style="width: 40px; border: 1px solid #d1d5db; border-radius: 4px; padding: 2px 4px; font-weight: 700; text-align: center; color: var(--ink); outline: none;">
|
|
|
+ </div>
|
|
|
+ <button id="batchProcBtn" class="btn" onclick="batchExtractProcedures()" style="background: #f0fdf4; color: var(--mint); border-color: rgba(31, 138, 112, 0.3); font-weight: 600; height: 38px;">⚡ 一键提取工序</button>
|
|
|
+
|
|
|
+ <select id="sort" onchange="renderGrid()">
|
|
|
+ <option value="score">按综合分排序</option>
|
|
|
+ <option value="date">按发布时间排序</option>
|
|
|
+ <option value="platform">按平台排序</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="grid" id="grid"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </main>
|
|
|
+
|
|
|
+ <!-- Details Dialog Modal -->
|
|
|
+ <dialog id="detailDialog">
|
|
|
+ <div class="modal-head">
|
|
|
+ <div>
|
|
|
+ <div id="modalMeta" class="meta"></div>
|
|
|
+ <h3 id="modalTitle"></h3>
|
|
|
+ </div>
|
|
|
+ <div style="display: flex; gap: 8px; align-items: center;">
|
|
|
+ <button id="toggleFullscreenBtn" class="btn" onclick="toggleModalFullscreen()" style="padding: 5px 12px; font-weight: 600; font-size: 12.5px;">📺 全屏</button>
|
|
|
+ <button class="btn" onclick="detailDialog.close()">关闭</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="modal-tabs" id="modalTabs" style="display: none;">
|
|
|
+ <button class="modal-tab active" onclick="switchModalTab('detail')" id="tabDetailBtn">帖子详情</button>
|
|
|
+ <button class="modal-tab" onclick="switchModalTab('procedure')" id="tabProcedureBtn">对应工序</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="modal-content" id="modalContentDetail">
|
|
|
+ <section>
|
|
|
+ <div class="rubric-note" id="modalReason"></div>
|
|
|
+ <div class="section-title">抓取文本节选</div>
|
|
|
+ <div class="raw" id="modalText"></div>
|
|
|
+ <div class="section-title">图片预览</div>
|
|
|
+ <div class="images" id="modalImages"></div>
|
|
|
+ </section>
|
|
|
+ <aside>
|
|
|
+ <div class="section-title" style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
+ <span>评分详情</span>
|
|
|
+ <span id="modalOverallScore" style="font-size: 13px; font-weight: 700; color: var(--muted); display: flex; align-items: center; gap: 4px; background: #faf7f1; border: 1px solid var(--line); padding: 3px 10px; border-radius: 6px;">
|
|
|
+ 综合评分 <strong style="font-size: 19px; color: #2563eb; font-weight: 900; line-height: 1;" id="modalOverallScoreVal">—</strong>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="scores" id="modalScores"></div>
|
|
|
+ <div class="section-title">类型 / 命中 query</div>
|
|
|
+ <div class="tags" id="modalTags"></div>
|
|
|
+ </aside>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="modalContentProcedure" style="display: none; min-height: 500px; height: calc(100vh - 180px); max-height: 700px; padding: 16px; box-sizing: border-box; flex-direction: column;">
|
|
|
+ <div id="procActionBar" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--line); flex-shrink: 0;">
|
|
|
+ <div style="font-size: 14px; font-weight: bold; color: var(--ink);" id="procStatusText">工序状态: 检测中...</div>
|
|
|
+ <div style="display: flex; gap: 8px;" id="procActionBtns"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="flex: 1; min-height: 0; position: relative; display: flex; flex-direction: column;">
|
|
|
+ <div id="procSetupPanel" style="display: none; flex-direction: column; align-items: center; justify-content: center; text-align: center; height: 100%; padding: 40px 20px;">
|
|
|
+ <div style="font-size: 36px; margin-bottom: 16px;">✨</div>
|
|
|
+ <h4 style="margin: 0 0 10px; font-size: 18px;">提取本帖工序</h4>
|
|
|
+ <p style="color: var(--muted); font-size: 14px; max-width: 500px; margin: 0 0 24px;">该帖子目前尚未生成对应的结构化工序。请在下方选择提取引擎和模型,点击开始提取。</p>
|
|
|
+
|
|
|
+ <div style="display: flex; gap: 16px; margin-bottom: 24px; text-align: left; background: #fff; border: 1px solid var(--line); padding: 16px; border-radius: 8px; box-shadow: var(--shadow);">
|
|
|
+ <div>
|
|
|
+ <label style="display: block; font-size: 12px; font-weight: bold; margin-bottom: 6px; color: var(--muted);">提取引擎 (Engine)</label>
|
|
|
+ <select id="procEngineSelect" style="min-width: 180px; padding: 6px 10px;" onchange="onProcEngineChange()">
|
|
|
+ <option value="cyber_runner">Cyber Runner (自研/OpenRouter)</option>
|
|
|
+ <option value="claude_sdk">Claude SDK (OAuth)</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label style="display: block; font-size: 12px; font-weight: bold; margin-bottom: 6px; color: var(--muted);">AI 模型 (Model)</label>
|
|
|
+ <select id="procModelSelect" style="min-width: 240px; padding: 6px 10px;"></select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button id="startProcBtn" class="btn btn-dark" onclick="startProcedureExtraction()" style="background: var(--mint); border-color: var(--mint); padding: 10px 24px; font-size: 15px; font-weight: bold;">开始提取工序</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="procConsolePanel" style="display: none; flex-direction: column; height: 100%; min-height: 0; background: #1e1b18; border: 1px solid #3d352e; border-radius: 8px; overflow: hidden;">
|
|
|
+ <div style="background: #2b2520; padding: 6px 12px; border-bottom: 1px solid #3d352e; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; font-family: monospace; font-size: 12px; color: #a3968d;">
|
|
|
+ <span>TERMINAL CONSOLE</span>
|
|
|
+ <span id="procConsoleStatus">idle</span>
|
|
|
+ </div>
|
|
|
+ <pre id="procConsoleOutput" style="flex: 1; margin: 0; padding: 12px; overflow: auto; font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; font-size: 13px; line-height: 1.45; color: #cbbba9; background: #1e1b18; white-space: pre-wrap; word-break: break-all;"></pre>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <iframe id="procedureIframe" style="display: none; width: 100%; height: 100%; border: 1px solid var(--line); border-radius: 8px; background: #fff;" referrerpolicy="no-referrer"></iframe>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </dialog>
|
|
|
+
|
|
|
+ <!-- Spec Prompt Editor Dialog -->
|
|
|
+ <dialog id="specEditorDialog" style="width: 850px; max-width: 95%; border: 1px solid var(--line); border-radius: 12px; padding: 0; box-shadow: var(--shadow); background: var(--panel);">
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center; background: #faf7f1; border-bottom: 1px solid var(--line); padding: 16px 20px;">
|
|
|
+ <h3 style="margin: 0; font-size: 18px; color: var(--ink); font-weight: 800;">📝 编辑 Spec 提示词规范</h3>
|
|
|
+ <button onclick="document.getElementById('specEditorDialog').close()" class="btn" style="padding: 5px 14px; font-weight: 600; font-size: 12px;">关闭</button>
|
|
|
+ </div>
|
|
|
+ <div style="padding: 20px;">
|
|
|
+ <div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
|
|
|
+ <span style="font-size: 13.5px; font-weight: 700; color: var(--ink);">选择提示词文件:</span>
|
|
|
+ <select id="specFileSelect" onchange="loadSpecFileContent()" style="padding: 6px 12px; border-radius: 8px; border: 1px solid #d1d5db; outline: none; min-width: 320px; font-family: monospace; font-size: 13px; font-weight: 600;">
|
|
|
+ </select>
|
|
|
+ <span id="specLoadStatus" style="font-size: 13px; font-weight: 600;"></span>
|
|
|
+ </div>
|
|
|
+ <div style="position: relative; margin-bottom: 20px;">
|
|
|
+ <textarea id="specContentTextarea" style="width: 100%; height: 500px; font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; font-size: 13.5px; line-height: 1.55; padding: 14px; border: 1px solid #d1d5db; border-radius: 8px; box-sizing: border-box; outline: none; background: #fafaf9; color: #1f2937; resize: vertical; border-left: 4px solid var(--cyan);"></textarea>
|
|
|
+ </div>
|
|
|
+ <div style="display: flex; justify-content: flex-end; gap: 12px; align-items: center; border-top: 1px solid var(--line); padding-top: 16px;">
|
|
|
+ <span id="specSaveStatus" style="font-size: 13.5px; font-weight: 700; margin-right: auto;"></span>
|
|
|
+ <button onclick="document.getElementById('specEditorDialog').close()" class="btn" style="background: #f3f4f6;">取消</button>
|
|
|
+ <button onclick="saveSpecFileContent()" class="btn" style="background: var(--mint); color: #fff; border-color: var(--mint); font-weight: bold;">💾 保存修改</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </dialog>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ const DIMS = [
|
|
|
+ {
|
|
|
+ "id": "tool_type",
|
|
|
+ "label": "工具类型",
|
|
|
+ "type": "flat",
|
|
|
+ "items": ["AI", "桌面 APP", "云端 Web", "API·CLI", "插件扩展"]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": "substance",
|
|
|
+ "label": "实质",
|
|
|
+ "type": "flat",
|
|
|
+ "items": ["人像", "信息"]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": "form",
|
|
|
+ "label": "形式",
|
|
|
+ "type": "flat",
|
|
|
+ "items": ["真实感", "写实风格", "实景拍摄", "版面设计", "版面解构"]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": "modality",
|
|
|
+ "label": "模态",
|
|
|
+ "type": "flat",
|
|
|
+ "items": ["图片", "视频"]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": "action",
|
|
|
+ "label": "动作",
|
|
|
+ "type": "hierarchical",
|
|
|
+ "data": {
|
|
|
+ "获取": {
|
|
|
+ "搜索": ["检索", "下载"],
|
|
|
+ "查询": ["调取"],
|
|
|
+ "录入": ["上传", "拍摄", "录音", "键入"],
|
|
|
+ "引用": ["选取"]
|
|
|
+ },
|
|
|
+ "提取": {
|
|
|
+ "物理提取": ["裁切", "抠取", "抽帧"],
|
|
|
+ "化学提取": ["识别", "反推", "解构"]
|
|
|
+ },
|
|
|
+ "生成": {
|
|
|
+ "元素生成": ["元素生成"],
|
|
|
+ "关系生成": ["数组生成", "结构生成"]
|
|
|
+ },
|
|
|
+ "修改": {
|
|
|
+ "增": ["添加", "叠加"],
|
|
|
+ "删": ["抹除", "剪除"],
|
|
|
+ "变": ["重述", "风格化", "转换", "替换", "调整", "增强"]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": "type",
|
|
|
+ "label": "类型",
|
|
|
+ "type": "hierarchical",
|
|
|
+ "data": {
|
|
|
+ "程序控制类型": {
|
|
|
+ "指令": ["提示词", "负向提示词", "描述"],
|
|
|
+ "参数": ["生成参数", "规格参数", "模型权重"],
|
|
|
+ "评估": ["评分", "评语"],
|
|
|
+ "流程": ["工作流", "批处理"]
|
|
|
+ },
|
|
|
+ "数据复用类型": {
|
|
|
+ "原子": ["数字人", "版式"],
|
|
|
+ "序列": ["模板"]
|
|
|
+ },
|
|
|
+ "内容类型": {
|
|
|
+ "素材/化学变化": ["参考图", "参考视频", "参考音频", "对标内容", "分镜图", "转场", "蒙版", "控制图", "运动轨迹", "滤镜", "构图布局"],
|
|
|
+ "素材/物理变化": ["截图", "视频片段", "转场片段", "关键帧", "音效", "特效"],
|
|
|
+ "半成品/序列": ["大纲", "脚本", "分镜脚本", "剪辑脚本", "配音文案"],
|
|
|
+ "半成品/原子": ["底图", "样图", "分镜视频"],
|
|
|
+ "半成品/组合": ["图层组合", "拼图"],
|
|
|
+ "准成品": ["歌词", "配音", "BGM", "字幕", "标题", "正文"],
|
|
|
+ "成品": ["成品图", "视频成品", "合成图"]
|
|
|
+ },
|
|
|
+ "知识类型": {
|
|
|
+ "知识库": ["知识库"]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": "suffix",
|
|
|
+ "label": "后缀",
|
|
|
+ "type": "flat",
|
|
|
+ "default": null,
|
|
|
+ "items": ["怎么做"]
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ const builderState = {};
|
|
|
+ DIMS.forEach(d => { builderState[d.id] = { l0: d.default !== undefined ? d.default : null, l1: null, l2: null }; });
|
|
|
+
|
|
|
+ let DATA = { queries: [], actions: [], types: [], matrix: [] };
|
|
|
+ let st = { qi: -1, fi: 0, channel: "all", sort: "score" };
|
|
|
+ let VIEW = [];
|
|
|
+
|
|
|
+ let currentProcTask = null;
|
|
|
+ let procPollInterval = null;
|
|
|
+ let isLogViewActive = false;
|
|
|
+ let searchPollInterval = null;
|
|
|
+ let reevalPollIntervals = {};
|
|
|
+
|
|
|
+ // Chip Rendering functions
|
|
|
+ function makeRow(indent) {
|
|
|
+ const el = document.createElement('div');
|
|
|
+ el.className = 'level-row' + (indent ? ' indent' : '');
|
|
|
+ return el;
|
|
|
+ }
|
|
|
+
|
|
|
+ function makeLabel(text, small) {
|
|
|
+ const el = document.createElement('span');
|
|
|
+ el.className = small ? 'sub-label' : 'dim-label';
|
|
|
+ el.textContent = text;
|
|
|
+ return el;
|
|
|
+ }
|
|
|
+
|
|
|
+ function makeChipsWrap() {
|
|
|
+ const el = document.createElement('div');
|
|
|
+ el.className = 'chips-wrap';
|
|
|
+ return el;
|
|
|
+ }
|
|
|
+
|
|
|
+ function makeChip(label, cls, onClick) {
|
|
|
+ const btn = document.createElement('button');
|
|
|
+ btn.className = 'chip ' + cls;
|
|
|
+ btn.textContent = label;
|
|
|
+ btn.addEventListener('click', onClick);
|
|
|
+ return btn;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderDim(dimId) {
|
|
|
+ const dim = DIMS.find(d => d.id === dimId);
|
|
|
+ const grp = document.querySelector('.dim-group[data-id="' + dimId + '"]');
|
|
|
+ grp.innerHTML = '';
|
|
|
+ const sel = builderState[dimId];
|
|
|
+
|
|
|
+ if (dim.type === 'flat') {
|
|
|
+ const row = makeRow(false);
|
|
|
+ row.appendChild(makeLabel(dim.label, false));
|
|
|
+ if (!dim.items || !dim.items.length) {
|
|
|
+ const ph = document.createElement('span');
|
|
|
+ ph.className = 'placeholder-text';
|
|
|
+ ph.textContent = '(待填写)';
|
|
|
+ row.appendChild(ph);
|
|
|
+ } else {
|
|
|
+ const chips = makeChipsWrap();
|
|
|
+ chips.appendChild(makeChip('无', sel.l0 == null ? 'none-active' : '', () => {
|
|
|
+ builderState[dimId].l0 = null; renderDim(dimId); updateQueryPreview();
|
|
|
+ }));
|
|
|
+ dim.items.forEach(item => {
|
|
|
+ chips.appendChild(makeChip(item, sel.l0 === item ? 'selected' : '', () => {
|
|
|
+ builderState[dimId].l0 = item; renderDim(dimId); updateQueryPreview();
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ row.appendChild(chips);
|
|
|
+ }
|
|
|
+ grp.appendChild(row);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dim.type === 'hierarchical') {
|
|
|
+ const row0 = makeRow(false);
|
|
|
+ row0.appendChild(makeLabel(dim.label, false));
|
|
|
+ const chips0 = makeChipsWrap();
|
|
|
+ chips0.appendChild(makeChip('无', sel.l0 == null ? 'none-active' : '', () => {
|
|
|
+ builderState[dimId] = { l0: null, l1: null, l2: null }; renderDim(dimId); updateQueryPreview();
|
|
|
+ }));
|
|
|
+ Object.keys(dim.data).forEach(key => {
|
|
|
+ chips0.appendChild(makeChip(key, sel.l0 === key ? 'selected' : '', () => {
|
|
|
+ builderState[dimId] = { l0: key, l1: null, l2: null }; renderDim(dimId); updateQueryPreview();
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ row0.appendChild(chips0);
|
|
|
+ grp.appendChild(row0);
|
|
|
+
|
|
|
+ if (sel.l0 && dim.data[sel.l0]) {
|
|
|
+ const L1keys = Object.keys(dim.data[sel.l0]);
|
|
|
+ const row1 = makeRow(true);
|
|
|
+ row1.appendChild(makeLabel(sel.l0, true));
|
|
|
+ const chips1 = makeChipsWrap();
|
|
|
+ chips1.appendChild(makeChip('无', sel.l1 == null ? 'none-active' : '', () => {
|
|
|
+ builderState[dimId].l1 = null; builderState[dimId].l2 = null; renderDim(dimId); updateQueryPreview();
|
|
|
+ }));
|
|
|
+ L1keys.forEach(key => {
|
|
|
+ chips1.appendChild(makeChip(key, sel.l1 === key ? 'selected' : '', () => {
|
|
|
+ builderState[dimId].l1 = key; builderState[dimId].l2 = null; renderDim(dimId); updateQueryPreview();
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ row1.appendChild(chips1);
|
|
|
+ grp.appendChild(row1);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (sel.l0 && sel.l1 && dim.data[sel.l0] && dim.data[sel.l0][sel.l1]) {
|
|
|
+ const L2items = dim.data[sel.l0][sel.l1];
|
|
|
+ const row2 = makeRow(true);
|
|
|
+ row2.appendChild(makeLabel(sel.l1, true));
|
|
|
+ const chips2 = makeChipsWrap();
|
|
|
+ chips2.appendChild(makeChip('无', sel.l2 == null ? 'none-active' : '', () => {
|
|
|
+ builderState[dimId].l2 = null; renderDim(dimId); updateQueryPreview();
|
|
|
+ }));
|
|
|
+ L2items.forEach(item => {
|
|
|
+ chips2.appendChild(makeChip(item, sel.l2 === item ? 'selected' : '', () => {
|
|
|
+ builderState[dimId].l2 = item; renderDim(dimId); updateQueryPreview();
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ row2.appendChild(chips2);
|
|
|
+ grp.appendChild(row2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function getSelections() {
|
|
|
+ return DIMS.map(dim => {
|
|
|
+ const sel = builderState[dim.id];
|
|
|
+ if (dim.type === 'flat') {
|
|
|
+ return sel.l0 ? { dim: dim.label, path: [sel.l0], val: sel.l0 } : null;
|
|
|
+ }
|
|
|
+ if (dim.type === 'hierarchical') {
|
|
|
+ const path = [sel.l0, sel.l1, sel.l2].filter(Boolean);
|
|
|
+ if (!path.length) return null;
|
|
|
+ return { dim: dim.label, path, val: path[path.length - 1] };
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }).filter(Boolean);
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateQueryPreview() {
|
|
|
+ const sels = getSelections();
|
|
|
+ const tagsEl = document.getElementById('previewTags');
|
|
|
+ const pathEl = document.getElementById('previewPath');
|
|
|
+ tagsEl.innerHTML = '';
|
|
|
+ pathEl.textContent = '';
|
|
|
+
|
|
|
+ if (!sels.length) {
|
|
|
+ tagsEl.innerHTML = '<span class="preview-empty">(请选择维度标签)</span>';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ sels.forEach((s, i) => {
|
|
|
+ if (i > 0) {
|
|
|
+ const sep = document.createElement('span');
|
|
|
+ sep.className = 'preview-sep';
|
|
|
+ sep.textContent = '·';
|
|
|
+ tagsEl.appendChild(sep);
|
|
|
+ }
|
|
|
+ const tag = document.createElement('span');
|
|
|
+ tag.className = 'preview-tag';
|
|
|
+ tag.textContent = s.val;
|
|
|
+ tagsEl.appendChild(tag);
|
|
|
+ });
|
|
|
+
|
|
|
+ pathEl.textContent = sels.map(s => '[' + s.dim + '] ' + s.path.join(' › ')).join(' ');
|
|
|
+ }
|
|
|
+
|
|
|
+ function getQueryText() {
|
|
|
+ return getSelections().map(s => s.val).join(' ');
|
|
|
+ }
|
|
|
+
|
|
|
+ function clearAll() {
|
|
|
+ DIMS.forEach(d => { builderState[d.id] = { l0: null, l1: null, l2: null }; });
|
|
|
+ DIMS.forEach(d => renderDim(d.id));
|
|
|
+ updateQueryPreview();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Sidebar navigation functions
|
|
|
+ function renderSidebar() {
|
|
|
+ const list = document.getElementById("querySidebarList");
|
|
|
+ list.innerHTML = "";
|
|
|
+ if (!DATA.queries.length) {
|
|
|
+ list.innerHTML = `<div style="color: #ccc; font-size: 13px; text-align: center; padding: 24px 0;">(暂无检索记录,请在上方组织新 Query 进行搜索)</div>`;
|
|
|
+ document.getElementById("resultsArea").style.display = "none";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ DATA.queries.forEach((q, i) => {
|
|
|
+ const div = document.createElement("div");
|
|
|
+ div.className = "sidebar-item" + (st.qi === i ? " active" : "");
|
|
|
+ div.innerHTML = `
|
|
|
+ <div class="q-text">${esc(q.original_q)}</div>
|
|
|
+ <div class="meta">
|
|
|
+ <span>采纳/命中: <strong>${q.hits}</strong></span>
|
|
|
+ <span>总帖数: ${q.tot}</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ div.onclick = () => {
|
|
|
+ st.qi = i;
|
|
|
+ st.fi = 0;
|
|
|
+ st.channel = "all";
|
|
|
+ renderSidebar();
|
|
|
+ renderGrid();
|
|
|
+ renderHead();
|
|
|
+ document.getElementById("resultsArea").style.display = "block";
|
|
|
+ };
|
|
|
+ list.appendChild(div);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function togglePlatformInput(chk) {
|
|
|
+ const parent = chk.closest('div');
|
|
|
+ if (!parent) return;
|
|
|
+ const numInput = parent.querySelector('input[type="number"]');
|
|
|
+ if (!numInput) return;
|
|
|
+ if (chk.checked) {
|
|
|
+ numInput.disabled = false;
|
|
|
+ numInput.style.opacity = "1";
|
|
|
+ parent.style.opacity = "1";
|
|
|
+ } else {
|
|
|
+ numInput.disabled = true;
|
|
|
+ numInput.style.opacity = "0.4";
|
|
|
+ parent.style.opacity = "0.6";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Search and evaluation trigger & log polling
|
|
|
+ function runSearchAndEvaluate() {
|
|
|
+ const q = getQueryText();
|
|
|
+ if (!q) {
|
|
|
+ alert("请先选择标签,组成要检索的 Query 词!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const checkboxes = document.querySelectorAll('input[name="platform"]:checked');
|
|
|
+ if (checkboxes.length === 0) {
|
|
|
+ alert("请至少选择一个目标检索渠道!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const platforms = Array.from(checkboxes).map(c => {
|
|
|
+ const parent = c.closest('div');
|
|
|
+ const numInput = parent ? parent.querySelector('input[type="number"]') : null;
|
|
|
+ const count = numInput ? parseInt(numInput.value) : 20;
|
|
|
+ return `${c.value}:${count}`;
|
|
|
+ }).join(",");
|
|
|
+
|
|
|
+ const searchBtn = document.getElementById("searchBtn");
|
|
|
+ searchBtn.disabled = true;
|
|
|
+ searchBtn.textContent = "⌛ 正在启动...";
|
|
|
+
|
|
|
+ const consoleCard = document.getElementById("execConsoleCard");
|
|
|
+ consoleCard.style.display = "flex";
|
|
|
+ const consoleOutput = document.getElementById("consoleOutput");
|
|
|
+ consoleOutput.textContent = "⏳ 正在向服务器提交搜索评估任务...\n";
|
|
|
+
|
|
|
+ const statusSpan = document.getElementById("consoleStatus");
|
|
|
+ statusSpan.textContent = "starting";
|
|
|
+ statusSpan.style.color = "var(--amber)";
|
|
|
+ document.getElementById("consoleTitle").textContent = `🚀 搜索并评估任务控制台 - [${q}]`;
|
|
|
+
|
|
|
+ fetch("/api/run_search_eval", {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
+ body: JSON.stringify({ query: q, platforms: platforms })
|
|
|
+ })
|
|
|
+ .then(r => r.json().then(d => ({ ok: r.ok, d })))
|
|
|
+ .then(({ ok, d }) => {
|
|
|
+ if (!ok) {
|
|
|
+ searchBtn.disabled = false;
|
|
|
+ searchBtn.textContent = "⚡ 搜索并评估";
|
|
|
+ statusSpan.textContent = "failed";
|
|
|
+ statusSpan.style.color = "var(--rose)";
|
|
|
+ consoleOutput.textContent += `❌ 启动失败: ${d.error || "未知错误"}\n`;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ statusSpan.textContent = "running";
|
|
|
+ statusSpan.style.color = "#00ff00";
|
|
|
+ consoleOutput.textContent += `✓ 搜评流程异步启动完成,正在获取实时日志...\n`;
|
|
|
+
|
|
|
+ startSearchPolling(q);
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ searchBtn.disabled = false;
|
|
|
+ searchBtn.textContent = "⚡ 搜索并评估";
|
|
|
+ statusSpan.textContent = "failed";
|
|
|
+ statusSpan.style.color = "var(--rose)";
|
|
|
+ consoleOutput.textContent += `❌ 请求失败: ${err}\n`;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function startSearchPolling(q) {
|
|
|
+ if (searchPollInterval) clearInterval(searchPollInterval);
|
|
|
+
|
|
|
+ const statusSpan = document.getElementById("consoleStatus");
|
|
|
+ const consoleOutput = document.getElementById("consoleOutput");
|
|
|
+ const searchBtn = document.getElementById("searchBtn");
|
|
|
+
|
|
|
+ const poll = () => {
|
|
|
+ fetch(`/api/search_eval_status?q=${encodeURIComponent(q)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ if (d.status === "success") {
|
|
|
+ clearInterval(searchPollInterval);
|
|
|
+ searchPollInterval = null;
|
|
|
+
|
|
|
+ statusSpan.textContent = "success";
|
|
|
+ statusSpan.style.color = "var(--mint)";
|
|
|
+ searchBtn.disabled = false;
|
|
|
+ searchBtn.textContent = "⚡ 搜索并评估";
|
|
|
+
|
|
|
+ consoleOutput.textContent += "\n🎉 搜评流程全部成功执行完毕,已保存于 runs_new 下!\n";
|
|
|
+ consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
|
|
+
|
|
|
+ loadData(true);
|
|
|
+ } else if (d.status === "failed") {
|
|
|
+ clearInterval(searchPollInterval);
|
|
|
+ searchPollInterval = null;
|
|
|
+
|
|
|
+ statusSpan.textContent = "failed";
|
|
|
+ statusSpan.style.color = "var(--rose)";
|
|
|
+ searchBtn.disabled = false;
|
|
|
+ searchBtn.textContent = "⚡ 搜索并评估";
|
|
|
+
|
|
|
+ consoleOutput.textContent += `\n❌ 搜评任务执行失败: ${d.error || "未知原因"}\n`;
|
|
|
+ consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
|
|
+
|
|
|
+ loadData(true);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ fetch(`/api/search_eval_log?q=${encodeURIComponent(q)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ if (d.log) {
|
|
|
+ consoleOutput.textContent = d.log;
|
|
|
+ consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ poll();
|
|
|
+ searchPollInterval = setInterval(poll, 1500);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Grid rendering logic
|
|
|
+ function curForm() {
|
|
|
+ return st.qi === -1 ? null : (DATA.queries[st.qi] ? DATA.queries[st.qi].forms[st.fi] : null);
|
|
|
+ }
|
|
|
+
|
|
|
+ function isItemDiscarded(it) {
|
|
|
+ if (it.anomaly) return false;
|
|
|
+ const input = document.getElementById("relThreshold");
|
|
|
+ const userThreshold = input ? parseFloat(input.value) : 4.0;
|
|
|
+
|
|
|
+ let isDiscard = false;
|
|
|
+ const relVal = it.production_relevance !== null && it.production_relevance !== undefined ? parseFloat(it.production_relevance) : null;
|
|
|
+ if (relVal !== null && !isNaN(relVal)) {
|
|
|
+ if (relVal < userThreshold) {
|
|
|
+ isDiscard = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (it.recency_hard !== null && it.recency_hard !== undefined && it.recency_hard < 2) {
|
|
|
+ isDiscard = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (it.overall !== null && it.overall !== undefined) {
|
|
|
+ if (it.overall < 6.0) {
|
|
|
+ isDiscard = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return isDiscard;
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateThresholdLimits() {
|
|
|
+ const input = document.getElementById("relThreshold");
|
|
|
+ if (input) {
|
|
|
+ input.max = "10";
|
|
|
+ input.value = "4.0";
|
|
|
+ input.step = "0.5";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateChannelCounts() {
|
|
|
+ const f = curForm();
|
|
|
+ const div = document.getElementById("channelCountsRow");
|
|
|
+ if (!div) return;
|
|
|
+ if (!f || !f.results || f.results.length === 0) {
|
|
|
+ div.innerHTML = "";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const counts = {};
|
|
|
+ f.results.forEach(r => {
|
|
|
+ const pk = r.platformKey || "other";
|
|
|
+ counts[pk] = (counts[pk] || 0) + 1;
|
|
|
+ });
|
|
|
+ const parts = [];
|
|
|
+ const sortedKeys = Object.keys(counts).sort((a, b) => counts[b] - counts[a]);
|
|
|
+ sortedKeys.forEach(pk => {
|
|
|
+ const name = PLATC[pk] || pk;
|
|
|
+ parts.push(`
|
|
|
+ <span style="background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 2px 8px; display: inline-flex; align-items: center; gap: 6px; font-weight: 500; font-size: 11.5px; box-shadow: 0 1px 3px rgba(0,0,0,0.02);">
|
|
|
+ <span class="platform p-${pk}" style="width: 7px; height: 7px; border-radius: 50%; display: inline-block; padding: 0; margin-right: 0;"></span>
|
|
|
+ <span style="color: var(--muted);">${name}</span>
|
|
|
+ <strong style="color: var(--ink); font-weight: 700;">${counts[pk]}</strong>
|
|
|
+ </span>
|
|
|
+ `);
|
|
|
+ });
|
|
|
+ div.innerHTML = `<span style="font-weight: 700; color: var(--muted); margin-right: 4px; font-size: 12px;">已收录帖数:</span>` + parts.join("");
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderHead() {
|
|
|
+ const f = curForm();
|
|
|
+ const div = document.getElementById("stats");
|
|
|
+ if (!f) {
|
|
|
+ div.innerHTML = "";
|
|
|
+ const cDiv = document.getElementById("channelCountsRow");
|
|
|
+ if (cDiv) cDiv.innerHTML = "";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const total = f.results.length;
|
|
|
+ const report = f.results.filter(r => !r.anomaly && !isItemDiscarded(r)).length;
|
|
|
+ const discard = f.results.filter(r => !r.anomaly && isItemDiscarded(r)).length;
|
|
|
+ const failed = f.results.filter(r => r.anomaly).length;
|
|
|
+
|
|
|
+ div.innerHTML = `
|
|
|
+ <div class="stat"><strong>${total}</strong><span>平台抓取总数</span></div>
|
|
|
+ <div class="stat" style="color:var(--mint)"><strong>${report}</strong><span>采纳上报案例</span></div>
|
|
|
+ <div class="stat" style="color:var(--amber)"><strong>${discard}</strong><span>不符过滤弃用</span></div>
|
|
|
+ <div class="stat" style="color:var(--rose)"><strong>${failed}</strong><span>评估失败/异常</span></div>
|
|
|
+ `;
|
|
|
+ updateChannelCounts();
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderGrid() {
|
|
|
+ const f = curForm();
|
|
|
+ const grid = document.getElementById("grid");
|
|
|
+ if (!f) { grid.innerHTML = `<div style="grid-column:1/-1;text-align:center;color:#999;padding:40px 0;">暂无数据</div>`; return; }
|
|
|
+
|
|
|
+ // Filter platforms checkboxes and render buttons
|
|
|
+ const platforms = Array.from(new Set(f.results.map(r => r.platformKey)));
|
|
|
+ const filterWrap = document.getElementById("platformFilterWrap");
|
|
|
+
|
|
|
+ let pFilterHtml = `<button class="btn ${st.channel === 'all' ? 'active' : ''}" onclick="setChannel('all')">全部渠道 (${f.results.length})</button>`;
|
|
|
+ platforms.forEach(p => {
|
|
|
+ const count = f.results.filter(r => r.platformKey === p).length;
|
|
|
+ pFilterHtml += `<button class="btn ${st.channel === p ? 'active' : ''}" onclick="setChannel('${p}')">${PLATC[p] || p} (${count})</button>`;
|
|
|
+ });
|
|
|
+ filterWrap.innerHTML = pFilterHtml;
|
|
|
+
|
|
|
+ let list = f.results;
|
|
|
+ if (st.channel !== "all") {
|
|
|
+ list = list.filter(r => r.platformKey === st.channel);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Sort
|
|
|
+ const sortBy = document.getElementById("sort").value;
|
|
|
+ if (sortBy === "score") {
|
|
|
+ list.sort((a, b) => (b.overall || 0) - (a.overall || 0));
|
|
|
+ } else if (sortBy === "date") {
|
|
|
+ list.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
|
+ } else if (sortBy === "platform") {
|
|
|
+ list.sort((a, b) => (a.platformKey || "").localeCompare(b.platformKey || ""));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (list.length === 0) {
|
|
|
+ grid.innerHTML = `<div style="grid-column:1/-1;text-align:center;color:#999;padding:40px 0;">无筛选结果</div>`;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ grid.innerHTML = list.map((it, i) => {
|
|
|
+ const idx = f.results.indexOf(it);
|
|
|
+ const isDiscard = isItemDiscarded(it);
|
|
|
+ const overallStr = it.overall !== null && it.overall !== undefined ? it.overall.toFixed(1) : '—';
|
|
|
+ const discardOverlay = (isDiscard && !it.anomaly) ? `
|
|
|
+ <div class="discard-overlay">
|
|
|
+ <div class="discard-badge">已过滤</div>
|
|
|
+ <div class="discard-reason">${esc(it.reason || "时效性或综合质量均分未达标")}</div>
|
|
|
+ </div>
|
|
|
+ ` : '';
|
|
|
+
|
|
|
+ const thumbs = it.images && it.images.length > 0 ? `
|
|
|
+ <div class="thumbs">
|
|
|
+ ${it.images.map(img => `<img src="${esc(img)}" referrerpolicy="no-referrer" onerror="this.src='${NOIMG}'">`).join('')}
|
|
|
+ </div>
|
|
|
+ ` : '';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <div class="result ${isDiscard ? 'discard' : ''}" data-idx="${idx}">
|
|
|
+ ${discardOverlay}
|
|
|
+ ${thumbs}
|
|
|
+ <div class="body">
|
|
|
+ <div class="meta">
|
|
|
+ <span class="platform p-${it.platformKey}">${esc(it.platform)}</span>
|
|
|
+ <span>${esc(it.date)}</span>
|
|
|
+ </div>
|
|
|
+ <h2 title="${esc(it.title)}">${esc(it.title)}</h2>
|
|
|
+ <div class="excerpt">${esc(it.text)}</div>
|
|
|
+
|
|
|
+ <div class="tags">
|
|
|
+ ${(it.tools || []).map(t => `<span class="tag">${esc(t)}</span>`).join('')}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="scorebar">
|
|
|
+ <div class="overall">
|
|
|
+ <span class="decision ${isDiscard ? 'discard' : ''}">${isDiscard ? '已过滤' : '上报'}</span>
|
|
|
+ <span class="score" style="color: ${it.anomaly ? 'var(--rose)' : '#2563eb'}">${overallStr}</span>
|
|
|
+ </div>
|
|
|
+ <div class="group-snapshot">
|
|
|
+ ${groupSnapshot(it)}
|
|
|
+ </div>
|
|
|
+ <div class="actions">
|
|
|
+ <a href="${esc(it.url)}" target="_blank" onclick="event.stopPropagation()">🌐 源链接</a>
|
|
|
+ <button onclick="event.stopPropagation(); showDetail(${idx})">🔍 查看详情</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+ }
|
|
|
+
|
|
|
+ function setChannel(ch) {
|
|
|
+ st.channel = ch;
|
|
|
+ renderGrid();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Modal dialogue script
|
|
|
+ function esc(s) {
|
|
|
+ return (s === undefined || s === null ? "" : String(s)).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
|
+ }
|
|
|
+
|
|
|
+ const PLATC = { xhs: "小红书", gzh: "公众号", zhihu: "知乎", x: "X", bili: "B站", douyin: "抖音", sph: "视频号", youtube: "YouTube", github: "GitHub", toutiao: "头条", weibo: "微博" };
|
|
|
+ const NOIMG = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='400'%3E%3Crect width='600' height='400' fill='%23eee7dc'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%236f6961' font-size='28'%3ENo image%3C/text%3E%3C/svg%3E";
|
|
|
+
|
|
|
+ const scoreGroupsNew = [
|
|
|
+ { id: "relevance", title: "相关性", short: "相关", keys: ["relevance_production", "relevance_query"] },
|
|
|
+ { id: "fixed", title: "固定维度", short: "固定", keys: ["recency", "popularity", "feedback"] },
|
|
|
+ { id: "usecase", title: "用例维度", short: "用例", keys: ["realism", "expressiveness"] },
|
|
|
+ { id: "dynamic", title: "动态维度", short: "动态", keys: [] }
|
|
|
+ ];
|
|
|
+
|
|
|
+ function makeModalRow(label, scoreKey, it) {
|
|
|
+ const rawV = it.scores[scoreKey];
|
|
|
+ const v = (rawV !== undefined && rawV !== null) ? parseFloat(rawV) : NaN;
|
|
|
+ const hasScore = !isNaN(v);
|
|
|
+ const valStr = hasScore ? (Number.isInteger(v) ? v : v.toFixed(1)) : '-';
|
|
|
+ const barV = hasScore ? v : 0;
|
|
|
+ const reason = it.score_reasons ? it.score_reasons[scoreKey] : '';
|
|
|
+ const infoIcon = reason ? `<span class="info-icon" onclick="pinScoreReason(this, '${esc(label)}', '${esc(scoreKey)}')" title="评判理由: ${esc(reason)}" style="margin-left: 5px; cursor: pointer; color: var(--muted); opacity: 0.7;">ⓘ</span>` : '';
|
|
|
+ return `
|
|
|
+ <div class="sc-row ${!hasScore ? 'missing' : ''}">
|
|
|
+ <span class="label">${esc(label)}</span>
|
|
|
+ <div class="bar-wrap">
|
|
|
+ <div class="bar">
|
|
|
+ <div class="bar-fill" style="--v: ${barV}"></div>
|
|
|
+ </div>
|
|
|
+ <span class="value">${valStr}</span>
|
|
|
+ ${infoIcon}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ function pinScoreReason(el, label, key) {
|
|
|
+ alert(`${label} 判定理由:\n\n${el.title.replace("评判理由: ", "")}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ function groupAverage(it, g) {
|
|
|
+ if (!it.scores) return null;
|
|
|
+ let keys = g.keys;
|
|
|
+ if (g.id === "dynamic") {
|
|
|
+ keys = [];
|
|
|
+ if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
|
|
|
+ keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
|
|
|
+ }
|
|
|
+ if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
|
|
|
+ keys.push("step_input", "step_implementation", "step_output", "step_generality");
|
|
|
+ }
|
|
|
+ if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
|
|
|
+ keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
|
|
|
+ return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ function getQualityAverage(it) {
|
|
|
+ if (!it.scores) return null;
|
|
|
+ const keys = ["recency", "popularity", "feedback", "realism", "expressiveness"];
|
|
|
+ if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
|
|
|
+ keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
|
|
|
+ }
|
|
|
+ if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
|
|
|
+ keys.push("step_input", "step_implementation", "step_output", "step_generality");
|
|
|
+ }
|
|
|
+ if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
|
|
|
+ keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
|
|
|
+ }
|
|
|
+ const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
|
|
|
+ return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ function fmt(v) {
|
|
|
+ return v === null ? "N/A" : v.toFixed(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ function groupSnapshot(it) {
|
|
|
+ const relAvg = groupAverage(it, scoreGroupsNew[0]);
|
|
|
+ const qualAvg = getQualityAverage(it);
|
|
|
+ return `
|
|
|
+ <div class="group-pill"><span>相关度</span><strong>${fmt(relAvg)}</strong></div>
|
|
|
+ <div class="group-pill"><span>制作质量</span><strong>${fmt(qualAvg)}</strong></div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderNewScores(it) {
|
|
|
+ const relAvg = groupAverage(it, scoreGroupsNew[0]);
|
|
|
+ const relAvgStr = relAvg !== null ? relAvg.toFixed(1) : 'N/A';
|
|
|
+
|
|
|
+ let relevanceHtml = `
|
|
|
+ <div class="sc-card">
|
|
|
+ <div class="sc-card-head">
|
|
|
+ <div class="title"><span class="badge">01</span>相关性</div>
|
|
|
+ <div class="avg-score">均分 <strong>${relAvgStr}</strong><span style="font-size:12px;color:#9ca3af;">/10</span></div>
|
|
|
+ </div>
|
|
|
+ <div class="sc-card-body">
|
|
|
+ ${makeModalRow("和内容制作知识相关", "relevance_production", it)}
|
|
|
+ ${makeModalRow("和 query 相关", "relevance_query", it)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ const qualAvg = getQualityAverage(it);
|
|
|
+ const qualAvgStr = qualAvg !== null ? qualAvg.toFixed(1) : 'N/A';
|
|
|
+
|
|
|
+ let qualityHtml = `
|
|
|
+ <div class="sc-card">
|
|
|
+ <div class="sc-card-head">
|
|
|
+ <div class="title"><span class="badge">02</span>质量</div>
|
|
|
+ <div class="avg-score">均分 <strong>${qualAvgStr}</strong><span style="font-size:12px;color:#9ca3af;">/10</span></div>
|
|
|
+ </div>
|
|
|
+ <div class="sc-card-body">
|
|
|
+ <div class="sc-sub-header">固定维度</div>
|
|
|
+ ${makeModalRow("时效性", "recency", it)}
|
|
|
+ ${makeModalRow("热度性", "popularity", it)}
|
|
|
+ ${makeModalRow("评论反馈", "feedback", it)}
|
|
|
+
|
|
|
+ <div class="sc-sub-header">用例</div>
|
|
|
+ ${makeModalRow("真实感 (非AI)", "realism", it)}
|
|
|
+ ${makeModalRow("表现力", "expressiveness", it)}
|
|
|
+ `;
|
|
|
+
|
|
|
+ let dynamicHtml = '';
|
|
|
+ if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
|
|
|
+ dynamicHtml += `
|
|
|
+ <div class="sc-sub-header">工序</div>
|
|
|
+ ${makeModalRow("流程完整性", "procedure_completeness", it)}
|
|
|
+ ${makeModalRow("输入完整性", "procedure_input", it)}
|
|
|
+ ${makeModalRow("实现完整性", "procedure_implementation", it)}
|
|
|
+ ${makeModalRow("输出完整性", "procedure_output", it)}
|
|
|
+ ${makeModalRow("泛化性", "procedure_generality", it)}
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
|
|
|
+ dynamicHtml += `
|
|
|
+ <div class="sc-sub-header">能力</div>
|
|
|
+ ${makeModalRow("输入完整性", "step_input", it)}
|
|
|
+ ${makeModalRow("实现完整性", "step_implementation", it)}
|
|
|
+ ${makeModalRow("输出完整性", "step_output", it)}
|
|
|
+ ${makeModalRow("泛化性", "step_generality", it)}
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
|
|
|
+ dynamicHtml += `
|
|
|
+ <div class="sc-sub-header">工具</div>
|
|
|
+ ${makeModalRow("能力边界覆盖", "tool_boundary", it)}
|
|
|
+ ${makeModalRow("有效比较", "tool_comparison", it)}
|
|
|
+ ${makeModalRow("参数/接口具体性", "tool_specificity", it)}
|
|
|
+ ${makeModalRow("实操示例", "tool_example", it)}
|
|
|
+ ${makeModalRow("版本&限制", "tool_limits", it)}
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ qualityHtml += dynamicHtml + `
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ return relevanceHtml + qualityHtml;
|
|
|
+ }
|
|
|
+
|
|
|
+ let detailDialogIndex = -1;
|
|
|
+ function showDetail(idx) {
|
|
|
+ detailDialogIndex = idx;
|
|
|
+ const f = curForm();
|
|
|
+ const it = f.results[idx];
|
|
|
+
|
|
|
+ const isDiscard = isItemDiscarded(it);
|
|
|
+ const overallStr = it.overall !== null && it.overall !== undefined ? it.overall.toFixed(1) : '—';
|
|
|
+
|
|
|
+ document.getElementById("modalMeta").innerHTML = `
|
|
|
+ <span class="platform p-${it.platformKey}">${esc(it.platform)}</span>
|
|
|
+ <span>发布日期: ${esc(it.date)}</span>
|
|
|
+ <span style="margin-left:auto; font-family: monospace;">CaseID: ${esc(it.case_id)}</span>
|
|
|
+ `;
|
|
|
+ document.getElementById("modalTitle").textContent = it.title;
|
|
|
+ document.getElementById("modalReason").textContent = it.reason || "时效与品质良好";
|
|
|
+ document.getElementById("modalText").textContent = it.text;
|
|
|
+
|
|
|
+ document.getElementById("modalOverallScoreVal").textContent = overallStr;
|
|
|
+ document.getElementById("modalOverallScoreVal").style.color = it.anomaly ? "var(--rose)" : "#2563eb";
|
|
|
+
|
|
|
+ document.getElementById("modalScores").innerHTML = renderNewScores(it);
|
|
|
+
|
|
|
+ const tags = (it.tools || []).map(t => `<span class="tag">${esc(t)}</span>`).join("");
|
|
|
+ const foundBy = (it.found_by || []).map(q => `<span class="tag" style="background:#e0f2fe; color:#0369a1;">${esc(q)}</span>`).join("");
|
|
|
+ document.getElementById("modalTags").innerHTML = tags + foundBy;
|
|
|
+
|
|
|
+ const imgDiv = document.getElementById("modalImages");
|
|
|
+ imgDiv.innerHTML = "";
|
|
|
+ if (it.images && it.images.length > 0) {
|
|
|
+ it.images.forEach(img => {
|
|
|
+ imgDiv.innerHTML += `<img src="${esc(img)}" referrerpolicy="no-referrer" onclick="window.open(this.src)" style="cursor:zoom-in;">`;
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ imgDiv.innerHTML = `<div style="grid-column:1/-1; color:#999; font-size:13px;">无图片</div>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ const tabs = document.getElementById("modalTabs");
|
|
|
+ tabs.style.display = "flex";
|
|
|
+
|
|
|
+ switchModalTab("detail");
|
|
|
+
|
|
|
+ const dialog = document.getElementById("detailDialog");
|
|
|
+ dialog.classList.remove("fullscreen");
|
|
|
+ document.getElementById("toggleFullscreenBtn").textContent = "📺 全屏";
|
|
|
+ dialog.showModal();
|
|
|
+ }
|
|
|
+
|
|
|
+ function switchModalTab(tab) {
|
|
|
+ const dBtn = document.getElementById("tabDetailBtn");
|
|
|
+ const pBtn = document.getElementById("tabProcedureBtn");
|
|
|
+ const dContent = document.getElementById("modalContentDetail");
|
|
|
+ const pContent = document.getElementById("modalContentProcedure");
|
|
|
+
|
|
|
+ if (tab === "detail") {
|
|
|
+ dBtn.classList.add("active");
|
|
|
+ pBtn.classList.remove("active");
|
|
|
+ dContent.style.display = "grid";
|
|
|
+ pContent.style.display = "none";
|
|
|
+ } else {
|
|
|
+ pBtn.classList.add("active");
|
|
|
+ dBtn.classList.remove("active");
|
|
|
+ dContent.style.display = "none";
|
|
|
+ pContent.style.display = "flex";
|
|
|
+
|
|
|
+ loadProcedureState();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function toggleModalFullscreen() {
|
|
|
+ const dialog = document.getElementById("detailDialog");
|
|
|
+ const btn = document.getElementById("toggleFullscreenBtn");
|
|
|
+ if (dialog.classList.contains("fullscreen")) {
|
|
|
+ dialog.classList.remove("fullscreen");
|
|
|
+ btn.textContent = "📺 全屏";
|
|
|
+ } else {
|
|
|
+ dialog.classList.add("fullscreen");
|
|
|
+ btn.textContent = "📺 退出全屏";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Procedure workflow extraction & terminal logger
|
|
|
+ function loadProcedureState() {
|
|
|
+ if (procPollInterval) clearInterval(procPollInterval);
|
|
|
+
|
|
|
+ const f = curForm();
|
|
|
+ const it = f.results[detailDialogIndex];
|
|
|
+ const q = DATA.queries[st.qi].key;
|
|
|
+ const form = "A";
|
|
|
+ const caseId = it.case_id;
|
|
|
+
|
|
|
+ const setupPanel = document.getElementById("procSetupPanel");
|
|
|
+ const consolePanel = document.getElementById("procConsolePanel");
|
|
|
+ const iframe = document.getElementById("procedureIframe");
|
|
|
+ const statusText = document.getElementById("procStatusText");
|
|
|
+ const actionBtns = document.getElementById("procActionBtns");
|
|
|
+
|
|
|
+ setupPanel.style.display = "none";
|
|
|
+ consolePanel.style.display = "none";
|
|
|
+ iframe.style.display = "none";
|
|
|
+ actionBtns.innerHTML = "";
|
|
|
+ statusText.textContent = "正在检测工序状态...";
|
|
|
+
|
|
|
+ fetch(`/api/procedure_status?q=${encodeURIComponent(q)}&form=${form}&case_id=${encodeURIComponent(caseId)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ if (d.status === "success") {
|
|
|
+ statusText.textContent = "工序状态: 已提取";
|
|
|
+ iframe.src = "/" + d.procedure_html;
|
|
|
+ iframe.style.display = "block";
|
|
|
+
|
|
|
+ actionBtns.innerHTML = `
|
|
|
+ <button class="btn" onclick="showProcSetupPanel()">♻️ 重新生成工序</button>
|
|
|
+ <button class="btn" onclick="openProcLog('${esc(q)}', '${form}', '${esc(caseId)}')">📋 提取日志</button>
|
|
|
+ `;
|
|
|
+ } else if (d.status === "running") {
|
|
|
+ statusText.textContent = "工序状态: 正在提取中...";
|
|
|
+ consolePanel.style.display = "flex";
|
|
|
+ pollProcLog(q, form, caseId);
|
|
|
+ } else if (d.status === "failed") {
|
|
|
+ statusText.textContent = "工序状态: 提取失败";
|
|
|
+ consolePanel.style.display = "flex";
|
|
|
+ document.getElementById("procConsoleOutput").textContent = d.error || "提取进程异常退出。";
|
|
|
+ actionBtns.innerHTML = `
|
|
|
+ <button class="btn" onclick="showProcSetupPanel()">♻️ 重试提取</button>
|
|
|
+ <button class="btn" onclick="openProcLog('${esc(q)}', '${form}', '${esc(caseId)}')">📋 提取日志</button>
|
|
|
+ `;
|
|
|
+ } else {
|
|
|
+ // not_started
|
|
|
+ statusText.textContent = "工序状态: 未提取";
|
|
|
+ showProcSetupPanel();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function showProcSetupPanel() {
|
|
|
+ document.getElementById("procSetupPanel").style.display = "flex";
|
|
|
+ document.getElementById("procConsolePanel").style.display = "none";
|
|
|
+ document.getElementById("procedureIframe").style.display = "none";
|
|
|
+
|
|
|
+ const engine = document.getElementById("procEngineSelect").value;
|
|
|
+ populateModels(engine);
|
|
|
+ }
|
|
|
+
|
|
|
+ const MODELS_BY_ENGINE = {
|
|
|
+ cyber_runner: [
|
|
|
+ "google/gemini-3.1-flash-lite",
|
|
|
+ "google/gemini-3.5-flash",
|
|
|
+ "anthropic/claude-3.5-haiku",
|
|
|
+ "anthropic/claude-3.5-sonnet",
|
|
|
+ "qwen/qwen-2.5-72b-instruct"
|
|
|
+ ],
|
|
|
+ claude_sdk: [
|
|
|
+ "claude-3-5-sonnet-20241022",
|
|
|
+ "claude-3-5-haiku-20241022"
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ function populateModels(engine) {
|
|
|
+ const select = document.getElementById("procModelSelect");
|
|
|
+ const models = MODELS_BY_ENGINE[engine] || [];
|
|
|
+ select.innerHTML = models.map(m => `<option value="${esc(m)}">${esc(m)}</option>`).join("");
|
|
|
+ }
|
|
|
+
|
|
|
+ function onProcEngineChange() {
|
|
|
+ const engine = document.getElementById("procEngineSelect").value;
|
|
|
+ populateModels(engine);
|
|
|
+ }
|
|
|
+
|
|
|
+ function startProcedureExtraction() {
|
|
|
+ const f = curForm();
|
|
|
+ const it = f.results[detailDialogIndex];
|
|
|
+ const q = DATA.queries[st.qi].key;
|
|
|
+ const form = "A";
|
|
|
+ const caseId = it.case_id;
|
|
|
+
|
|
|
+ const engine = document.getElementById("procEngineSelect").value;
|
|
|
+ const model = document.getElementById("procModelSelect").value;
|
|
|
+
|
|
|
+ document.getElementById("procSetupPanel").style.display = "none";
|
|
|
+ document.getElementById("procConsolePanel").style.display = "flex";
|
|
|
+ document.getElementById("procStatusText").textContent = "工序状态: 启动提取任务...";
|
|
|
+
|
|
|
+ fetch("/api/generate_procedure", {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
+ body: JSON.stringify({ q, form, case_id: caseId, engine, model })
|
|
|
+ })
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ if (d.status === "started") {
|
|
|
+ pollProcLog(q, form, caseId);
|
|
|
+ } else {
|
|
|
+ alert("提取任务启动失败: " + d.error);
|
|
|
+ showProcSetupPanel();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ alert("网络请求出错: " + err);
|
|
|
+ showProcSetupPanel();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function pollProcLog(q, form, caseId) {
|
|
|
+ if (procPollInterval) clearInterval(procPollInterval);
|
|
|
+
|
|
|
+ const consoleOutput = document.getElementById("procConsoleOutput");
|
|
|
+ const statusText = document.getElementById("procStatusText");
|
|
|
+ const consoleStatus = document.getElementById("procConsoleStatus");
|
|
|
+
|
|
|
+ consoleStatus.textContent = "streaming";
|
|
|
+ consoleStatus.style.color = "#00ff00";
|
|
|
+
|
|
|
+ const poll = () => {
|
|
|
+ fetch(`/api/procedure_status?q=${encodeURIComponent(q)}&form=${form}&case_id=${encodeURIComponent(caseId)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ if (d.status === "success") {
|
|
|
+ clearInterval(procPollInterval);
|
|
|
+ procPollInterval = null;
|
|
|
+ loadProcedureState();
|
|
|
+ } else if (d.status === "failed") {
|
|
|
+ clearInterval(procPollInterval);
|
|
|
+ procPollInterval = null;
|
|
|
+ statusText.textContent = "工序状态: 提取失败";
|
|
|
+ consoleOutput.textContent += `\n❌ 进程异常退出: ${d.error || "未知原因"}`;
|
|
|
+ consoleStatus.textContent = "failed";
|
|
|
+ consoleStatus.style.color = "var(--rose)";
|
|
|
+
|
|
|
+ document.getElementById("procActionBtns").innerHTML = `
|
|
|
+ <button class="btn" onclick="showProcSetupPanel()">♻️ 重试提取</button>
|
|
|
+ <button class="btn" onclick="openProcLog('${esc(q)}', '${form}', '${esc(caseId)}')">📋 提取日志</button>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ fetch(`/api/procedure_log?q=${encodeURIComponent(q)}&form=${form}&case_id=${encodeURIComponent(caseId)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ if (d.log) {
|
|
|
+ consoleOutput.textContent = d.log;
|
|
|
+ consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ poll();
|
|
|
+ procPollInterval = setInterval(poll, 1500);
|
|
|
+ }
|
|
|
+
|
|
|
+ function openProcLog(q, form, caseId) {
|
|
|
+ const win = window.open("", "_blank");
|
|
|
+ win.document.write("<h3>加载日志中...</h3>");
|
|
|
+ fetch(`/api/procedure_log?q=${encodeURIComponent(q)}&form=${form}&case_id=${encodeURIComponent(caseId)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ win.document.body.innerHTML = "";
|
|
|
+ const pre = win.document.createElement("pre");
|
|
|
+ pre.style.whiteSpace = "pre-wrap";
|
|
|
+ pre.style.fontFamily = "monospace";
|
|
|
+ pre.textContent = d.log || "无日志输出";
|
|
|
+ win.document.body.appendChild(pre);
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ win.document.body.innerHTML = "<h3>日志加载出错: " + err + "</h3>";
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Spec prompt editor script
|
|
|
+ const ALLOWED_SPEC_FILES = [
|
|
|
+ "README.md",
|
|
|
+ "tools.md",
|
|
|
+ "extraction/phase1-skeleton.md",
|
|
|
+ "extraction/phase2-normalize.md",
|
|
|
+ "extraction/phase3-finalize.md",
|
|
|
+ "taxonomy/type_suggestions.md"
|
|
|
+ ];
|
|
|
+
|
|
|
+ function openSpecEditor() {
|
|
|
+ const select = document.getElementById("specFileSelect");
|
|
|
+ select.innerHTML = ALLOWED_SPEC_FILES.map(f => `<option value="${esc(f)}">${esc(f)}</option>`).join("");
|
|
|
+
|
|
|
+ document.getElementById("specLoadStatus").textContent = "";
|
|
|
+ document.getElementById("specSaveStatus").textContent = "";
|
|
|
+ document.getElementById("specContentTextarea").value = "";
|
|
|
+
|
|
|
+ const dialog = document.getElementById("specEditorDialog");
|
|
|
+ dialog.showModal();
|
|
|
+ loadSpecFileContent();
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadSpecFileContent() {
|
|
|
+ const file = document.getElementById("specFileSelect").value;
|
|
|
+ const status = document.getElementById("specLoadStatus");
|
|
|
+ const textarea = document.getElementById("specContentTextarea");
|
|
|
+
|
|
|
+ status.textContent = "⏳ 正在读取...";
|
|
|
+ status.style.color = "var(--amber)";
|
|
|
+ textarea.disabled = true;
|
|
|
+
|
|
|
+ fetch(`/api/spec_content?file=${encodeURIComponent(file)}`)
|
|
|
+ .then(r => r.json().then(d => ({ ok: r.ok, d })))
|
|
|
+ .then(({ ok, d }) => {
|
|
|
+ textarea.disabled = false;
|
|
|
+ if (ok) {
|
|
|
+ textarea.value = d.content || "";
|
|
|
+ status.textContent = "✓ 读取成功";
|
|
|
+ status.style.color = "var(--mint)";
|
|
|
+ } else {
|
|
|
+ status.textContent = "❌ 读取失败: " + (d.error || "未知错误");
|
|
|
+ status.style.color = "var(--rose)";
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ textarea.disabled = false;
|
|
|
+ status.textContent = "❌ 读取失败: " + err;
|
|
|
+ status.style.color = "var(--rose)";
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function saveSpecFileContent() {
|
|
|
+ const file = document.getElementById("specFileSelect").value;
|
|
|
+ const content = document.getElementById("specContentTextarea").value;
|
|
|
+ const status = document.getElementById("specSaveStatus");
|
|
|
+
|
|
|
+ status.textContent = "⏳ 正在保存到磁盘...";
|
|
|
+ status.style.color = "var(--amber)";
|
|
|
+
|
|
|
+ fetch("/api/save_spec", {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
+ body: JSON.stringify({ file, content })
|
|
|
+ })
|
|
|
+ .then(r => r.json().then(d => ({ ok: r.ok, d })))
|
|
|
+ .then(({ ok, d }) => {
|
|
|
+ if (ok) {
|
|
|
+ status.textContent = "✅ 保存成功!新的 Prompt 规则已在本地生效。";
|
|
|
+ status.style.color = "var(--mint)";
|
|
|
+ setTimeout(() => {
|
|
|
+ if (status.textContent.includes("保存成功")) {
|
|
|
+ status.textContent = "";
|
|
|
+ }
|
|
|
+ }, 4000);
|
|
|
+ } else {
|
|
|
+ status.textContent = "❌ 保存失败: " + (d.error || "未知错误");
|
|
|
+ status.style.color = "var(--rose)";
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ status.textContent = "❌ 网络错误: " + err;
|
|
|
+ status.style.color = "var(--rose)";
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Re-eval current query result cases
|
|
|
+ function reevalCurrentQuery() {
|
|
|
+ if (st.qi === -1 || !DATA.queries[st.qi]) return;
|
|
|
+ const q = DATA.queries[st.qi].key;
|
|
|
+ const btn = document.getElementById("reevalBtn");
|
|
|
+
|
|
|
+ btn.disabled = true;
|
|
|
+ btn.textContent = `♻️ 重评中 ${q}...`;
|
|
|
+
|
|
|
+ fetch("/api/reeval", {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
+ body: JSON.stringify({ q })
|
|
|
+ })
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ if (d.status === "started") {
|
|
|
+ startReevalPolling(q);
|
|
|
+ } else {
|
|
|
+ alert("重评任务启动失败: " + d.error);
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = "♻️ 重新评估当前结果";
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ alert("请求出错: " + err);
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = "♻️ 重新评估当前结果";
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function startReevalPolling(q) {
|
|
|
+ if (reevalPollIntervals[q]) return;
|
|
|
+ const btn = document.getElementById("reevalBtn");
|
|
|
+
|
|
|
+ const poll = () => {
|
|
|
+ fetch(`/api/reeval_status?q=${encodeURIComponent(q)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ const isCurrent = DATA.queries[st.qi] && DATA.queries[st.qi].key === q;
|
|
|
+ if (d.status === "success") {
|
|
|
+ clearInterval(reevalPollIntervals[q]);
|
|
|
+ delete reevalPollIntervals[q];
|
|
|
+ if (isCurrent) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = '♻️ 重新评估当前结果';
|
|
|
+ }
|
|
|
+ loadData(true);
|
|
|
+ alert(`Query "${q}" 重新评估完成!已自动更新本页评分展示。`);
|
|
|
+ } else if (d.status === "failed") {
|
|
|
+ clearInterval(reevalPollIntervals[q]);
|
|
|
+ delete reevalPollIntervals[q];
|
|
|
+ if (isCurrent) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = '♻️ 重新评估当前结果';
|
|
|
+ }
|
|
|
+ alert(`Query "${q}" 重新评估失败: ${d.error}`);
|
|
|
+ } else if (d.status === "running") {
|
|
|
+ if (isCurrent) {
|
|
|
+ btn.disabled = true;
|
|
|
+ btn.textContent = `♻️ 重评中 ${q}...`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => console.error("Poll error:", err));
|
|
|
+ };
|
|
|
+
|
|
|
+ poll();
|
|
|
+ reevalPollIntervals[q] = setInterval(poll, 3000);
|
|
|
+ }
|
|
|
+
|
|
|
+ let batchProcPollInterval = null;
|
|
|
+
|
|
|
+ function batchExtractProcedures() {
|
|
|
+ if (st.qi === -1 || !DATA.queries[st.qi]) return;
|
|
|
+ const q = DATA.queries[st.qi].key;
|
|
|
+ const form = ['A', 'B', 'C'][st.fi];
|
|
|
+
|
|
|
+ const concurrencyInput = document.getElementById("batchConcurrency");
|
|
|
+ const concurrency = concurrencyInput ? parseInt(concurrencyInput.value) : 4;
|
|
|
+ const model = "google/gemini-3.1-flash-lite";
|
|
|
+
|
|
|
+ const btn = document.getElementById("batchProcBtn");
|
|
|
+ btn.disabled = true;
|
|
|
+ btn.textContent = "⚡ 提交任务...";
|
|
|
+
|
|
|
+ const execConsoleCard = document.getElementById("execConsoleCard");
|
|
|
+ if (execConsoleCard) {
|
|
|
+ execConsoleCard.style.display = "flex";
|
|
|
+ }
|
|
|
+ const consoleTitle = document.getElementById("consoleTitle");
|
|
|
+ if (consoleTitle) consoleTitle.textContent = `🚀 批量工序提取控制台 - ${q}`;
|
|
|
+ const consoleStatus = document.getElementById("consoleStatus");
|
|
|
+ if (consoleStatus) {
|
|
|
+ consoleStatus.textContent = "running";
|
|
|
+ consoleStatus.style.color = "var(--mint)";
|
|
|
+ }
|
|
|
+ const consoleOutput = document.getElementById("consoleOutput");
|
|
|
+ if (consoleOutput) consoleOutput.textContent = `[info] 正在为 Query: "${q}" (Form ${form}) 提交批量工序提取任务...\n`;
|
|
|
+
|
|
|
+ fetch("/api/batch_generate_procedure", {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
+ body: JSON.stringify({ q, form, concurrency, model })
|
|
|
+ })
|
|
|
+ .then(r => r.json().then(d => ({ ok: r.ok, d })))
|
|
|
+ .then(({ ok, d }) => {
|
|
|
+ if (!ok) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = "⚡ 一键提取工序";
|
|
|
+ if (consoleStatus) {
|
|
|
+ consoleStatus.textContent = "failed";
|
|
|
+ consoleStatus.style.color = "var(--rose)";
|
|
|
+ }
|
|
|
+ if (consoleOutput) consoleOutput.textContent += `[error] 提交失败: ${d.error || "未知错误"}\n`;
|
|
|
+ alert("提交批量提取任务失败: " + (d.error || "未知错误"));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (consoleOutput) consoleOutput.textContent += `[info] 任务启动成功,后台 PID: ${d.pid || "N/A"}\n[info] 正在获取日志...\n`;
|
|
|
+ startBatchProcPolling(q);
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = "⚡ 一键提取工序";
|
|
|
+ if (consoleStatus) {
|
|
|
+ consoleStatus.textContent = "failed";
|
|
|
+ consoleStatus.style.color = "var(--rose)";
|
|
|
+ }
|
|
|
+ if (consoleOutput) consoleOutput.textContent += `[error] 网络错误: ${err}\n`;
|
|
|
+ alert("网络错误: " + err);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function startBatchProcPolling(q) {
|
|
|
+ if (batchProcPollInterval) {
|
|
|
+ clearInterval(batchProcPollInterval);
|
|
|
+ }
|
|
|
+
|
|
|
+ const btn = document.getElementById("batchProcBtn");
|
|
|
+ const consoleStatus = document.getElementById("consoleStatus");
|
|
|
+ const consoleOutput = document.getElementById("consoleOutput");
|
|
|
+
|
|
|
+ const poll = () => {
|
|
|
+ fetch(`/api/batch_generate_status?q=${encodeURIComponent(q)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ const isCurrent = DATA.queries[st.qi] && DATA.queries[st.qi].key === q;
|
|
|
+
|
|
|
+ if (d.status === "success") {
|
|
|
+ clearInterval(batchProcPollInterval);
|
|
|
+ batchProcPollInterval = null;
|
|
|
+ if (isCurrent) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = "⚡ 一键提取工序";
|
|
|
+ if (consoleStatus) {
|
|
|
+ consoleStatus.textContent = "success";
|
|
|
+ consoleStatus.style.color = "var(--mint)";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fetch(`/api/batch_generate_log?q=${encodeURIComponent(q)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(ld => {
|
|
|
+ if (isCurrent && ld.log && consoleOutput) {
|
|
|
+ consoleOutput.textContent = ld.log + "\n🎉 批量工序提取已全部成功完成,已自动生成并编译 workflow json 和 HTML 文件!\n";
|
|
|
+ consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ loadData(true);
|
|
|
+ } else if (d.status === "failed") {
|
|
|
+ clearInterval(batchProcPollInterval);
|
|
|
+ batchProcPollInterval = null;
|
|
|
+ if (isCurrent) {
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = "⚡ 一键提取工序";
|
|
|
+ if (consoleStatus) {
|
|
|
+ consoleStatus.textContent = "failed";
|
|
|
+ consoleStatus.style.color = "var(--rose)";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fetch(`/api/batch_generate_log?q=${encodeURIComponent(q)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(ld => {
|
|
|
+ if (isCurrent && ld.log && consoleOutput) {
|
|
|
+ consoleOutput.textContent = ld.log + `\n❌ 任务执行失败: ${d.error || "未知原因"}\n`;
|
|
|
+ consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else if (d.status === "running") {
|
|
|
+ if (isCurrent) {
|
|
|
+ btn.disabled = true;
|
|
|
+ btn.textContent = "⚡ 批量提取中...";
|
|
|
+ if (consoleStatus) {
|
|
|
+ consoleStatus.textContent = "running";
|
|
|
+ consoleStatus.style.color = "var(--mint)";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fetch(`/api/batch_generate_log?q=${encodeURIComponent(q)}`)
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(ld => {
|
|
|
+ if (isCurrent && ld.log && consoleOutput) {
|
|
|
+ consoleOutput.textContent = ld.log;
|
|
|
+ consoleOutput.scrollTop = consoleOutput.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => console.error("Batch poll error:", err));
|
|
|
+ };
|
|
|
+
|
|
|
+ poll();
|
|
|
+ batchProcPollInterval = setInterval(poll, 2000);
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadData(forceReload) {
|
|
|
+ document.getElementById("lede").textContent = "正在加载数据...";
|
|
|
+ fetch("/api/data")
|
|
|
+ .then(r => r.json())
|
|
|
+ .then(d => {
|
|
|
+ DATA = d;
|
|
|
+ document.getElementById("lede").textContent = `runs_new/ 下共找到 ${d.queries.length} 个检索到的 query。`;
|
|
|
+
|
|
|
+ updateThresholdLimits();
|
|
|
+
|
|
|
+ if (DATA.queries.length > 0) {
|
|
|
+ if (st.qi === -1 || st.qi >= DATA.queries.length) {
|
|
|
+ st.qi = 0;
|
|
|
+ }
|
|
|
+ st.fi = 0;
|
|
|
+ document.getElementById("resultsArea").style.display = "block";
|
|
|
+ } else {
|
|
|
+ st.qi = -1;
|
|
|
+ document.getElementById("resultsArea").style.display = "none";
|
|
|
+ }
|
|
|
+
|
|
|
+ renderSidebar();
|
|
|
+ renderGrid();
|
|
|
+ renderHead();
|
|
|
+
|
|
|
+ if (d.active_reevals) {
|
|
|
+ Object.keys(d.active_reevals).forEach(q => {
|
|
|
+ if (d.active_reevals[q] === "running") {
|
|
|
+ startReevalPolling(q);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (d.active_batch_tasks) {
|
|
|
+ Object.keys(d.active_batch_tasks).forEach(q => {
|
|
|
+ if (d.active_batch_tasks[q] === "running") {
|
|
|
+ startBatchProcPolling(q);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ document.getElementById("lede").textContent = "读取数据失败:" + err;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // App Initialization
|
|
|
+ (function init() {
|
|
|
+ // Build query builder dimension groups
|
|
|
+ const root = document.getElementById('dimensions');
|
|
|
+ DIMS.forEach(dim => {
|
|
|
+ const grp = document.createElement('div');
|
|
|
+ grp.className = 'dim-group';
|
|
|
+ grp.dataset.id = dim.id;
|
|
|
+ root.appendChild(grp);
|
|
|
+ renderDim(dim.id);
|
|
|
+ });
|
|
|
+ updateQueryPreview();
|
|
|
+
|
|
|
+ // Load scanned queries list
|
|
|
+ loadData(false);
|
|
|
+ })();
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+
|
|
|
+</html>
|