codebook / potato /templates /Simple-Likert-Scale-Example-base_template.html
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
97.3 kB
<!DOCTYPE html>
<html lang="en">
<!--- HEADER GOES BELOW HERE--->
<head>
<!-- <script src="jquery-3.3.1.min.js"></script> -->
<script
src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"
></script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"
></script>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="/static/styles/emphasis.css" />
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"
></script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<style>
* {
box-sizing: border-box;
}
.row {
display: flex;
flex-direction: column;
}
/* border:1px solid black;
border-radius: 5px; */
/* Create two equal columns that sits next to each other */
.column {
flex: 50%;
padding: 15px;
}
.instance {
border: 1px solid #070707;
background-color: cccccc;
border-radius: 5px;
padding: 15px;
display: block;
justify-content: center;
width: fit-content;
max-width: 80%;
text-align: left;
margin: 0 auto;
font-size: 15pt;
}
.instance p {
font-weight: bold;
display: block;
}
.annotation_schema {
padding: 5px;
display: flex;
justify-content: center;
width: 80%;
text-align: left;
margin: 0 auto;
}
fieldset {
border: 1px solid #160085;
background-color: cccccc;
padding: 10px 15px 10px 15px;
}
legend {
color: black;
}
input[type="text"] {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
}
input {
margin: 5px;
}
.likert li {
float: left;
list-style-type: none;
}
/* The sidepanel menu */
.sidepanel {
min-height: 250px; /* Specify a height */
width: 0; /* 0 width - change this with JavaScript */
position: fixed; /* Stay in place */
z-index: 1; /* Stay on top */
top: 95px;
left: 1px;
background-color: #111; /* Black*/
overflow-x: hidden; /* Disable horizontal scroll */
padding-top: 10px; /* Place content 60px from the top */
padding-bottom: 20px;
transition: 0.5s; /* 0.5 second transition effect to slide in the sidepanel */
}
/* The sidepanel links */
.sidepanel a {
padding: 8px 8px 8px 32px;
text-decoration: none;
font-size: 55px;
color: #ffffff; /* #818181; */
display: block;
transition: 0.3s;
}
/* When you mouse over the navigation links, change their color */
.sidepanel a:hover {
color: #f1f1f1;
}
/* When you mouse over the navigation links, change their color */
.sidepanel table {
color: #ffffff;
font-size: 20px;
/*padding: 8px 8px 18px 62px;*/
margin-left: 20px;
margin-top: 10px;
border-spacing: 10px;
}
.sidepanel td,
th {
color: #ffffff;
padding: 5px;
}
.sidepanel tr:nth-child(even) {
background-color: #333333;
}
/* Position and style the close button (top right corner) */
.sidepanel .closebtn {
position: absolute;
top: 0;
right: 25px;
font-size: 36px;
margin-left: 50px;
}
/* Style the button that is used to open the sidepanel */
.openbtn {
cursor: pointer;
/*
font-size: 20px;
background-color: #111;
color: white;
padding: 10px 15px;
border: none;*/
}
.openbtn:hover {
background-color: #444;
}
/* The instructions */
.instructions {
/* width: 0; */ /* 0 width - change this with JavaScript */
transition: 0.5s; /* 0.5 second transition effect to slide in the sidepanel */
overflow-y: hidden; /* Disable horizontal scroll */
border: 1px solid #070707;
background-color: cccccc;
border-radius: 5px;
padding: 15px;
display: block;
justify-content: center;
width: fit-content;
max-width: 80%;
text-align: left;
margin: 0 auto;
font-size: 14pt;
}
/* Position and style the close button (top right corner) */
.instructions .closebtn {
position: absolute;
top: 0;
right: 25px;
font-size: 36px;
margin-left: 50px;
}
/* Style the button that is used to open the sidepanel */
.instructions .openbtn {
cursor: pointer;
/*
font-size: 20px;
background-color: #111;
color: white;
padding: 10px 15px;
border: none;*/
}
.span_container {
border-width: 3px;
border-style: solid;
border: 2px solid black;
position: relative;
border-radius: 5px;
padding: 2px 5px;
background-color: pink;
}
.span_label {
position: absolute;
top: -12px;
left: 5px;
font-size: 7px;
padding: 1px 1px;
border-radius: 3px;
background-color: pink;
border: 2px solid red;
text-align: center;
}
.span_close {
position: absolute;
top: -7px;
right: -7px;
width: 14px;
height: 14px;
font-size: 7px;
padding: 2px 2px;
border-radius: 50%;
background-color: gray;
border: 2px solid black;
text-align: center;
font: 7pt Arial, sans-serif;
}
</style>
</head>
<!--- HEADER GOES ABOVE HERE--->
<style>
:root {
--background: #ffffff;
--foreground: #09090b;
--card: #ffffff;
--card-foreground: #09090b;
--popover: #ffffff;
--popover-foreground: #09090b;
--primary: #6e56cf;
--primary-foreground: #ffffff;
--secondary: #f4f4f5;
--secondary-foreground: #18181b;
--muted: #f4f4f5;
--muted-foreground: #71717a;
--accent: #f4f4f5;
--accent-foreground: #18181b;
--destructive: #ef4444;
--destructive-foreground: #fafafa;
--border: #e4e4e7;
--input: #e4e4e7;
--ring: #6e56cf;
--radius: 0.5rem;
--primary-color: #6e56cf; /* Shadcn purple */
--secondary-color: #7c66ce;
--accent-color: #a18fff;
--dark-color: #09090b;
--light-color: #fafafa;
--gray-color: #71717a;
--light-gray: #f4f4f5;
--light-bg: #f5f7fb; /* Light background for sections */
--heading-color: white; /* For headings */
--text-color: #555555; /* For regular text */
--success-color: #10b981;
--border-radius: 0.5rem;
--box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1),
0 1px 2px -1px rgb(0 0 0 / 0.1);
--transition: all 0.2s ease;
--section-padding-top: 100px;
}
/* Shadcn-style button */
.shadcn-button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
font-weight: 500;
font-size: 0.875rem;
height: 2.5rem;
padding-left: 1rem;
padding-right: 1rem;
transition: var(--transition);
cursor: pointer;
text-decoration: none;
}
.shadcn-button-primary {
background-color: var(--primary);
color: var(--primary-foreground);
border: 1px solid var(--primary);
}
.shadcn-button-primary:hover {
background-color: var(--secondary-color);
color: var(--primary-foreground);
}
.shadcn-button-outline {
background-color: transparent;
color: var(--primary);
border: 1px solid var(--border);
}
.shadcn-button-outline:hover {
background-color: var(--secondary);
color: var(--primary);
}
/* Sidepanel styling */
.sidepanel {
height: 100%;
width: 0;
position: fixed;
z-index: 1000;
top: 0;
right: 0;
background-color: var(--card);
overflow-x: hidden;
transition: 0.3s;
padding-top: 60px;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
}
.sidepanel a {
padding: 8px 8px 8px 32px;
text-decoration: none;
font-size: 16px;
color: var(--foreground);
display: block;
transition: 0.3s;
}
.sidepanel .closebtn {
position: absolute;
top: 0;
right: 25px;
font-size: 36px;
margin-left: 50px;
color: var(--muted-foreground);
}
/* Span annotation styling */
.span_container {
position: relative;
display: inline-block;
border-radius: var(--radius);
}
.span_label {
position: absolute;
top: -18px;
left: 0;
font-size: 10px;
border-radius: 3px;
padding: 0 3px;
color: var(--primary);
}
.span_close {
position: absolute;
top: -18px;
right: 0;
font-size: 12px;
border-radius: 3px;
padding: 0 3px;
cursor: pointer;
color: var(--destructive);
}
/* Card styling */
.shadcn-card {
height: 65vh;
background-color: var(--card);
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: var(--box-shadow);
overflow: hidden;
}
.shadcn-card-header {
padding: 1.5rem;
border-bottom: 1px solid var(--border);
}
.shadcn-card-content {
padding: 1.5rem;
}
fieldset {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
margin-bottom: 1rem;
}
legend {
font-weight: 500;
color: var(--heading-color);
padding: 0 0.5rem;
}
/* Form elements */
input[type="text"],
input[type="number"],
textarea,
select {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.5rem;
background-color: var(--background);
color: var(--foreground);
transition: var(--transition);
}
input[type="text"]:focus,
input[type="number"]:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--ring);
box-shadow: 0 0 0 2px var(--ring-opacity);
}
</style>
{{ custom_js | safe }}
<body style="background-color: var(--background); color: var(--foreground)">
<input type="hidden" name="username" id="username" value="{{username}}" />
<input
type="hidden"
name="instance_id"
id="instance_id"
value="{{instance_id}}"
/>
<input
type="hidden"
name="alert_time_each_instance"
id="alert_time_each_instance"
value="{{alert_time_each_instance}}"
/>
<p id="timecounter" hidden>Time spent:</p>
<div id="mySidepanel" class="sidepanel">
<a
href="javascript:void(0)"
class="closebtn"
target_id="mySidepanel"
onclick="closeNav(this.getAttribute('target_id'))"
>&times;</a
>
<table><tr><th>Key</th><th>Description</th></tr><tr><td style="text-align: center;">&#8592;</td><td>Move backward</td></tr><tr><td style="text-align: center;">&#8594;</td><td>Move forward</td></tr><tr><td style="text-align: center;">1</td><td>relevance: 1</td></tr><tr><td style="text-align: center;">2</td><td>relevance: 2</td></tr><tr><td style="text-align: center;">3</td><td>relevance: 3</td></tr><tr><td style="text-align: center;">4</td><td>relevance: 4</td></tr><tr><td style="text-align: center;">5</td><td>relevance: 5</td></tr><tr><td style="text-align: center;">1</td><td>coherence: 1</td></tr><tr><td style="text-align: center;">2</td><td>coherence: 2</td></tr><tr><td style="text-align: center;">3</td><td>coherence: 3</td></tr><tr><td style="text-align: center;">4</td><td>coherence: 4</td></tr><tr><td style="text-align: center;">5</td><td>coherence: 5</td></tr></table>
</div>
<div id="Statpanel" class="sidepanel">
<a
href="javascript:void(0)"
class="closebtn"
target_id="Statpanel"
onclick="closeNav(this.getAttribute('target_id'))"
>&times;</a
>
[[statistics_nav]]
</div>
<script>
// this is for ai hints
document.addEventListener("click", function (event) {
const hintEl = event.target.closest(".hint");
if (!hintEl) return; // Not a .hint click
const aiHelp = hintEl.closest(".ai-help");
const tooltip = aiHelp.querySelector(".tooltip");
const tooltipText = tooltip.querySelector(".tooltip-text");
const annotationId = aiHelp.closest("[data-annotation-id]")?.dataset
.annotationId;
const text = document.getElementById("instance-text").innerText;
fetch("get_ai_hint", {
method: "POST",
credentials: "same-origin",
body: JSON.stringify({ annotation_id: annotationId, text: text }),
headers: { "Content-Type": "application/json" },
})
.then((response) => response.json())
.then((data) => {
tooltipText.innerHTML = `<span class="reasoning">Reasoning:</span> ${data.reasoning}`;
tooltip.classList.toggle("active");
})
.catch((error) => {
tooltipText.innerHTML = `<span class="reasoning">Error:</span> Could not load hint`;
tooltip.classList.toggle("active");
console.error("Error fetching AI hint:", error);
});
});
document.addEventListener("click", function (event) {
if (!event.target.closest(".ai-help")) {
document.querySelectorAll(".tooltip.active").forEach((tooltip) => {
tooltip.classList.remove("active");
});
}
});
</script>
<script>
document.addEventListener("keyup", function (event) {
var active_id = document.activeElement.id;
var active_type = document.activeElement.getAttribute("type");
if ((active_id == "go_to") | (active_type == "text")) return;
//first check whether this keyboard input is a shortcut for checkboxes
var checkboxes = document.querySelectorAll("input[type=checkbox]");
var radios = document.querySelectorAll("input[type=radio]");
var x = event.key.toLowerCase();
for (var i = 0; i < checkboxes.length; i++) {
//alert(checkboxes[i].value)
if (x === checkboxes[i].value) {
checkboxes[i].checked = !checkboxes[i].checked;
if (checkboxes[i].onclick != null)
checkboxes[i].onclick.apply(checkboxes[i]);
return;
}
}
for (var i = 0; i < radios.length; i++) {
//alert(checkboxes[i].value)
if (x === radios[i].value) {
radios[i].checked = !radios[i].checked;
if (radios[i].onclick != null) radios[i].onclick.apply(radios[i]);
return;
}
}
// Each time we process a user's key presses, track who is doing
// it by grabbing the hidden firstname and lastname fields
get_new_instance(event);
});
function getFieldSetState(element) {
// console.dir('saw annotation' + element.value);
// console.dir(element.parentElement);
//var fieldSet = element.parentElement;
// Find the closeset ancestor fieldset element from this input
var fieldSet = element.closest("fieldset");
// Get all the inputs in this field set (what are the annotation options)
var inputs = fieldSet.querySelectorAll("input");
// Get state of all the in this group
var state = Array.from(inputs).map((input) => {
return {
name: input.getAttribute("label_name"),
value: input.checked,
// checked: input.value // unused??
};
});
return state;
}
function registerAnnotation(element) {
// If the element is a radio button, we need to uncheck all the other which
// requires sending the state of all the other buttons in the group
sc = element.getAttribute("selection_constraint");
if (sc && sc == "single") {
state = getFieldSetState(element);
} else if (element.getAttribute("type") == "range") {
state = [
{
name: element.getAttribute("label_name"),
value: element.value,
},
];
}
// This is a checkbox so we can just send the state of this checkbox
else {
state = [
{
name: element.getAttribute("label_name"),
value: element.checked,
// checked: element.value // unused??
},
];
}
// Get which annotation group this change belongs to
var schema = element.getAttribute("schema");
// Get the instance id
var instance_id = document.getElementById("instance_id").value;
// Package this all up in a post request to the server's updateinstance endpoint
var post_req = {
type: "label",
schema: schema,
state: state,
instance_id: instance_id,
};
// Send the post request
fetch("/updateinstance", {
method: "POST",
body: JSON.stringify(post_req),
credentials: "same-origin",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
});
}
function registerTextAnnotation(element) {
// Get which annotation group this change belongs to
var schema = element.getAttribute("schema");
// Get the instance id
var instance_id = document.getElementById("instance_id").value;
state = [
{
name: element.getAttribute("label_name"),
value: element.value,
},
];
// Package this all up in a post request to the server's updateinstance endpoint
var post_req = {
type: "label",
schema: schema,
state: state,
instance_id: instance_id,
};
// Send the post request
fetch("/updateinstance", {
method: "POST",
body: JSON.stringify(post_req),
credentials: "same-origin",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
});
}
function getSelectedText() {
var text = "";
if (typeof window.getSelection != "undefined") {
text = window.getSelection().toString();
} else if (
typeof document.selection != "undefined" &&
document.selection.type == "Text"
) {
text = document.selection.createRange().text;
}
return text;
}
function deleteSpanAnnotation(
spanElem,
schema,
labelName,
title,
start,
end
) {
// Get the instance id
var instance_id = document.getElementById("instance_id").value;
// Get the text of the span
var spanText = spanElem.innerText;
// Package this all up in a post request to the server's updateinstance endpoint
var post_req = {
type: "span",
schema: schema,
state: [
{
name: labelName,
start: start,
end: end,
title: title,
value: null,
},
],
instance_id: instance_id,
};
// Send the post request
fetch("/updateinstance", {
method: "POST",
body: JSON.stringify(post_req),
credentials: "same-origin",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
});
// Remove the outer most div tag that has the span's annotation title (the small text above)
$(spanElem).find("div").first().remove();
// Remove the close tag
$(spanElem).find("div").last().remove();
// Remove the span highlight by unwrapping the inner text
$(spanElem.firstChild).unwrap();
}
function getEarlierChildren(n, skipMe) {
var r = [];
for (; n; n = n.nextSibling)
if (n.nodeType == 1 && n != skipMe) r.push(n);
else if (n == skipMe) break;
return r;
}
function getEarlierSiblings(n) {
return getEarlierChildren(n.parentNode.firstChild, n);
}
function getTextOfSpans(elems) {
text = "";
for (i = 0; i < elems.length; i++) {
//console.log(elems[i].textContent)
e = elems[i].cloneNode(true);
console.log("e", e);
// Delete all the nested <div> elements with jquery to get the plain text
$(e).find("div").remove();
console.log("sib text", e.textContent);
text += " " + e.textContent;
}
return text.trim();
}
function getSelectionIndices() {
// Get the user selection
var selection = window.getSelection();
if (selection.rangeCount === 0) {
return { start: -1, end: -1 }; // No selection
}
// Get the range object representing the selected portion
var range = selection.getRangeAt(0);
// Get the left most element in the selection
var leftMost = range.startContainer;
var leftSibs = getEarlierSiblings(leftMost);
console.log("leftSibs", leftSibs);
// Get the text of the left elements
var leftText = getTextOfSpans(leftSibs);
// Find the parent div element with name "instance_text" using jquery
var parentDiv = $(range.commonAncestorContainer).closest(
'div[name="instance_text"]'
)[0];
//console.log('parentDiv', parentDiv)
if (!parentDiv) {
return { start: -2, end: -2 }; // Not within a <div class="context_text">
}
//console.log('parentDiv')
//console.dir(parentDiv.innerHTML)
// Make a copy of the parent div to manipulate
parentDiv = parentDiv.cloneNode(true);
// Create a temporary Range to find the offset relative to the parent div
console.log(
"before delete",
range.startContainer,
range.startOffset,
range.endOffset
);
var tempRange = document.createRange();
tempRange.setStart(parentDiv, 0);
tempRange.setEnd(range.startContainer, range.startOffset);
// Delete all the nested <div> elements with jquery to get the plain text
$(parentDiv).find("div").remove();
console.log("after delete");
console.dir(parentDiv.innerHTML);
console.log("text");
console.dir(parentDiv.textContent);
console.log(
"after delete",
range.startContainer,
range.startOffset,
range.endOffset
);
// Get the starting offset of the selection within the parent div
var div = document.createElement("div");
div.appendChild(tempRange.cloneContents());
var s = range.startContainer.textContent.toString();
//console.log(`text: "${text}"`);
var whitespaceOffset = s.length - s.trimStart().length;
var startOffset = range.startOffset - whitespaceOffset;
if (leftText.length > 0) {
// Add 1 for space between the left text and the selected text
startOffset += leftText.length + 1;
}
// Get the length of the selected text
var selectedTextLength = range.toString().length;
// Calculate the ending offset of the selection within the parent div
var endOffset = startOffset + selectedTextLength;
return { start: startOffset, end: endOffset };
}
function surroundSelection(schema, labelName, title, selectionColor) {
//var span = document.createElement("span");
//span.style.fontWeight = "bold";
//span.style.color = "green";
// Check that this wasn't a spurious click or the click for the delete button which
// also seems to trigger this selection event
if (window.getSelection().rangeCount == 0) {
return;
}
var range = window.getSelection().getRangeAt(0);
// console.dir(window.getSelection())
// console.log(range.startOffset, range.endOffset)
if (range.startOffset == range.endOffset) {
return;
}
// Get the instance id
var instance_id = document.getElementById("instance_id").value;
if (window.getSelection) {
var sel = window.getSelection();
// console.dir(sel);
// Check that we're labeling something in the instance text that
// we want to annotate
if (!sel.anchorNode.parentElement) {
return;
}
// TODO: check that we're not nesting a span of the same time here
// Otherwise, we're going to be adding a new span annotation, if
// the user has selected some non-empty part of the text
if (sel.rangeCount && sel.toString().trim().length > 0) {
// Get the selection text as a string
var selText = window.getSelection().toString().trim();
console.log("selText1", selText);
// Get the start and end offsets of the selection
var range = sel.getRangeAt(0);
var start = range.startOffset;
var end = range.endOffset;
// Get the generic text between the start and end offsets
var text = range.startContainer.textContent.substring(start, end);
var s = range.startContainer.textContent.toString();
//console.log(`text: "${text}"`);
var whitespaceOffset = s.length - s.trimStart().length;
//console.log('whitespaceOffset', whitespaceOffset);
/*
console.log(`full range text "${range.startContainer.textContent}`);
console.log('start', start);
console.log('end', end);
console.log('start', start - whitespaceOffset);
console.log('end', end - whitespaceOffset);
console.log(`ranged substring "${text}"`);
*/
// Get the raw text of the selection without any HTML
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var div = document.createElement("div");
div.appendChild(range.cloneContents());
var selText = div.innerHTML;
//console.log(`selText2: "${selText}"`);
tsc = selectionColor.replace(")", ", 0.25)");
var span = document.createElement("span");
span.className = "span_container";
span.setAttribute("selection_label", labelName);
span.setAttribute("style", "background-color:rgb" + tsc + ";");
var label = document.createElement("div");
label.className = "span_label";
label.textContent = title;
label.setAttribute(
"style",
"background-color:white;" +
"border:2px solid rgb" +
selectionColor +
";"
);
var delLabel = document.createElement("div");
delLabel.className = "span_close";
delLabel.textContent = "×";
delLabel.setAttribute("style", "background-color:white;");
// + "border:2px solid rgb" + selectionColor + ";");
delLabel.onclick = function () {
deleteSpanAnnotation(
span,
schema,
labelName,
title,
range.startOffset,
range.endOffset
);
};
// Get the offsets
startEnd = getSelectionIndices();
console.log("startEnd", startEnd);
state = [
{
name: labelName,
start: startEnd["start"],
end: startEnd["end"],
title: title,
value: sel.toString(),
},
];
// console.log('adding new annotation', range.startOffset, range.endOffset, sel.toString())
// Package this all up in a post request to the server's updateinstance endpoint
var post_req = {
type: "span",
schema: schema,
state: state,
instance_id: instance_id,
};
// Send the post request
fetch("/updateinstance", {
method: "POST",
body: JSON.stringify(post_req),
credentials: "same-origin",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
});
var range = sel.getRangeAt(0).cloneRange();
range.surroundContents(span);
sel.removeAllRanges();
sel.addRange(range);
span.appendChild(label);
span.appendChild(delLabel);
// Clear the current selection
sel.empty();
}
}
}
function changeSpanLabel(
checkbox,
schema,
spanLabel,
spanTitle,
spanColor
) {
// Listen for when the user has highlighted some text (only when the label is checked)
document.onmouseup = function (e) {
var senderElement = e.target;
// Avoid the case where the user clicks the delete button
if (senderElement.getAttribute("class") == "span_close") {
e.stopPropagation();
return true;
}
if (checkbox.checked) {
surroundSelection(schema, spanLabel, spanTitle, spanColor);
}
};
}
// Listen for when the user has highlighted some text
// document.onmouseup = function() { surroundSelection("Undefined"); }
</script>
<script>
function barValue(range, sibling) {
// function used to obtain value for range input
sibling.value = range.value;
//var x = document.getElementsByClassName(range.className);
//var i;
//for (i=0: i < x.length; i++) {
// if(x[i].value != range.value) x[i]
//}
}
</script>
<script>
function onlyOne(checkbox) {
// this function is used for the single-choice setting
//alert(checkbox.className)
var x = document.getElementsByClassName(checkbox.className);
var i;
for (i = 0; i < x.length; i++) {
if (x[i].value != checkbox.value) x[i].checked = false;
}
}
</script>
<script>
function whetherNone(checkbox) {
// this function is used to uncheck all the other labels when "None" is checked
//alert(checkbox.className)
var x = document.getElementsByClassName(checkbox.className);
var i;
for (i = 0; i < x.length; i++) {
if (checkbox.value == "None" && x[i].value != "None")
x[i].checked = false;
if (checkbox.value != "None" && x[i].value == "None")
x[i].checked = false;
}
}
</script>
<script>
function click_to_next() {
// Gacky code to simulate the submit button as a keyboard event
// and not have two separate paths to handle keyboard and mouse
// events
var e = $.Event("keyup");
e.key = "ArrowRight";
get_new_instance(e);
}
function click_to_prev() {
// Gacky code to simulate the submit button as a keyboard event
// and not have two separate paths to handle keyboard and mouse
// events
var e = $.Event("keyup");
e.key = "ArrowLeft";
get_new_instance(e);
}
</script>
{{annotation}}
<script>
/*
window.onunload = check_close;
function check_close() {
// console.error("session closed");
var post_req = {
is_close: "closed"
}
post(post_req);
}
*/
</script>
<script>
// Set the date we're counting down to
var countDownDate = new Date().getTime();
// Update the count down every 1 second
var x = setInterval(function () {
// Get today's date and time
var now = new Date().getTime();
// Find the distance between now and the count down date
var distance = now - countDownDate;
// Time calculations for days, hours, minutes and seconds
var days = Math.floor(distance / (1000 * 60 * 60 * 24));
var hours = Math.floor(
(distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
);
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
var total_seconds = Math.floor(distance / 1000);
// Output the result in an element with id="timecounter"
//document.getElementById("timecounter").innerHTML = "Time spent: " + days + "d " + hours + "h "
// + minutes + "m " + seconds + "s ";
// TODO: add configurations for alert message and maximum time spent on each instance
// If the count down is over, write some text
var alert_time_each_instance = document.getElementById(
"alert_time_each_instance"
).value;
if (total_seconds % alert_time_each_instance == 0) {
//clearInterval(x);
//document.getElementById("timecounter").innerHTML = "EXPIRED";
alert(
"You have spent " + total_seconds + " seconds on this instance"
);
}
}, 1000);
</script>
<script>
// We submit a new post to the same (user/annotate) endpoint
function post(params) {
// Package this all up in a post request to the server's updateinstance endpoint
var post_req = {
instance_id: instance_id,
};
for (var key in params) {
if (params.hasOwnProperty(key)) {
post_req[key] = params[key];
}
}
// Extract inputs from the DOM and add to post_req
$("form input, form select").each(function () {
var input = $(this);
var type = input.attr("type");
var name = input.attr("name");
if (!name) return; // skip unnamed inputs
if (type === "checkbox" || type === "radio") {
if (input.is(":checked")) {
post_req[name] = true;
}
} else if (type === "text" || input.is("select")) {
post_req[name] = input.val();
} else {
console.log("unknown input type:", type);
}
});
// Send the post request and re-render the page with the new instance
//
// TODO: fix this the server-client protocol so we're just sending over
// the new instance data and not the entire page
fetch("/annotate", {
method: "POST",
body: JSON.stringify(post_req),
credentials: "same-origin",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
})
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.text(); // Assuming the response is text/html
})
.then((html) => {
// 2. Update the page content
document.body.innerHTML = html;
// Add all the listeners back to the new page
watch_text_boxes();
})
.catch((error) => {
console.error("Error:", error);
// Handle errors appropriately, e.g., display an error message
});
/*
// The rest of this code assumes you are not using a library.
// It can be made less wordy if you use one.
var form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", "/");
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "email");
hiddenField.setAttribute("value", document.getElementById('username').value);
form.appendChild(hiddenField);
for (var key in params) {
if (params.hasOwnProperty(key)) {
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", key);
hiddenField.setAttribute("value", params[key]);
form.appendChild(hiddenField);
}
}
// Stuff all the current annotations into attributes for processing on the server side
$('form input, form select, form textarea').each(
function(index){
var input = $(this);
if (input.attr('type') == 'checkbox' || input.attr('type') == 'radio') {
if (input.is(":checked")) {
// Stuff all the input fields into something for the post
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", input.attr('name'));
hiddenField.setAttribute("value", input.attr('value'));
form.appendChild(hiddenField);
}
}
else if (input.attr('type') == 'text' || input.attr('type') == 'number') {
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", input.attr('name'));
hiddenField.setAttribute("value", input[0].value);
form.appendChild(hiddenField);
}
else if (input.attr('type') == 'range') {
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", input.attr('name'));
hiddenField.setAttribute("value", input[0].value);
form.appendChild(hiddenField);
}
else if (input.attr('type') == 'select-one') {
//alert(input[0].value)
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", input.attr('name'));
hiddenField.setAttribute("value", input[0].value);
form.appendChild(hiddenField);
}
else {
console.log("unknown form type: \"" + input.attr('type') + "\"")
}
}
);
*/
/*
// Get all the highlighted text for this instance and marshall that
// into some kind of representation for the server side
$(".span_container").first().each(
function(index) {
// we save the outerHTML to accomadate user-defined HTML inputs,
// otherwise we just save the plain text
if ($(this).parent().parent().attr("name") == "instance_text"){
var annotated_spans = $(this).parent().prop('outerHTML');
} else {
var annotated_spans = $(this).parent().prop('innerHTML');
}
// Due to the DJ's inability to write decent Javascript, we're
// fully punting on the idea of doing label preprocessing here
// and instead shuttling the entire HTML of the instance to
// the server for python-based processing. The main issue is
// figuring out the precise text offsets of the annotated
// spans while dealing with nested DOM elements.
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", "span-annotation");
hiddenField.setAttribute("value", annotated_spans);
form.appendChild(hiddenField);
//console.log(annotated_spans)
}
);
*/
//document.body.appendChild(form);
//form.submit();
}
/**
* Validate if the user has completed each row for multi-rate schema
*/
function validateForm() {
var rows = document.querySelectorAll("tr[schema='multirate']");
for (var i = 0; i < rows.length; i++) {
var inputs = rows[i].querySelectorAll(
"input[type='radio'][validation='required']"
);
if (inputs.length > 0) {
var checked = Array.from(inputs).some((input) => input.checked);
} else {
var checked = true;
}
if (!checked) {
alert("Please complete all the require fields");
return false;
}
}
return true;
}
/**
* Validate if the input answers meet certain rules
* along with any relevant key presses to request a new instance to
* annotate.
*/
function validate_answers() {
// Test whether all the required labels are checked
var inputs = document.querySelectorAll(
"input[validation=required_label]"
);
for (var i = 0; i < inputs.length; ++i) {
if (
inputs[i].getAttribute("validation") == "required_label" &&
inputs[i].checked == false
) {
// Get the text of the legend element that contains the question
var legend = inputs[i].closest("fieldset").querySelector("legend");
var question = legend.textContent;
// Get the text of the answer
var neededAnswer = inputs[i].getAttribute("label_name");
// Generate an alert that shows the question text and the required label
// so the user knows what they missed
alert(
'The question "' +
question +
'" requires the label "' +
neededAnswer +
'" to be selected to proceed'
);
return false;
}
}
// identify all the fieldsets and check if all the required forms are filled
var fields = document.getElementsByTagName("fieldset");
for (var i = 0; i < fields.length; ++i) {
var inputs = fields[i].querySelectorAll(
"input[validation=required], select[validation=required], textarea[validation=required]"
);
// continue if all there's no required inputs in the current field set
if (inputs.length == 0) {
continue;
}
//var required = true;
// check if the current form requires inputs
//if (inputs[0].getAttribute('validation') == 'required'){
// required= true;
//}
checked_flag = false;
for (var j = 0; j < inputs.length; ++j) {
// if a right_label is not selected, display an error msg and return false
//if (inputs[j].getAttribute('validation') == 'right_label' && inputs[j].checked == false) {
//alert(inputs[j].name + " must be selected to proceed");
// return false;
//}
// if the input is for a span annotation schema, check if the at least some span is annotated or if the
// bad_text label is selected
if (inputs[j].getAttribute("for_span") == "True") {
//alert($(".span_container").length);
if ($(".span_container").length > 0) {
checked_flag = true;
break;
} else if (
inputs[j].name.slice(-8) == "bad_text" &&
inputs[j].checked == true
) {
checked_flag = true;
break;
}
}
// if any of the labels is checked, set checked_flag as true;
if (
inputs[j].getAttribute("for_span") != "True" &&
inputs[j].checked == true
) {
checked_flag = true;
break;
}
// if the input_type is number, text, select or textarea, check if it's empty
// todo: the current way might not work well if there are mixed textinput and radio buttons under a sample fieldset
if (
inputs[j].type == "text" ||
inputs[j].tagName == "TEXTAREA" ||
inputs[j].type == "number" ||
inputs[j].type == "select-one"
) {
if (inputs[j].value.length == 0) {
alert(inputs[j].name + " must be completed to proceed");
return false;
} else {
checked_flag = true;
}
}
}
// Get the text of the legend element that contains the question
//
// TODO: check that this works with questions for the text input fields
var legend = inputs[i].closest("fieldset").querySelector("legend");
var question = legend.textContent;
// if this form requires inputs, but nothing is checked, display an error msg and return false
if (checked_flag == false) {
alert("You must answer the following item to proceed: " + question);
return false;
}
//alert(instance_obj.id)
}
return true;
}
/**
* Sends the current state of the instance's annotation to the server,
* along with any relevant key presses to request a new instance to
* annotate.
*/
function get_new_instance(event) {
var x = event.key;
var action = "";
var ism = "";
//console.log(x)
//console.log(firstname)
//console.log(lastname)
if (x == "ArrowLeft") {
// Spacebar
action = "prev_instance";
} else if (x == "ArrowRight") {
//console.log("validateForm", validateForm())
//console.log("validate_answers", validate_answers())
if (validateForm() == true && validate_answers() == true) {
action = "next_instance";
} else {
return;
}
} else {
// console.log("Unknown key press", event)
return;
}
var instance_id = document.getElementById("instance_id").value;
var time_string = document.getElementById("timecounter").innerHTML; //get time spent on this instance
//time_string = '-1'
var post_req = {
label: ism,
action: action,
instance_id: instance_id,
behavior_time_string: time_string,
};
console.log("post_req: ", post_req);
// Sends the post message to the server which will let us update the
// currently displayed content
post(post_req);
}
</script>
<script>
$(document).ready(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{{var_elems | safe}}
<!-- Header navigation bar -->
<nav
class="navbar sticky-top shadow-sm"
style="
background-color: var(--primary-color);
padding: 0.75rem 1rem;
margin-bottom: 1.5rem;
"
>
<div class="container-fluid d-flex align-items-center">
<!-- Task name (left aligned) -->
<div class="navbar-brand">
<span
class="fw-bold"
style="color: var(--light-color); font-size: 1.25rem"
>Simple Likert Scale Example</span
>
</div>
<!-- Middle section with three elements -->
<div
class="d-flex align-items-center justify-content-center flex-grow-1 gap-4"
>
<!-- 1. Annotation codebook link -->
<div></div>
<!-- 2. Progress counter -->
<div
class="d-flex align-items-center"
style="color: var(--light-color); margin-right: 40px"
>
<span
>Progress:
<span class="fw-medium">{{finished}}/{{total_count}}</span></span
>
</div>
<!-- 3. Jump to item form -->
<div style="margin-left: 40px">
<form
action="/go_to"
method="post"
class="d-flex align-items-center"
>
<input
type="hidden"
name="firstname"
id="a"
value="{{firstname}}"
/>
<input
type="hidden"
name="lastname"
id="b"
value="{{lastname}}"
/>
<input type="hidden" name="email" id="c" value="{{username}}" />
<input type="hidden" name="src" id="src" value="go_to" />
<label
for="go_to"
class="me-2"
style="color: var(--light-color); margin-bottom: 0"
>Go to:</label
>
<input
type="number"
name="go_to"
id="go_to"
class="form-control form-control-sm me-2"
style="
width: 70px;
height: 31px;
border: none;
border-radius: var(--radius);
"
value=""
onfocusin="user_input()"
onfocusout="user_input_leave()"
max="{{total_count}}"
min="0"
required
/>
<button
type="submit"
class="shadcn-button"
style="
height: 31px;
line-height: 1;
padding: 0 0.75rem;
background-color: var(--light-color);
color: var(--primary-color);
border: none;
"
>
Go
</button>
</form>
</div>
</div>
<!-- User info and logout (right aligned) -->
<div
class="d-flex align-items-center"
style="
background-color: rgba(255, 255, 255, 0.15);
padding: 0.5rem 1rem;
border-radius: var(--radius);
"
>
<div class="me-3" style="color: var(--light-color)">
<span
>Logged in as <span class="fw-medium">{{username}}</span></span
>
</div>
<div
class="border-start"
style="
padding-left: 20px;
height: 24px;
display: flex;
align-items: center;
"
>
<a
href="/logout"
class="shadcn-button"
style="
background-color: var(--light-color);
color: var(--primary-color);
height: auto;
padding: 0.25rem 0.75rem !important;
font-size: 0.875rem;
min-width: 60px;
text-align: center;
border: none;
text-decoration: none;
"
>Logout</a
>
</div>
</div>
</div>
</nav>
<div class="container-fluid">
<div id="task_layout" class="shadcn-card mb-4">
<div class="shadcn-card-content">
<!--- LAYOUT GOES BELOW HERE --->
<div class="row">
<div class="col-md-12">
<div class="row">
<div class="col-md-12">
<div name="context_text" class="instance">
<div id="instance-text" name="instance_text">
{{instance | safe}}
</div>
</div>
</div>
</div>
<div style="height: 15px;" />
</div>
<div class="col-md-12">
<div class="annotation_schema">
<style>
.shadcn-likert-container {
border: 1px solid #E5E5EA;
border-radius: 18px 0 18px 18px;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
width: fit-content;
max-width: 100%;
margin: 1.5rem auto;
font-family: ui-sans-serif, system-ui, sans-serif;
position: relative;
}
.shadcn-likert-title {
font-size: 1rem;
font-weight: 500;
color: black;
text-align: left;
width: 100%;
}
.shadcn-likert-scale {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.shadcn-likert-endpoint {
display: flex;
align-items: center;
height: 2.5rem;
flex: 0 0 auto;
font-size: 0.875rem;
color: var(--muted-foreground);
padding: 0 0.5rem;
max-width: 30%;
text-align: center;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
.shadcn-likert-options {
display: flex;
flex: 1;
justify-content: space-between;
position: relative;
padding: 0 0.5rem;
gap: 0.5rem;
}
.shadcn-likert-track {
position: absolute;
height: 2px;
background-color: var(--border);
left: 0.5rem;
right: 0.5rem;
top: 0.625rem;
transform: none;
z-index: 0;
}
.shadcn-likert-option {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
}
.shadcn-likert-input {
opacity: 0;
position: absolute;
width: 0;
height: 0;
}
.shadcn-likert-button {
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background-color: var(--secondary);
border: 2px solid var(--border);
margin-bottom: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.shadcn-likert-input:checked + .shadcn-likert-button {
background-color: var(--primary);
border-color: var(--primary);
transform: scale(1.1);
box-shadow: 0 0 0 3px rgba(110, 86, 207, 0.2);
}
.shadcn-likert-input:focus + .shadcn-likert-button {
box-shadow: 0 0 0 3px rgba(110, 86, 207, 0.2);
}
.shadcn-likert-input:hover + .shadcn-likert-button {
border-color: var(--primary);
}
.shadcn-likert-label {
font-size: 0.75rem;
color: var(--muted-foreground);
margin-top: 0.25rem;
text-align: center;
}
.shadcn-likert-input:checked ~ .shadcn-likert-label {
color: var(--primary);
font-weight: 500;
}
.shadcn-likert-bad-text {
display: flex;
align-items: center;
margin-top: 0.75rem;
padding: 0.5rem 0;
}
.shadcn-likert-bad-text-label {
margin-left: 0.5rem;
font-size: 0.875rem;
color: var(--text-color);
word-wrap: break-word;
overflow-wrap: break-word;
flex: 1;
}
fieldset[schema] {
width: fit-content;
max-width: 100%;
overflow: visible;
}
.ai-help {
width: 9.375rem;
height: 2.063rem;
border: 1px solid #E5E5EA;
border-radius: 18px;
position: absolute;
top: -0.75rem;
right: -0.05rem;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
}
.ai-help-word {
font-size: 1rem;
margin: 0;
}
.hint {
cursor: pointer;
}
.hint:hover {
color: #6E56CF;
}
.tooltip {
position: absolute;
top: 120%;
left: 50%;
transform: translateX(-50%);
background-color: white;
padding: 1rem;
border-radius: .5rem;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
min-width: 20rem;
max-width: 24rem;
overflow-wrap: break-word;
word-wrap: break-word;
white-space: normal;
max-height: 15rem;
overflow: auto;
}
.tooltip.active {
opacity: 1;
visibility: visible;
}
.reasoning {
font-weight: bold;
}
.tooltip-text{
margin: 0;
}
</style>
<form id="relevance" class="annotation-form likert shadcn-likert-container" action="javascript:void(0)" data-annotation-id="0">
<div class="ai-help">
<h3 class="ai-help-word"><span class="hint">Hint</span> | <span>Keyword</span></h3>
<div class="tooltip">
<p class="tooltip-text">
<span class="reasoning">Reasoning:</span> {ai}
</p>
</div>
</div>
<fieldset schema="relevance" style="border: none; padding: 0; margin: 0; width: auto; min-width: fit-content;">
<legend class="shadcn-likert-title">Relevance</legend>
<div class="shadcn-likert-scale" style="max-width: min(100%, calc(300px + 5 * 40px + 250px));">
<div class="shadcn-likert-endpoint">Not Relevant</div>
<div class="shadcn-likert-options">
<div class="shadcn-likert-track"></div>
<div class="shadcn-likert-option">
<input class="relevance shadcn-likert-input"
type="radio"
id="relevance:::1"
name="relevance:::1"
value="1"
schema="relevance"
label_name="1"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="relevance:::1"></label>
<span class="shadcn-likert-label">1</span>
</div>
<div class="shadcn-likert-option">
<input class="relevance shadcn-likert-input"
type="radio"
id="relevance:::2"
name="relevance:::2"
value="2"
schema="relevance"
label_name="2"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="relevance:::2"></label>
<span class="shadcn-likert-label">2</span>
</div>
<div class="shadcn-likert-option">
<input class="relevance shadcn-likert-input"
type="radio"
id="relevance:::3"
name="relevance:::3"
value="3"
schema="relevance"
label_name="3"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="relevance:::3"></label>
<span class="shadcn-likert-label">3</span>
</div>
<div class="shadcn-likert-option">
<input class="relevance shadcn-likert-input"
type="radio"
id="relevance:::4"
name="relevance:::4"
value="4"
schema="relevance"
label_name="4"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="relevance:::4"></label>
<span class="shadcn-likert-label">4</span>
</div>
<div class="shadcn-likert-option">
<input class="relevance shadcn-likert-input"
type="radio"
id="relevance:::5"
name="relevance:::5"
value="5"
schema="relevance"
label_name="5"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="relevance:::5"></label>
<span class="shadcn-likert-label">5</span>
</div>
</div>
<div class="shadcn-likert-endpoint">Super Relevant</div>
</div>
</fieldset></form>
<style>
.shadcn-likert-container {
border: 1px solid #E5E5EA;
border-radius: 18px 0 18px 18px;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
width: fit-content;
max-width: 100%;
margin: 1.5rem auto;
font-family: ui-sans-serif, system-ui, sans-serif;
position: relative;
}
.shadcn-likert-title {
font-size: 1rem;
font-weight: 500;
color: black;
text-align: left;
width: 100%;
}
.shadcn-likert-scale {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.shadcn-likert-endpoint {
display: flex;
align-items: center;
height: 2.5rem;
flex: 0 0 auto;
font-size: 0.875rem;
color: var(--muted-foreground);
padding: 0 0.5rem;
max-width: 30%;
text-align: center;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
.shadcn-likert-options {
display: flex;
flex: 1;
justify-content: space-between;
position: relative;
padding: 0 0.5rem;
gap: 0.5rem;
}
.shadcn-likert-track {
position: absolute;
height: 2px;
background-color: var(--border);
left: 0.5rem;
right: 0.5rem;
top: 0.625rem;
transform: none;
z-index: 0;
}
.shadcn-likert-option {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
}
.shadcn-likert-input {
opacity: 0;
position: absolute;
width: 0;
height: 0;
}
.shadcn-likert-button {
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background-color: var(--secondary);
border: 2px solid var(--border);
margin-bottom: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.shadcn-likert-input:checked + .shadcn-likert-button {
background-color: var(--primary);
border-color: var(--primary);
transform: scale(1.1);
box-shadow: 0 0 0 3px rgba(110, 86, 207, 0.2);
}
.shadcn-likert-input:focus + .shadcn-likert-button {
box-shadow: 0 0 0 3px rgba(110, 86, 207, 0.2);
}
.shadcn-likert-input:hover + .shadcn-likert-button {
border-color: var(--primary);
}
.shadcn-likert-label {
font-size: 0.75rem;
color: var(--muted-foreground);
margin-top: 0.25rem;
text-align: center;
}
.shadcn-likert-input:checked ~ .shadcn-likert-label {
color: var(--primary);
font-weight: 500;
}
.shadcn-likert-bad-text {
display: flex;
align-items: center;
margin-top: 0.75rem;
padding: 0.5rem 0;
}
.shadcn-likert-bad-text-label {
margin-left: 0.5rem;
font-size: 0.875rem;
color: var(--text-color);
word-wrap: break-word;
overflow-wrap: break-word;
flex: 1;
}
fieldset[schema] {
width: fit-content;
max-width: 100%;
overflow: visible;
}
.ai-help {
width: 9.375rem;
height: 2.063rem;
border: 1px solid #E5E5EA;
border-radius: 18px;
position: absolute;
top: -0.75rem;
right: -0.05rem;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
}
.ai-help-word {
font-size: 1rem;
margin: 0;
}
.hint {
cursor: pointer;
}
.hint:hover {
color: #6E56CF;
}
.tooltip {
position: absolute;
top: 120%;
left: 50%;
transform: translateX(-50%);
background-color: white;
padding: 1rem;
border-radius: .5rem;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
min-width: 20rem;
max-width: 24rem;
overflow-wrap: break-word;
word-wrap: break-word;
white-space: normal;
max-height: 15rem;
overflow: auto;
}
.tooltip.active {
opacity: 1;
visibility: visible;
}
.reasoning {
font-weight: bold;
}
.tooltip-text{
margin: 0;
}
</style>
<form id="fluency" class="annotation-form likert shadcn-likert-container" action="javascript:void(0)" data-annotation-id="1">
<div class="ai-help">
<h3 class="ai-help-word"><span class="hint">Hint</span> | <span>Keyword</span></h3>
<div class="tooltip">
<p class="tooltip-text">
<span class="reasoning">Reasoning:</span> {ai}
</p>
</div>
</div>
<fieldset schema="fluency" style="border: none; padding: 0; margin: 0; width: auto; min-width: fit-content;">
<legend class="shadcn-likert-title">Fluency</legend>
<div class="shadcn-likert-scale" style="max-width: min(100%, calc(300px + 10 * 40px + 250px));">
<div class="shadcn-likert-endpoint">Not fluent</div>
<div class="shadcn-likert-options">
<div class="shadcn-likert-track"></div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::1"
name="fluency:::1"
value="1"
schema="fluency"
label_name="1"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::1"></label>
<span class="shadcn-likert-label">1</span>
</div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::2"
name="fluency:::2"
value="2"
schema="fluency"
label_name="2"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::2"></label>
<span class="shadcn-likert-label">2</span>
</div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::3"
name="fluency:::3"
value="3"
schema="fluency"
label_name="3"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::3"></label>
<span class="shadcn-likert-label">3</span>
</div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::4"
name="fluency:::4"
value="4"
schema="fluency"
label_name="4"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::4"></label>
<span class="shadcn-likert-label">4</span>
</div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::5"
name="fluency:::5"
value="5"
schema="fluency"
label_name="5"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::5"></label>
<span class="shadcn-likert-label">5</span>
</div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::6"
name="fluency:::6"
value="6"
schema="fluency"
label_name="6"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::6"></label>
<span class="shadcn-likert-label">6</span>
</div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::7"
name="fluency:::7"
value="7"
schema="fluency"
label_name="7"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::7"></label>
<span class="shadcn-likert-label">7</span>
</div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::8"
name="fluency:::8"
value="8"
schema="fluency"
label_name="8"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::8"></label>
<span class="shadcn-likert-label">8</span>
</div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::9"
name="fluency:::9"
value="9"
schema="fluency"
label_name="9"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::9"></label>
<span class="shadcn-likert-label">9</span>
</div>
<div class="shadcn-likert-option">
<input class="fluency shadcn-likert-input"
type="radio"
id="fluency:::10"
name="fluency:::10"
value="0"
schema="fluency"
label_name="0"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="fluency:::10"></label>
<span class="shadcn-likert-label">10</span>
</div>
</div>
<div class="shadcn-likert-endpoint">Super fluent</div>
</div>
</fieldset></form>
<style>
.shadcn-likert-container {
border: 1px solid #E5E5EA;
border-radius: 18px 0 18px 18px;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
width: fit-content;
max-width: 100%;
margin: 1.5rem auto;
font-family: ui-sans-serif, system-ui, sans-serif;
position: relative;
}
.shadcn-likert-title {
font-size: 1rem;
font-weight: 500;
color: black;
text-align: left;
width: 100%;
}
.shadcn-likert-scale {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.shadcn-likert-endpoint {
display: flex;
align-items: center;
height: 2.5rem;
flex: 0 0 auto;
font-size: 0.875rem;
color: var(--muted-foreground);
padding: 0 0.5rem;
max-width: 30%;
text-align: center;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
.shadcn-likert-options {
display: flex;
flex: 1;
justify-content: space-between;
position: relative;
padding: 0 0.5rem;
gap: 0.5rem;
}
.shadcn-likert-track {
position: absolute;
height: 2px;
background-color: var(--border);
left: 0.5rem;
right: 0.5rem;
top: 0.625rem;
transform: none;
z-index: 0;
}
.shadcn-likert-option {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
}
.shadcn-likert-input {
opacity: 0;
position: absolute;
width: 0;
height: 0;
}
.shadcn-likert-button {
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background-color: var(--secondary);
border: 2px solid var(--border);
margin-bottom: 0.5rem;
cursor: pointer;
transition: var(--transition);
}
.shadcn-likert-input:checked + .shadcn-likert-button {
background-color: var(--primary);
border-color: var(--primary);
transform: scale(1.1);
box-shadow: 0 0 0 3px rgba(110, 86, 207, 0.2);
}
.shadcn-likert-input:focus + .shadcn-likert-button {
box-shadow: 0 0 0 3px rgba(110, 86, 207, 0.2);
}
.shadcn-likert-input:hover + .shadcn-likert-button {
border-color: var(--primary);
}
.shadcn-likert-label {
font-size: 0.75rem;
color: var(--muted-foreground);
margin-top: 0.25rem;
text-align: center;
}
.shadcn-likert-input:checked ~ .shadcn-likert-label {
color: var(--primary);
font-weight: 500;
}
.shadcn-likert-bad-text {
display: flex;
align-items: center;
margin-top: 0.75rem;
padding: 0.5rem 0;
}
.shadcn-likert-bad-text-label {
margin-left: 0.5rem;
font-size: 0.875rem;
color: var(--text-color);
word-wrap: break-word;
overflow-wrap: break-word;
flex: 1;
}
fieldset[schema] {
width: fit-content;
max-width: 100%;
overflow: visible;
}
.ai-help {
width: 9.375rem;
height: 2.063rem;
border: 1px solid #E5E5EA;
border-radius: 18px;
position: absolute;
top: -0.75rem;
right: -0.05rem;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
}
.ai-help-word {
font-size: 1rem;
margin: 0;
}
.hint {
cursor: pointer;
}
.hint:hover {
color: #6E56CF;
}
.tooltip {
position: absolute;
top: 120%;
left: 50%;
transform: translateX(-50%);
background-color: white;
padding: 1rem;
border-radius: .5rem;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
min-width: 20rem;
max-width: 24rem;
overflow-wrap: break-word;
word-wrap: break-word;
white-space: normal;
max-height: 15rem;
overflow: auto;
}
.tooltip.active {
opacity: 1;
visibility: visible;
}
.reasoning {
font-weight: bold;
}
.tooltip-text{
margin: 0;
}
</style>
<form id="coherence" class="annotation-form likert shadcn-likert-container" action="javascript:void(0)" data-annotation-id="2">
<div class="ai-help">
<h3 class="ai-help-word"><span class="hint">Hint</span> | <span>Keyword</span></h3>
<div class="tooltip">
<p class="tooltip-text">
<span class="reasoning">Reasoning:</span> {ai}
</p>
</div>
</div>
<fieldset schema="coherence" style="border: none; padding: 0; margin: 0; width: auto; min-width: fit-content;">
<legend class="shadcn-likert-title">Coherence</legend>
<div class="shadcn-likert-scale" style="max-width: min(100%, calc(300px + 5 * 40px + 250px));">
<div class="shadcn-likert-endpoint">Not Coherent</div>
<div class="shadcn-likert-options">
<div class="shadcn-likert-track"></div>
<div class="shadcn-likert-option">
<input class="coherence shadcn-likert-input"
type="radio"
id="coherence:::1"
name="coherence:::1"
value="1"
schema="coherence"
label_name="1"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="coherence:::1"></label>
<span class="shadcn-likert-label">1</span>
</div>
<div class="shadcn-likert-option">
<input class="coherence shadcn-likert-input"
type="radio"
id="coherence:::2"
name="coherence:::2"
value="2"
schema="coherence"
label_name="2"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="coherence:::2"></label>
<span class="shadcn-likert-label">2</span>
</div>
<div class="shadcn-likert-option">
<input class="coherence shadcn-likert-input"
type="radio"
id="coherence:::3"
name="coherence:::3"
value="3"
schema="coherence"
label_name="3"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="coherence:::3"></label>
<span class="shadcn-likert-label">3</span>
</div>
<div class="shadcn-likert-option">
<input class="coherence shadcn-likert-input"
type="radio"
id="coherence:::4"
name="coherence:::4"
value="4"
schema="coherence"
label_name="4"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="coherence:::4"></label>
<span class="shadcn-likert-label">4</span>
</div>
<div class="shadcn-likert-option">
<input class="coherence shadcn-likert-input"
type="radio"
id="coherence:::5"
name="coherence:::5"
value="5"
schema="coherence"
label_name="5"
selection_constraint="single"
validation="required"
onclick="onlyOne(this);registerAnnotation(this);">
<label class="shadcn-likert-button" for="coherence:::5"></label>
<span class="shadcn-likert-label">5</span>
</div>
</div>
<div class="shadcn-likert-endpoint">Super Coherent</div>
</div>
</fieldset></form>
</div>
</div>
</div>
<!--- LAYOUT GOES ABOVE --->
</div>
</div>
<!-- Test -->
<!-- <div id="task_layout" class="shadcn-card mb-4">
<div class="shadcn-card-content">
<strong>AI Label Suggestion</strong>
<div>{{ ai }}</div>
</div>
</div> -->
<!-- Test -->
<div class="my-4">
<!-- Reorganized button layout with 3 columns -->
<div class="d-flex justify-content-between align-items-center">
<!-- Statistics button (left) -->
<div>
<a
role="button"
href="#"
class="shadcn-button shadcn-button-outline openbtn"
target_id="Statpanel"
width="475px"
onclick="openNav(this.getAttribute('target_id'),this.getAttribute('width'))"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-bar-chart-line me-1"
viewBox="0 0 16 16"
>
<path
d="M11 2a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12h.5a.5.5 0 0 1 0 1H.5a.5.5 0 0 1 0-1H1v-3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3h1V7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7h1V2zm1 12h2V2h-2v12zm-3 0V7H7v7h2zm-5 0v-3H2v3h2z"
/>
</svg>
Statistics
</a>
</div>
<!-- Previous/Next buttons (center) -->
<div>
<a
class="shadcn-button shadcn-button-outline me-2"
href="#"
role="button"
onclick="click_to_prev()"
style="vertical-align: middle"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-arrow-left"
viewBox="0 0 16 16"
style="
display: inline-block;
vertical-align: middle;
margin-right: 0.25rem;
"
>
<path
fill-rule="evenodd"
d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"
/>
</svg>
<span style="display: inline-block; vertical-align: middle"
>Previous</span
>
</a>
<a
class="shadcn-button shadcn-button-primary"
href="#"
role="button"
onclick="click_to_next()"
style="vertical-align: middle"
>
<span style="display: inline-block; vertical-align: middle"
>Next</span
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-arrow-right"
viewBox="0 0 16 16"
style="
display: inline-block;
vertical-align: middle;
margin-left: 0.25rem;
"
>
<path
fill-rule="evenodd"
d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"
/>
</svg>
</a>
</div>
<!-- Help button (right) -->
<div>
<a
role="button"
href="#"
class="shadcn-button shadcn-button-outline openbtn"
target_id="mySidepanel"
onclick="openNav(this.getAttribute('target_id'))"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-question-circle me-1"
viewBox="0 0 16 16"
>
<path
d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"
/>
<path
d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"
/>
</svg>
Help
</a>
</div>
</div>
</div>
<hr style="color: var(--border)" />
<footer
class="py-2 text-center"
style="font-size: 0.75rem; color: var(--muted-foreground)"
>
Copyright &copy; 2025
<a
href="https://blablablab.si.umich.edu/"
style="color: var(--primary); text-decoration: none"
>Blablablab</a
>
<span class="mx-2">|</span>
<a
href="https://github.com/davidjurgens/potato"
style="color: var(--primary); text-decoration: none"
>Chat with us on GitHub</a
>
<span class="mx-2">|</span>
<a
href="https://github.com/davidjurgens/potato#cite-us"
style="color: var(--primary); text-decoration: none"
>Cite Us</a
>
</footer>
</div>
<script>
/* Set the width of the sidebar to 250px (show it) */
function openNav(target_id, width = "350px") {
document.getElementById(target_id).style.width = width;
}
/* Set the width of the sidebar to 0 (hide it) */
function closeNav(target_id) {
document.getElementById(target_id).style.width = "0";
}
function closeNav2(target_id) {
// document.getElementById(target_id).style.height = "20px";
console.error(document.getElementById(target_id).style.display);
if (
document.getElementById(target_id).style.display == "block" ||
document.getElementById(target_id).style.display == ""
) {
document.getElementById(target_id).style.display = "none";
localStorage.setItem("show_instructions", "false");
} else {
document.getElementById(target_id).style.display = "block";
localStorage.setItem("show_instructions", "true");
}
}
/* Keep the instructions hidden/shown across instance transitions based on
what the user had selected */
window.addEventListener("load", function () {
var show = localStorage.getItem("show_instructions");
if (document.getElementById("instructions")) {
if (show === "true") {
document.getElementById("instructions").style.display = "block";
} else {
document.getElementById("instructions").style.display = "none";
}
}
});
function watch_text_boxes() {
let timer;
const waitTime = 1000;
var text_boxes = document.querySelectorAll("input[type=text]");
for (var i = 0; i < text_boxes.length; i++) {
//console.log('adding event listener to text box');
//console.log(text_boxes[i]);
text_boxes[i].addEventListener("input", function (event) {
console.log("text box input event");
clearTimeout(timer);
timer = setTimeout(() => {
registerTextAnnotation(event.target);
}, waitTime);
});
}
var text_areas = document.querySelectorAll("textarea");
//console.log('num text_areas: ' + text_areas.length);
for (var i = 0; i < text_areas.length; i++) {
//console.log('adding event listener to text area');
//console.dir(text_areas[i]);
text_areas[i].addEventListener("input", function (event) {
//console.log('text area input event')
clearTimeout(timer);
timer = setTimeout(() => {
registerTextAnnotation(event.target);
}, waitTime);
});
}
}
watch_text_boxes();
</script>
</body>
</html>