|
|
<!DOCTYPE html>
|
|
|
<html lang="id">
|
|
|
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>RCFA RLHF Comparison Desk</title>
|
|
|
<link
|
|
|
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
|
rel="stylesheet">
|
|
|
<style>
|
|
|
:root {
|
|
|
--bg: #0b1120;
|
|
|
--card: #141e33;
|
|
|
--card2: #1a2640;
|
|
|
--border: #2a3a5c;
|
|
|
--primary: #4f8ef7;
|
|
|
--accent: #22d3ee;
|
|
|
--purple: #7c6ef7;
|
|
|
--text: #e2e8f0;
|
|
|
--muted: #7a8faf;
|
|
|
--high: #ef4444;
|
|
|
--high-bg: rgba(239, 68, 68, .13);
|
|
|
--medium: #f59e0b;
|
|
|
--medium-bg: rgba(245, 158, 11, .13);
|
|
|
--low: #10b981;
|
|
|
--low-bg: rgba(16, 185, 129, .13);
|
|
|
}
|
|
|
|
|
|
* {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
box-sizing: border-box
|
|
|
}
|
|
|
|
|
|
body {
|
|
|
font-family: 'Outfit', sans-serif;
|
|
|
background: var(--bg);
|
|
|
color: var(--text);
|
|
|
min-height: 100vh;
|
|
|
font-size: 15px;
|
|
|
padding-top: 40px;
|
|
|
}
|
|
|
|
|
|
code {
|
|
|
background: rgba(0, 0, 0, .3);
|
|
|
padding: .1rem .4rem;
|
|
|
border-radius: 4px;
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
font-size: .82em
|
|
|
}
|
|
|
|
|
|
button {
|
|
|
cursor: pointer;
|
|
|
font-family: inherit;
|
|
|
}
|
|
|
|
|
|
/* PROGRESS BAR */
|
|
|
#prog-bar {
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
right: 0;
|
|
|
background: var(--card2);
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
padding: 0.4rem 2rem;
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
z-index: 999;
|
|
|
font-size: 0.8rem;
|
|
|
height: 40px;
|
|
|
}
|
|
|
|
|
|
.pb-stats {
|
|
|
display: flex;
|
|
|
gap: 1.5rem;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
.pb-fill {
|
|
|
width: 200px;
|
|
|
height: 6px;
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
border-radius: 3px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.pb-inner {
|
|
|
height: 100%;
|
|
|
background: var(--low);
|
|
|
width: 0%;
|
|
|
transition: width 0.3s;
|
|
|
}
|
|
|
|
|
|
.btn-small {
|
|
|
background: none;
|
|
|
border: 1px solid var(--border);
|
|
|
color: var(--text);
|
|
|
padding: 0.2rem 0.6rem;
|
|
|
border-radius: 4px;
|
|
|
font-size: 0.75rem;
|
|
|
}
|
|
|
|
|
|
.btn-small:hover {
|
|
|
border-color: var(--primary);
|
|
|
color: var(--primary);
|
|
|
}
|
|
|
|
|
|
/* HEADER */
|
|
|
header {
|
|
|
background: rgba(15, 22, 43, .92);
|
|
|
backdrop-filter: blur(14px);
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
padding: 1rem 2rem;
|
|
|
position: sticky;
|
|
|
top: 40px;
|
|
|
z-index: 200
|
|
|
}
|
|
|
|
|
|
.hi {
|
|
|
max-width: 1400px;
|
|
|
margin: 0 auto;
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
gap: 1rem;
|
|
|
flex-wrap: wrap
|
|
|
}
|
|
|
|
|
|
h1 {
|
|
|
font-size: 1.3rem;
|
|
|
font-weight: 700;
|
|
|
background: linear-gradient(90deg, #fff 20%, var(--accent));
|
|
|
-webkit-background-clip: text;
|
|
|
-webkit-text-fill-color: transparent
|
|
|
}
|
|
|
|
|
|
.hr {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: .6rem;
|
|
|
flex-wrap: wrap
|
|
|
}
|
|
|
|
|
|
.pill {
|
|
|
padding: .28rem .85rem;
|
|
|
background: rgba(255, 255, 255, .05);
|
|
|
border: 1px solid var(--border);
|
|
|
border-radius: 20px;
|
|
|
font-size: .78rem
|
|
|
}
|
|
|
|
|
|
/* MODAL */
|
|
|
#modal-overlay {
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
right: 0;
|
|
|
bottom: 0;
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
z-index: 1000;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
.modal {
|
|
|
background: var(--card);
|
|
|
padding: 2rem;
|
|
|
border-radius: 12px;
|
|
|
border: 1px solid var(--border);
|
|
|
max-width: 400px;
|
|
|
width: 100%;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.modal input {
|
|
|
width: 100%;
|
|
|
padding: 0.8rem;
|
|
|
margin: 1rem 0;
|
|
|
background: var(--bg);
|
|
|
border: 1px solid var(--border);
|
|
|
color: white;
|
|
|
border-radius: 6px;
|
|
|
}
|
|
|
|
|
|
.modal button {
|
|
|
background: var(--primary);
|
|
|
color: white;
|
|
|
border: none;
|
|
|
padding: 0.8rem 1.5rem;
|
|
|
border-radius: 6px;
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
|
|
|
.hidden {
|
|
|
display: none !important;
|
|
|
}
|
|
|
|
|
|
/* UPLOAD */
|
|
|
#up {
|
|
|
max-width: 760px;
|
|
|
margin: 4rem auto;
|
|
|
padding: 0 2rem
|
|
|
}
|
|
|
|
|
|
.up-title {
|
|
|
font-size: 1rem;
|
|
|
font-weight: 600;
|
|
|
color: var(--accent);
|
|
|
margin-bottom: 1.2rem
|
|
|
}
|
|
|
|
|
|
.up-slots {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: .8rem;
|
|
|
margin-bottom: 1rem
|
|
|
}
|
|
|
|
|
|
.up-slot {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 1rem;
|
|
|
background: var(--card);
|
|
|
border: 1px solid var(--border);
|
|
|
border-radius: 12px;
|
|
|
padding: .9rem 1.3rem
|
|
|
}
|
|
|
|
|
|
.sl-label {
|
|
|
font-size: .8rem;
|
|
|
color: var(--muted);
|
|
|
width: 75px;
|
|
|
flex-shrink: 0;
|
|
|
font-weight: 600
|
|
|
}
|
|
|
|
|
|
.sl-name {
|
|
|
flex: 1;
|
|
|
font-size: .82rem;
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
color: var(--muted);
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis
|
|
|
}
|
|
|
|
|
|
.sl-pick {
|
|
|
padding: .35rem .9rem;
|
|
|
background: rgba(79, 142, 247, .1);
|
|
|
border: 1px solid var(--primary);
|
|
|
border-radius: 7px;
|
|
|
color: var(--primary);
|
|
|
font-size: .8rem;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.sl-rm {
|
|
|
background: none;
|
|
|
border: none;
|
|
|
color: var(--muted);
|
|
|
cursor: pointer;
|
|
|
font-size: 1rem;
|
|
|
padding: .1rem .35rem
|
|
|
}
|
|
|
|
|
|
.sl-rm:hover {
|
|
|
color: var(--high)
|
|
|
}
|
|
|
|
|
|
.add-btn {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: .4rem;
|
|
|
padding: .6rem 1.1rem;
|
|
|
background: rgba(34, 211, 238, .04);
|
|
|
border: 1px dashed var(--accent);
|
|
|
border-radius: 9px;
|
|
|
color: var(--accent);
|
|
|
cursor: pointer;
|
|
|
font-size: .84rem;
|
|
|
font-family: inherit;
|
|
|
transition: all .2s;
|
|
|
width: fit-content;
|
|
|
margin-bottom: 1.4rem
|
|
|
}
|
|
|
|
|
|
.add-btn:hover {
|
|
|
background: rgba(34, 211, 238, .1)
|
|
|
}
|
|
|
|
|
|
.go-btn {
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
padding: .85rem;
|
|
|
background: var(--primary);
|
|
|
border: none;
|
|
|
border-radius: 12px;
|
|
|
color: #fff;
|
|
|
font-size: 1rem;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.go-btn:disabled {
|
|
|
opacity: .35;
|
|
|
cursor: default
|
|
|
}
|
|
|
|
|
|
/* CONTROLS */
|
|
|
.controls {
|
|
|
max-width: 1400px;
|
|
|
margin: 1.5rem auto 1rem;
|
|
|
padding: 0 2rem;
|
|
|
display: flex;
|
|
|
gap: .7rem;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center
|
|
|
}
|
|
|
|
|
|
input[type=text],
|
|
|
select {
|
|
|
background: var(--card);
|
|
|
border: 1px solid var(--border);
|
|
|
border-radius: 9px;
|
|
|
color: var(--text);
|
|
|
padding: .6rem 1rem;
|
|
|
font-size: .88rem;
|
|
|
}
|
|
|
|
|
|
input[type=text] {
|
|
|
flex: 1;
|
|
|
min-width: 200px
|
|
|
}
|
|
|
|
|
|
.rmeta {
|
|
|
max-width: 1400px;
|
|
|
margin: 0 auto .5rem;
|
|
|
padding: 0 2rem;
|
|
|
font-size: .8rem;
|
|
|
color: var(--muted)
|
|
|
}
|
|
|
|
|
|
.export-pref-btn {
|
|
|
background: rgba(16, 185, 129, .1);
|
|
|
border: 1px solid var(--low);
|
|
|
border-radius: 9px;
|
|
|
color: var(--low);
|
|
|
padding: .6rem 1.1rem;
|
|
|
font-size: .84rem;
|
|
|
margin-left: auto;
|
|
|
}
|
|
|
|
|
|
/* CARDS */
|
|
|
#results {
|
|
|
max-width: 1400px;
|
|
|
margin: 0 auto 5rem;
|
|
|
padding: 0 2rem;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 2rem
|
|
|
}
|
|
|
|
|
|
.card {
|
|
|
background: var(--card);
|
|
|
border: 1px solid var(--border);
|
|
|
border-radius: 18px;
|
|
|
overflow: hidden;
|
|
|
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s;
|
|
|
scroll-margin-top: 130px;
|
|
|
}
|
|
|
.card:hover {
|
|
|
transform: translateY(-2px);
|
|
|
box-shadow: 0 12px 24px rgba(0,0,0,0.4);
|
|
|
border-color: rgba(79, 142, 247, 0.3);
|
|
|
}
|
|
|
|
|
|
.card-top {
|
|
|
padding: 1.2rem 1.6rem;
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
background: var(--card2)
|
|
|
}
|
|
|
|
|
|
.card-top-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: .8rem;
|
|
|
margin-bottom: .9rem;
|
|
|
flex-wrap: wrap
|
|
|
}
|
|
|
|
|
|
.case-n {
|
|
|
font-size: .72rem;
|
|
|
color: var(--muted)
|
|
|
}
|
|
|
|
|
|
.tag {
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
font-size: 1rem;
|
|
|
font-weight: 700;
|
|
|
color: var(--accent)
|
|
|
}
|
|
|
|
|
|
/* RISK EDITOR */
|
|
|
.risk-edit-grp {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 0.5rem;
|
|
|
background: rgba(0, 0, 0, 0.2);
|
|
|
padding: 0.2rem 0.5rem;
|
|
|
border-radius: 6px;
|
|
|
border: 1px solid var(--border);
|
|
|
}
|
|
|
|
|
|
.risk-sel {
|
|
|
background: transparent;
|
|
|
border: none;
|
|
|
color: inherit;
|
|
|
font-weight: bold;
|
|
|
outline: none;
|
|
|
font-size: 0.75rem;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.risk-reason-inp {
|
|
|
background: transparent;
|
|
|
border: none;
|
|
|
border-left: 1px solid var(--border);
|
|
|
padding-left: 0.5rem;
|
|
|
color: var(--text);
|
|
|
font-size: 0.75rem;
|
|
|
outline: none;
|
|
|
width: 200px;
|
|
|
}
|
|
|
|
|
|
.diff-badge {
|
|
|
font-size: 0.6rem;
|
|
|
border: 1px dashed var(--medium);
|
|
|
color: var(--medium);
|
|
|
padding: 0.1rem 0.3rem;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
|
|
|
/* FLAG & SKIP */
|
|
|
.card-actions {
|
|
|
margin-left: auto;
|
|
|
display: flex;
|
|
|
gap: 0.5rem;
|
|
|
}
|
|
|
|
|
|
.flag-btn {
|
|
|
background: rgba(239, 68, 68, .1);
|
|
|
border: 1px solid var(--high);
|
|
|
color: var(--high);
|
|
|
padding: 0.3rem 0.8rem;
|
|
|
border-radius: 6px;
|
|
|
font-size: 0.75rem;
|
|
|
}
|
|
|
|
|
|
.flag-btn.flagged {
|
|
|
background: var(--high);
|
|
|
color: white;
|
|
|
}
|
|
|
|
|
|
.flag-reason {
|
|
|
display: none;
|
|
|
background: var(--bg);
|
|
|
border: 1px solid var(--high);
|
|
|
color: var(--text);
|
|
|
padding: 0.3rem;
|
|
|
border-radius: 4px;
|
|
|
font-size: 0.7rem;
|
|
|
outline: none;
|
|
|
}
|
|
|
|
|
|
/* GROUND TRUTH BOX */
|
|
|
.gt-box {
|
|
|
background: rgba(124, 110, 247, 0.06);
|
|
|
padding: 1.2rem 1.6rem;
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
border-left: 4px solid var(--purple);
|
|
|
}
|
|
|
|
|
|
.gt-title {
|
|
|
font-size: 0.75rem;
|
|
|
text-transform: uppercase;
|
|
|
font-weight: 700;
|
|
|
color: var(--purple);
|
|
|
margin-bottom: 0.8rem;
|
|
|
letter-spacing: 0.5px;
|
|
|
}
|
|
|
|
|
|
.gt-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
gap: 0.8rem 1.5rem;
|
|
|
}
|
|
|
|
|
|
.fi-key {
|
|
|
font-size: .68rem;
|
|
|
font-weight: 600;
|
|
|
color: var(--muted);
|
|
|
margin-bottom: .12rem
|
|
|
}
|
|
|
|
|
|
.fi-val {
|
|
|
font-size: .82rem;
|
|
|
line-height: 1.5;
|
|
|
color: var(--text)
|
|
|
}
|
|
|
|
|
|
/* RESPONSES */
|
|
|
.card-responses {
|
|
|
display: grid
|
|
|
}
|
|
|
|
|
|
.resp-col {
|
|
|
padding: 1.3rem 1.6rem;
|
|
|
border-right: 1px solid var(--border);
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.resp-col:last-child {
|
|
|
border-right: none
|
|
|
}
|
|
|
|
|
|
.resp-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 1rem;
|
|
|
padding-bottom: .7rem;
|
|
|
border-bottom: 1px solid var(--border)
|
|
|
}
|
|
|
|
|
|
.resp-model {
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
font-size: .8rem;
|
|
|
font-weight: 700;
|
|
|
color: var(--accent)
|
|
|
}
|
|
|
|
|
|
.resp-body {
|
|
|
font-size: .84rem;
|
|
|
line-height: 1.7;
|
|
|
flex: 1;
|
|
|
}
|
|
|
|
|
|
.resp-body p {
|
|
|
margin: .35rem 0
|
|
|
}
|
|
|
|
|
|
.resp-body ul {
|
|
|
padding-left: 1.3rem;
|
|
|
margin: .3rem 0
|
|
|
}
|
|
|
|
|
|
.resp-body strong {
|
|
|
font-weight: 600;
|
|
|
color: var(--text)
|
|
|
}
|
|
|
|
|
|
.resp-body code {
|
|
|
background: rgba(0, 0, 0, .3);
|
|
|
padding: .1rem .3rem;
|
|
|
border-radius: 4px;
|
|
|
color: var(--accent)
|
|
|
}
|
|
|
|
|
|
/* DIFF VIEW */
|
|
|
.diff-removed {
|
|
|
background-color: rgba(239, 68, 68, 0.25);
|
|
|
text-decoration: line-through;
|
|
|
opacity: 0.7;
|
|
|
}
|
|
|
|
|
|
.diff-toggle {
|
|
|
font-size: 0.7rem;
|
|
|
color: var(--primary);
|
|
|
cursor: pointer;
|
|
|
background: none;
|
|
|
border: none;
|
|
|
text-decoration: underline;
|
|
|
}
|
|
|
|
|
|
/* SOP & TAGS */
|
|
|
.sop-dots {
|
|
|
display: flex;
|
|
|
gap: .3rem;
|
|
|
align-items: center
|
|
|
}
|
|
|
|
|
|
.sop-dot {
|
|
|
width: 8px;
|
|
|
height: 8px;
|
|
|
border-radius: 50%;
|
|
|
}
|
|
|
|
|
|
.sop-dot.ok {
|
|
|
background: var(--low);
|
|
|
}
|
|
|
|
|
|
.sop-dot.miss {
|
|
|
background: var(--high);
|
|
|
}
|
|
|
|
|
|
.tag-error {
|
|
|
border: 1px solid var(--high) !important;
|
|
|
box-shadow: inset 0 0 10px rgba(239, 68, 68, .2);
|
|
|
}
|
|
|
|
|
|
/* RLHF FORM */
|
|
|
.rlhf-form {
|
|
|
margin-top: 1rem;
|
|
|
padding-top: 1rem;
|
|
|
border-top: 1px dashed var(--border);
|
|
|
}
|
|
|
|
|
|
.stars {
|
|
|
display: flex;
|
|
|
flex-direction: row-reverse;
|
|
|
justify-content: flex-end;
|
|
|
gap: 0.2rem;
|
|
|
margin-bottom: 0.5rem;
|
|
|
}
|
|
|
|
|
|
.stars input {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.stars label {
|
|
|
font-size: 1.4rem;
|
|
|
color: rgba(255, 255, 255, 0.15);
|
|
|
cursor: pointer;
|
|
|
transition: color 0.2s, transform 0.1s;
|
|
|
}
|
|
|
|
|
|
.stars label:active {
|
|
|
transform: scale(0.75);
|
|
|
}
|
|
|
|
|
|
.stars label:hover,
|
|
|
.stars label:hover~label,
|
|
|
.stars input:checked~label {
|
|
|
color: #fbbf24;
|
|
|
text-shadow: 0 0 10px rgba(251, 191, 36, 0.4);
|
|
|
}
|
|
|
|
|
|
.dims {
|
|
|
display: flex;
|
|
|
gap: 0.4rem;
|
|
|
margin-bottom: 0.5rem;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.dim-btn {
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
border: 1px solid var(--border);
|
|
|
color: var(--muted);
|
|
|
font-size: 0.65rem;
|
|
|
padding: 0.2rem 0.5rem;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
|
|
|
.dim-btn.active {
|
|
|
background: rgba(79, 142, 247, 0.15);
|
|
|
border-color: var(--primary);
|
|
|
color: var(--primary);
|
|
|
}
|
|
|
|
|
|
.qc-group {
|
|
|
display: flex;
|
|
|
gap: 0.3rem;
|
|
|
margin-bottom: 0.3rem;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.qc-btn {
|
|
|
background: none;
|
|
|
border: 1px solid var(--border);
|
|
|
color: var(--muted);
|
|
|
font-size: 0.65rem;
|
|
|
padding: 0.1rem 0.4rem;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
|
|
|
.qc-btn:hover {
|
|
|
border-color: var(--text);
|
|
|
color: var(--text);
|
|
|
}
|
|
|
|
|
|
.rv-comment {
|
|
|
width: 100%;
|
|
|
height: 50px;
|
|
|
background: rgba(0, 0, 0, 0.3);
|
|
|
border: 1px solid var(--border);
|
|
|
color: var(--text);
|
|
|
padding: 0.4rem;
|
|
|
font-size: 0.75rem;
|
|
|
border-radius: 4px;
|
|
|
resize: vertical;
|
|
|
font-family: inherit;
|
|
|
}
|
|
|
|
|
|
.best-btn {
|
|
|
width: 100%;
|
|
|
margin-top: .7rem;
|
|
|
padding: .5rem;
|
|
|
background: rgba(255, 255, 255, .03);
|
|
|
border: 1px dashed rgba(255, 215, 0, .25);
|
|
|
border-radius: 9px;
|
|
|
color: rgba(255, 215, 0, .55);
|
|
|
font-size: .78rem;
|
|
|
transition: all .25s
|
|
|
}
|
|
|
|
|
|
.best-btn:hover {
|
|
|
background: rgba(255, 215, 0, .07);
|
|
|
border-color: rgba(255, 215, 0, .5);
|
|
|
color: #ffd700
|
|
|
}
|
|
|
|
|
|
.best-btn.chosen {
|
|
|
background: rgba(255, 215, 0, .15);
|
|
|
border-color: #ffd700;
|
|
|
color: #ffd700;
|
|
|
font-weight: 700
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div id="modal-overlay">
|
|
|
<div class="modal">
|
|
|
<h2>Welcome to RCFA Validation</h2>
|
|
|
<p style="margin-top:0.5rem;font-size:0.9rem;color:var(--muted)">Enter your Reviewer ID to begin.</p>
|
|
|
<input type="text" id="reviewer-name" placeholder="E.g., Eng-Daniel">
|
|
|
<button onclick="startSession()">Start Reviewing</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div id="prog-bar" class="hidden">
|
|
|
<div class="pb-stats">
|
|
|
<span>👤 <b id="pb-user"></b></span>
|
|
|
<span>✅ Reviewed: <b id="pb-rev">0</b> / <span id="pb-tot">0</span></span>
|
|
|
<span style="color:#f87171">🚩 Flagged: <b id="pb-flg">0</b></span>
|
|
|
<div class="pb-fill">
|
|
|
<div class="pb-inner" id="pb-bar"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div>
|
|
|
<button class="btn-small" onclick="jumpNext()">⏭ Jump to Next Unreviewed</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<header>
|
|
|
<div class="hi">
|
|
|
<h1>⚡ RCFA Data Validation Desk</h1>
|
|
|
<div class="hr">
|
|
|
<div id="hpills"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<div id="up">
|
|
|
<div class="up-title">Select Datasets to Evaluate</div>
|
|
|
<p style="font-size:0.85rem; color:var(--muted); margin-bottom:1rem;">The datasets below will be automatically downloaded from the repository. Select the ones you wish to compare.</p>
|
|
|
<div class="up-slots" id="slots">
|
|
|
<label class="sl-pick" style="display:flex; gap:0.5rem; align-items:center; cursor:pointer;">
|
|
|
<input type="checkbox" value="../benchmark/100_queries/results_gemma4-e2b_100.jsonl" checked>
|
|
|
Gemma4 2B (results_gemma4-e2b_100.jsonl)
|
|
|
</label>
|
|
|
<label class="sl-pick" style="display:flex; gap:0.5rem; align-items:center; cursor:pointer;">
|
|
|
<input type="checkbox" value="../benchmark/100_queries/results_gemma4-e4b_100.jsonl" checked>
|
|
|
Gemma4 4B (results_gemma4-e4b_100.jsonl)
|
|
|
</label>
|
|
|
<label class="sl-pick" style="display:flex; gap:0.5rem; align-items:center; cursor:pointer;">
|
|
|
<input type="checkbox" value="../benchmark/100_queries/results_qwen3-4b_100.jsonl">
|
|
|
Qwen3 4B (results_qwen3-4b_100.jsonl)
|
|
|
</label>
|
|
|
<label class="sl-pick" style="display:flex; gap:0.5rem; align-items:center; cursor:pointer;">
|
|
|
<input type="checkbox" value="../benchmark/100_queries/results_qwen3-8b_100.jsonl">
|
|
|
Qwen3 8B (results_qwen3-8b_100.jsonl)
|
|
|
</label>
|
|
|
</div>
|
|
|
<button class="go-btn" id="gobtn" onclick="fetchAndCompare()">Download & Start Validation →</button>
|
|
|
<div id="fetch-status" style="margin-top:1rem; color:var(--accent); font-size:0.85rem; text-align:center;"></div>
|
|
|
|
|
|
<div style="margin-top: 2.5rem; padding-top: 1.5rem; border-top: 1px dashed var(--border);">
|
|
|
<div class="up-title" style="font-size:0.95rem;">Fallback: Manual Data Loading</div>
|
|
|
|
|
|
<div style="background: rgba(34, 211, 238, 0.05); border: 1px dashed rgba(34, 211, 238, 0.3); padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem;">
|
|
|
<h4 style="color: var(--accent); font-size: 0.85rem; margin-bottom: 0.5rem;">💡 How to load data manually:</h4>
|
|
|
<p style="font-size:0.8rem; color:var(--text); line-height: 1.5; margin: 0;">
|
|
|
If the auto-download fails due to browser security restrictions (like CORS) when running locally, you must load the data files manually.
|
|
|
</p>
|
|
|
<ul style="font-size:0.8rem; color:var(--muted); line-height: 1.6; padding-left: 1.2rem; margin-top: 0.5rem;">
|
|
|
<li>Click the <b>Browse</b> buttons below for each model slot.</li>
|
|
|
<li>Navigate to the <code style="color: var(--primary);">benchmark/100_queries</code> folder in this project directory.</li>
|
|
|
<li>Select the corresponding inference result file (e.g., <code>results_gemma4-e2b_100.jsonl</code>).</li>
|
|
|
<li>Click <b>+ Add manual file</b> if you want to compare more than two models at once.</li>
|
|
|
<li>Once all files are selected, click <b>Load Local Files →</b> to begin your validation session.</li>
|
|
|
</ul>
|
|
|
</div>
|
|
|
|
|
|
<div class="up-slots" id="manual-slots">
|
|
|
<div class="up-slot" data-i="0">
|
|
|
<span class="sl-label">Model A</span>
|
|
|
<span class="sl-name" id="msn0">No file selected</span>
|
|
|
<label class="sl-pick">Browse <input type="file" accept=".jsonl" style="display:none" onchange="pickManual(0,this)"></label>
|
|
|
</div>
|
|
|
<div class="up-slot" data-i="1">
|
|
|
<span class="sl-label">Model B</span>
|
|
|
<span class="sl-name" id="msn1">No file selected</span>
|
|
|
<label class="sl-pick">Browse <input type="file" accept=".jsonl" style="display:none" onchange="pickManual(1,this)"></label>
|
|
|
</div>
|
|
|
</div>
|
|
|
<button class="add-btn" onclick="addManualSlot()">+ Add manual file</button>
|
|
|
<button class="go-btn" id="manualbtn" onclick="compareManual()" disabled style="background:var(--border); color:var(--text); margin-top:0.5rem;">Load Local Files →</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="controls hidden" id="ctrl">
|
|
|
<input type="text" id="search" placeholder="🔍 Search tags, symptoms...">
|
|
|
<select id="rflt">
|
|
|
<option value="all">All Risks</option>
|
|
|
<option value="High">🔴 High</option>
|
|
|
<option value="Medium">🟡 Medium</option>
|
|
|
<option value="Low">🟢 Low</option>
|
|
|
</select>
|
|
|
<select id="sflt">
|
|
|
<option value="all">All Status</option>
|
|
|
<option value="pending">⏳ Pending</option>
|
|
|
<option value="reviewed">✅ Reviewed</option>
|
|
|
<option value="flagged">🚩 Flagged</option>
|
|
|
</select>
|
|
|
<button class="export-pref-btn" onclick="exportData()">⬇ Export Validation Dataset</button>
|
|
|
</div>
|
|
|
<div class="rmeta hidden" id="rmeta"></div>
|
|
|
<div id="results"></div>
|
|
|
|
|
|
<script>
|
|
|
let files = [null, null];
|
|
|
let mSlots = 2;
|
|
|
let cases = []; // merged
|
|
|
let state = JSON.parse(localStorage.getItem('rcfa_state')) || {};
|
|
|
let reviewer = localStorage.getItem('rcfa_reviewer') || '';
|
|
|
let diffMode = {}; // caseIdx -> boolean
|
|
|
|
|
|
const SOP = [{ l: 'Gejala', r: /gejala/i }, { l: 'Verifikasi', r: /verifikasi/i }, { l: 'Akar Masalah', r: /akar\s*masalah/i }, { l: 'Dampak', r: /dampak/i }, { l: 'FDT', r: /rekomendasi|FDT/i }];
|
|
|
const TAG_RE = /\b3[A-Z]{2}-H\d{3}\b/g;
|
|
|
|
|
|
// INIT
|
|
|
if (reviewer) {
|
|
|
document.getElementById('modal-overlay').classList.add('hidden');
|
|
|
document.getElementById('pb-user').textContent = reviewer;
|
|
|
}
|
|
|
function startSession() {
|
|
|
const v = document.getElementById('reviewer-name').value.trim();
|
|
|
if (!v) return;
|
|
|
reviewer = v;
|
|
|
localStorage.setItem('rcfa_reviewer', reviewer);
|
|
|
document.getElementById('pb-user').textContent = reviewer;
|
|
|
document.getElementById('modal-overlay').classList.add('hidden');
|
|
|
}
|
|
|
|
|
|
async function fetchAndCompare() {
|
|
|
const btn = document.getElementById('gobtn');
|
|
|
const stat = document.getElementById('fetch-status');
|
|
|
const checks = Array.from(document.querySelectorAll('#slots input:checked'));
|
|
|
if (checks.length < 1) { alert('Please select at least one dataset to evaluate.'); return; }
|
|
|
|
|
|
btn.disabled = true;
|
|
|
stat.textContent = "Starting download...";
|
|
|
files = [];
|
|
|
|
|
|
try {
|
|
|
for(let i=0; i<checks.length; i++) {
|
|
|
const url = checks[i].value;
|
|
|
const name = url.split('/').pop();
|
|
|
stat.textContent = `Downloading ${name} (${i+1}/${checks.length})...`;
|
|
|
const res = await fetch(url);
|
|
|
if(!res.ok) throw new Error(`HTTP ${res.status} when downloading ${name}`);
|
|
|
const text = await res.text();
|
|
|
files.push({ name: name, rows: text.split('\n').filter(l => l.trim()).map(l => JSON.parse(l)) });
|
|
|
}
|
|
|
stat.textContent = "Merging datasets...";
|
|
|
compare();
|
|
|
} catch (err) {
|
|
|
alert("Failed to download datasets: " + err.message + "\n\nMake sure the files exist in the repository relative to this page.");
|
|
|
btn.disabled = false;
|
|
|
stat.textContent = "";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function addManualSlot() {
|
|
|
const i = mSlots++;
|
|
|
files.push(null);
|
|
|
const labels = 'ABCDEFG';
|
|
|
const d = document.createElement('div');
|
|
|
d.className = 'up-slot'; d.dataset.i = i;
|
|
|
d.innerHTML = `<span class="sl-label">Model ${labels[i] || i + 1}</span>
|
|
|
<span class="sl-name" id="msn${i}">No file selected</span>
|
|
|
<label class="sl-pick">Browse <input type="file" accept=".jsonl" style="display:none" onchange="pickManual(${i},this)"></label>
|
|
|
<button class="sl-rm" onclick="rmManualSlot(this,${i})" title="Remove">✕</button>`;
|
|
|
document.getElementById('manual-slots').appendChild(d);
|
|
|
updateManualBtn();
|
|
|
}
|
|
|
|
|
|
function rmManualSlot(btn, i) {
|
|
|
btn.parentElement.remove();
|
|
|
files[i] = null;
|
|
|
updateManualBtn();
|
|
|
}
|
|
|
|
|
|
function updateManualBtn() {
|
|
|
const btn = document.getElementById('manualbtn');
|
|
|
btn.disabled = files.filter(Boolean).length < 1;
|
|
|
if (files.filter(Boolean).length > 0) {
|
|
|
btn.style.background = 'var(--primary)';
|
|
|
btn.style.color = '#fff';
|
|
|
} else {
|
|
|
btn.style.background = 'var(--border)';
|
|
|
btn.style.color = 'var(--text)';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function pickManual(i, inp) {
|
|
|
const f = inp.files[0]; if (!f) return;
|
|
|
document.getElementById('msn' + i).textContent = f.name;
|
|
|
const reader = new FileReader();
|
|
|
reader.onload = ev => {
|
|
|
try {
|
|
|
files[i] = { name: f.name, rows: ev.target.result.split('\n').filter(l => l.trim()).map(l => JSON.parse(l)) };
|
|
|
updateManualBtn();
|
|
|
} catch (e) { alert('Parse error'); }
|
|
|
};
|
|
|
reader.readAsText(f);
|
|
|
}
|
|
|
|
|
|
function compareManual() {
|
|
|
compare();
|
|
|
}
|
|
|
|
|
|
function compare() {
|
|
|
const loaded = files.filter(Boolean);
|
|
|
if (loaded.length < 1) return;
|
|
|
const maxLen = Math.max(...loaded.map(m => m.rows.length));
|
|
|
cases = [];
|
|
|
for (let i = 0; i < maxLen; i++) {
|
|
|
const firstRow = loaded.find(m => m.rows[i])?.rows[i];
|
|
|
if (!firstRow) continue;
|
|
|
cases.push({ tag: firstRow.tag, risk: firstRow.risk, prompt: firstRow.input_prompt, rows: loaded.map(m => m.rows[i] || null) });
|
|
|
if (!state[i]) state[i] = { status: 'pending', origRisk: firstRow.risk, risk: firstRow.risk, riskReason: '', flag: '', best: null, models: {} };
|
|
|
}
|
|
|
document.getElementById('up').classList.add('hidden');
|
|
|
document.getElementById('prog-bar').classList.remove('hidden');
|
|
|
document.getElementById('ctrl').classList.remove('hidden');
|
|
|
document.getElementById('rmeta').classList.remove('hidden');
|
|
|
['search', 'rflt', 'sflt'].forEach(id => document.getElementById(id).addEventListener('input', render));
|
|
|
updateProgress();
|
|
|
render();
|
|
|
}
|
|
|
|
|
|
function updateProgress() {
|
|
|
if (!cases.length) return;
|
|
|
let rev = 0, flg = 0;
|
|
|
Object.values(state).forEach(s => {
|
|
|
if (s.status === 'reviewed') rev++;
|
|
|
if (s.status === 'flagged') flg++;
|
|
|
});
|
|
|
document.getElementById('pb-rev').textContent = rev;
|
|
|
document.getElementById('pb-tot').textContent = cases.length;
|
|
|
document.getElementById('pb-flg').textContent = flg;
|
|
|
document.getElementById('pb-bar').style.width = `${(rev / cases.length) * 100}%`;
|
|
|
localStorage.setItem('rcfa_state', JSON.stringify(state));
|
|
|
}
|
|
|
|
|
|
function jumpNext() {
|
|
|
for (let i = 0; i < cases.length; i++) {
|
|
|
if (state[i].status === 'pending') {
|
|
|
document.getElementById(`case-${i}`).scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// STATE UPDATERS (Now using updateCaseDOM for lag-free rendering)
|
|
|
function setRisk(ci, val) { state[ci].risk = val; checkStatus(ci); updateCaseDOM(ci); }
|
|
|
function setRiskReason(ci, val) { state[ci].riskReason = val; checkStatus(ci); }
|
|
|
function toggleFlag(ci) {
|
|
|
state[ci].status = state[ci].status === 'flagged' ? 'pending' : 'flagged';
|
|
|
if (state[ci].status !== 'flagged') state[ci].flag = '';
|
|
|
checkStatus(ci); updateCaseDOM(ci);
|
|
|
}
|
|
|
function setFlagReason(ci, val) { state[ci].flag = val; checkStatus(ci); }
|
|
|
function setRating(ci, model, val) {
|
|
|
if (!state[ci].models[model]) state[ci].models[model] = { rating: 0, comment: '', dims: {} };
|
|
|
if (state[ci].models[model].rating === val) {
|
|
|
state[ci].models[model].rating = 0; // Unclick
|
|
|
} else {
|
|
|
state[ci].models[model].rating = val;
|
|
|
}
|
|
|
checkStatus(ci); updateCaseDOM(ci);
|
|
|
}
|
|
|
function toggleDim(ci, model, dim) {
|
|
|
if (!state[ci].models[model]) state[ci].models[model] = { rating: 0, comment: '', dims: {} };
|
|
|
state[ci].models[model].dims[dim] = !state[ci].models[model].dims[dim];
|
|
|
updateCaseDOM(ci);
|
|
|
}
|
|
|
function setComment(ci, model, val) {
|
|
|
if (!state[ci].models[model]) state[ci].models[model] = { rating: 0, comment: '', dims: {} };
|
|
|
state[ci].models[model].comment = val; checkStatus(ci);
|
|
|
}
|
|
|
function addQc(ci, model, txt) {
|
|
|
if (!state[ci].models[model]) state[ci].models[model] = { rating: 0, comment: '', dims: {} };
|
|
|
state[ci].models[model].comment += (state[ci].models[model].comment ? '; ' : '') + txt;
|
|
|
updateCaseDOM(ci);
|
|
|
}
|
|
|
function toggleBest(ci, model) {
|
|
|
state[ci].best = state[ci].best === model ? null : model;
|
|
|
checkStatus(ci); updateCaseDOM(ci);
|
|
|
}
|
|
|
function checkStatus(ci) {
|
|
|
if (state[ci].status === 'flagged') return updateProgress();
|
|
|
const s = state[ci];
|
|
|
let hasRating = false;
|
|
|
Object.values(s.models).forEach(m => { if (m.rating > 0) hasRating = true; });
|
|
|
if (s.best || hasRating || s.risk !== s.origRisk) s.status = 'reviewed';
|
|
|
else s.status = 'pending';
|
|
|
updateProgress();
|
|
|
}
|
|
|
function toggleDiff(ci) { diffMode[ci] = !diffMode[ci]; updateCaseDOM(ci); }
|
|
|
|
|
|
function getDiff(txtA, txtB) {
|
|
|
const sA = txtA.split(/(?<=[.!?])\s+/); const sB = txtB.split(/(?<=[.!?])\s+/);
|
|
|
return sA.map(s => sB.some(b => b.includes(s)) ? s : `<span class="diff-removed">${s}</span>`).join(' ');
|
|
|
}
|
|
|
|
|
|
// Parses the Ground Truth from the system prompt
|
|
|
function parseFmea(p) {
|
|
|
const ind = (p.match(/Indikasi:\s*([\s\S]+?)(?=\nVerifikasi:|$)/) || [])[1]?.trim() || '';
|
|
|
const ver = (p.match(/Verifikasi:\s*([\s\S]+?)(?=\nPenyebab:|$)/) || [])[1]?.trim() || '';
|
|
|
const cau = (p.match(/Penyebab:\s*([\s\S]+?)(?=\nEfek:|$)/) || [])[1]?.trim() || '';
|
|
|
const eff = (p.match(/Efek:\s*([\s\S]+?)(?=\nRekomendasi FDT:|$)/) || [])[1]?.trim() || '';
|
|
|
const rec = (p.match(/Rekomendasi FDT:\s*([\s\S]+?)(?=\n\nPERTANYAAN:|$)/) || [])[1]?.trim() || '';
|
|
|
return { ind, ver, cau, eff, rec };
|
|
|
}
|
|
|
|
|
|
function esc(s) { return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') }
|
|
|
function md(t) { return String(t || '').replace(/\*\*(.+?)\*\*/g, '<b>$1</b>').replace(/\n/g, '<br>'); }
|
|
|
|
|
|
function getCaseHtml(ci) {
|
|
|
const c = cases[ci];
|
|
|
const st = state[ci];
|
|
|
const fm = parseFmea(c.prompt);
|
|
|
|
|
|
const isExactlyTwo = c.rows.length === 2 && c.rows[0] && c.rows[1];
|
|
|
|
|
|
const cols = c.rows.map((row, idx) => {
|
|
|
if (!row) return `<div class="resp-col"><p style="color:var(--muted);font-style:italic;padding:1rem">No Data</p></div>`;
|
|
|
const mName = row.model;
|
|
|
let rHtml = row.generated_response || '';
|
|
|
|
|
|
if (diffMode[ci] && isExactlyTwo) {
|
|
|
const otherHtml = c.rows[idx === 0 ? 1 : 0].generated_response || '';
|
|
|
rHtml = getDiff(rHtml, otherHtml);
|
|
|
}
|
|
|
|
|
|
const mSt = st.models[mName] || { rating: 0, comment: '', dims: {} };
|
|
|
const tagErr = validateTag(c.tag, row.generated_response).length > 0;
|
|
|
const sopOk = checkSop(row.generated_response);
|
|
|
|
|
|
const stars = [5, 4, 3, 2, 1].map(v => `<input type="radio" id="s_${ci}_${idx}_${v}" name="s_${ci}_${idx}" value="${v}" ${mSt.rating == v ? 'checked' : ''} onclick="setRating(${ci},'${mName}',${v})"><label for="s_${ci}_${idx}_${v}">★</label>`).join('');
|
|
|
|
|
|
return `<div class="resp-col ${tagErr ? 'tag-error' : ''}">
|
|
|
<div class="resp-header">
|
|
|
<span class="resp-model">Model ${String.fromCharCode(65 + idx)}</span>
|
|
|
<div class="sop-dots">${sopOk.map(s => `<span class="sop-dot ${s ? 'ok' : 'miss'}"></span>`).join('')}</div>
|
|
|
</div>
|
|
|
<div class="resp-body">${md(rHtml)}</div>
|
|
|
|
|
|
<div class="rlhf-form">
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center;">
|
|
|
<span style="font-size:0.75rem;color:var(--muted)">Rate this response:</span>
|
|
|
<div class="stars">${stars}</div>
|
|
|
</div>
|
|
|
<div class="dims">
|
|
|
${['Accuracy', 'Format', 'Detail'].map(d => `<button class="dim-btn ${mSt.dims[d] ? 'active' : ''}" onclick="toggleDim(${ci},'${mName}','${d}')">${d}</button>`).join('')}
|
|
|
</div>
|
|
|
<div class="qc-group">
|
|
|
<button class="qc-btn" onclick="addQc(${ci},'${mName}','Missing RCA')">+ No RCA</button>
|
|
|
<button class="qc-btn" onclick="addQc(${ci},'${mName}','Tag Mismatch')">+ Tag Err</button>
|
|
|
<button class="qc-btn" onclick="addQc(${ci},'${mName}','Perfect')">+ Perfect</button>
|
|
|
</div>
|
|
|
<textarea class="rv-comment" placeholder="Reviewer comments..." oninput="setComment(${ci},'${mName}',this.value)">${esc(mSt.comment)}</textarea>
|
|
|
<button class="best-btn ${st.best === mName ? 'chosen' : ''}" onclick="toggleBest(${ci},'${mName}')">${st.best === mName ? '⭐ Selected as Best' : 'Mark as Best'}</button>
|
|
|
</div>
|
|
|
</div>`;
|
|
|
}).join('');
|
|
|
|
|
|
return `
|
|
|
<div class="card-top">
|
|
|
<div class="card-top-row">
|
|
|
<span class="case-n">Case #${ci + 1}</span>
|
|
|
<span class="tag">${esc(c.tag)}</span>
|
|
|
|
|
|
<div class="risk-edit-grp" style="border-color:var(--${st.risk.toLowerCase()}); background:var(--${st.risk.toLowerCase()}-bg)">
|
|
|
<select class="risk-sel" style="color:var(--${st.risk.toLowerCase()})" onchange="setRisk(${ci},this.value)">
|
|
|
<option value="High" ${st.risk === 'High' ? 'selected' : ''}>High</option>
|
|
|
<option value="Medium" ${st.risk === 'Medium' ? 'selected' : ''}>Medium</option>
|
|
|
<option value="Low" ${st.risk === 'Low' ? 'selected' : ''}>Low</option>
|
|
|
</select>
|
|
|
${st.risk !== st.origRisk ? `<input type="text" class="risk-reason-inp" placeholder="Reason for change..." value="${esc(st.riskReason)}" oninput="setRiskReason(${ci},this.value)"><span class="diff-badge" title="Original risk">Orig: ${st.origRisk}</span>` : ''}
|
|
|
</div>
|
|
|
|
|
|
<div class="card-actions">
|
|
|
<button class="diff-toggle" style="display:${isExactlyTwo ? 'block' : 'none'}" onclick="toggleDiff(${ci})">${diffMode[ci] ? 'Hide Diff' : 'Show Diff'}</button>
|
|
|
<button class="flag-btn ${st.status === 'flagged' ? 'flagged' : ''}" onclick="toggleFlag(${ci})">🚩 Flag Issue</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
${st.status === 'flagged' ? `<input type="text" class="flag-reason" style="display:block;width:100%;margin-bottom:0.5rem" placeholder="Why is this case flagged?" value="${esc(st.flag)}" oninput="setFlagReason(${ci},this.value)">` : ''}
|
|
|
</div>
|
|
|
|
|
|
<div class="gt-box">
|
|
|
<div class="gt-title">Ground Truth (Expected Context)</div>
|
|
|
<div class="gt-grid">
|
|
|
<div><div class="fi-key">1. Gejala / Indikasi</div><div class="fi-val">${esc(fm.ind)}</div></div>
|
|
|
<div><div class="fi-key">2. Verifikasi Lapangan</div><div class="fi-val">${esc(fm.ver)}</div></div>
|
|
|
<div><div class="fi-key">3. Akar Masalah (Penyebab)</div><div class="fi-val">${esc(fm.cau)}</div></div>
|
|
|
<div><div class="fi-key">4. Dampak Sistem (Efek)</div><div class="fi-val">${esc(fm.eff)}</div></div>
|
|
|
<div style="grid-column: 1 / -1;"><div class="fi-key">5. Rekomendasi Tindakan (FDT)</div><div class="fi-val">${esc(fm.rec)}</div></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card-responses" style="grid-template-columns:repeat(${c.rows.length},1fr)">${cols}</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
function updateCaseDOM(ci) {
|
|
|
const el = document.getElementById(`case-${ci}`);
|
|
|
if (el) el.innerHTML = getCaseHtml(ci);
|
|
|
}
|
|
|
|
|
|
function render() {
|
|
|
const q = document.getElementById('search').value.toLowerCase();
|
|
|
const rflt = document.getElementById('rflt').value;
|
|
|
const sflt = document.getElementById('sflt').value;
|
|
|
|
|
|
let html = '';
|
|
|
let count = 0;
|
|
|
|
|
|
cases.forEach((c, ci) => {
|
|
|
const st = state[ci];
|
|
|
if (rflt !== 'all' && st.risk !== rflt) return;
|
|
|
if (sflt !== 'all' && st.status !== sflt) return;
|
|
|
if (q && !(c.tag + c.prompt).toLowerCase().includes(q)) return;
|
|
|
count++;
|
|
|
|
|
|
html += `<div class="card" id="case-${ci}">${getCaseHtml(ci)}</div>`;
|
|
|
});
|
|
|
|
|
|
document.getElementById('results').innerHTML = html;
|
|
|
document.getElementById('rmeta').textContent = `Showing ${count} cases`;
|
|
|
}
|
|
|
|
|
|
function checkSop(t) { return SOP.map(k => k.r.test(t)); }
|
|
|
function validateTag(exp, t) { return [...new Set(String(t || '').match(TAG_RE) || [])].filter(x => x !== exp); }
|
|
|
|
|
|
function exportData() {
|
|
|
const lines = cases.map((c, i) => {
|
|
|
const s = state[i];
|
|
|
if (s.status === 'pending') return null;
|
|
|
return JSON.stringify({
|
|
|
case_index: i + 1, tag: c.tag, reviewer,
|
|
|
status: s.status, flag_reason: s.flag,
|
|
|
original_risk: s.origRisk, reviewer_risk: s.risk, risk_change_reason: s.riskReason,
|
|
|
best_model: s.best,
|
|
|
evaluations: c.rows.filter(Boolean).map(row => {
|
|
|
const m = row.model;
|
|
|
const ev = s.models[m] || {};
|
|
|
return { model: m, rating: ev.rating || 0, comment: ev.comment || '', dimensions: ev.dims || {}, is_best: s.best === m };
|
|
|
})
|
|
|
});
|
|
|
}).filter(Boolean);
|
|
|
if (!lines.length) { alert('No reviewed cases to export.'); return; }
|
|
|
const a = document.createElement('a');
|
|
|
a.href = URL.createObjectURL(new Blob([lines.join('\n')]));
|
|
|
a.download = `rcfa_expert_eval_${reviewer || 'anon'}_${new Date().getTime()}.jsonl`;
|
|
|
a.click();
|
|
|
}
|
|
|
|
|
|
// Hotkeys
|
|
|
document.addEventListener('keydown', e => {
|
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
|
if (e.key === 'Enter') { e.preventDefault(); jumpNext(); }
|
|
|
});
|
|
|
</script>
|
|
|
</body>
|
|
|
|
|
|
</html> |