-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
229 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
|
||
<div class="content"> | ||
<h2 class="title is-size-4">Contexts</h2> | ||
<p style="max-width: 50em">Contexts provide additional information to the LLM for each query a student makes. You can have a single default context that is always used, or you can create separate contexts for individual assignments or modules. If multiple contexts are available, students will be able to select from them when making queries.</p> | ||
{% if contexts | length == 0 %} | ||
<p class="has-text-danger">While not strictly required, we recommend defining at least one context to specify the language(s), frameworks, and/or libraries in use in this class.</p> | ||
{% endif %} | ||
{# Link to the 'contexts.md' docs page if it exists #} | ||
{% if 'contexts' in docs_pages %} | ||
<p>See the <a href="{{ url_for('docs.page', name='contexts') }}">contexts documentation</a> for more information and suggestions.</p> | ||
{% endif %} | ||
<script type="text/javascript"> | ||
document.addEventListener('alpine:init', () => { | ||
Alpine.data('reorderable', () => ({ | ||
items: {{ contexts | tojson }}, | ||
drag_index: null, | ||
|
||
dragenter(index) { | ||
if (this.drag_index === null) { return } | ||
if (index === this.drag_index) { return } | ||
// reorder the list, placing the dragged item at this index and shifting others | ||
let new_items = []; | ||
const drag_item = this.items[this.drag_index]; | ||
this.items.forEach((el, i) => { | ||
if (i === this.drag_index) { return } | ||
else if (i === index && i < this.drag_index) { | ||
new_items.push(drag_item); | ||
new_items.push(el); | ||
} | ||
else if (i === index && i > this.drag_index) { | ||
new_items.push(el); | ||
new_items.push(drag_item); | ||
} | ||
else { | ||
new_items.push(el); | ||
} | ||
}); | ||
this.items = new_items; // update w/ newly ordered list | ||
this.drag_index = index; // this new index is now the one we're dragging | ||
}, | ||
stop_drag() { | ||
this.drag_index = null; | ||
// post updated ordering to save in DB | ||
fetch("{{ url_for('context_config.update_order') }}", { | ||
method: "POST", | ||
headers: { "Content-Type": "application/json" }, | ||
body: JSON.stringify(this.items.map(item => item.id)), | ||
}); | ||
}, | ||
})); | ||
Alpine.data('available_dropdown', () => ({ | ||
showDropdown: false, | ||
showModal: false, | ||
newDate: null, | ||
|
||
// throughout, 'this.ctx' refers to a ctx object from the for loop in the parent Alpine scope ('reorderable') | ||
init() { | ||
this.$watch('ctx.available', newval => { | ||
// post updated date to save in DB | ||
fetch('{{ url_for('context_config.update_available') }}', { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: JSON.stringify({'ctx_id': this.ctx.id, 'available': this.ctx.available}), | ||
}); | ||
}); | ||
}, | ||
get status() { return this.ctx.available === '9999-12-31' ? 'Hidden' : this.datePassed(this.ctx.available) ? 'Now' : 'Scheduled'; }, | ||
get min_date_str() { const min_date = new Date(); min_date.setDate(min_date.getDate() + 1); return min_date.toISOString().split('T')[0]; }, | ||
datePassed(date) { | ||
const now_datetime = new Date(); // automatically UTC | ||
const target_date = new Date(date); // automatically UTC | ||
target_date.setHours(target_date.getHours() - 12); // UTC-12 for anywhere on Earth | ||
return now_datetime >= target_date; | ||
}, | ||
formatDate(date) { | ||
// Add 'T00:00' to force parsing as local time so UTC-local shift doesn't change date | ||
return new Date(date + 'T00:00').toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); | ||
}, | ||
chooseScheduled() { | ||
if (this.status === 'Scheduled') { | ||
this.newDate = this.ctx.available; | ||
} | ||
else { | ||
// get today in YYYY-MM-DD format as starting point for date picker | ||
this.newDate = this.min_date_str; | ||
} | ||
this.showModal = true; | ||
}, | ||
})); | ||
}); | ||
</script> | ||
<table class="table is-hoverable is-narrow" x-data="reorderable"> | ||
<thead> | ||
<tr> | ||
<th class="p-0 has-text-centered has-text-grey" title="Reorder"> | ||
<svg aria-hidden="true" class="icon is-small" style="vertical-align: bottom;"><use href="#svg_arrow_up_down" /></svg> | ||
</th> | ||
<th>Name</th> | ||
<th class="has-text-centered">Available</th> | ||
<th class="has-text-centered has-text-grey"><small>actions</small></th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<template x-for="(ctx, index) in items" x-bind:key="ctx.name"> | ||
<tr x-bind:draggable="(drag_index == index)" @dragenter="dragenter(index)" @dragover.prevent @dragend="stop_drag" x-bind:style="(drag_index == index) && {background: '#fc9'}"> | ||
<td style="cursor: move; vertical-align: middle; text-align: center;" @mousedown="drag_index=index" @mouseup="stop_drag" title="drag to reorder"> | ||
<svg aria-hidden="true" class="icon is-small mt-1"><use href="#svg_grip" /></svg> | ||
</td> | ||
<td style="vertical-align: middle;"> | ||
<a class="is-underlined hover-show-icon has-text-link-dark" x-bind:title="`edit '${ctx.name}'`" x-bind:href="'{{ url_for('context_config.context_form') }}/' + ctx.id" x-text="ctx.name"></a> | ||
<svg aria-hidden="true" class="icon is-small has-text-link-dark"><use href="#svg_pencil" /></svg> | ||
</td> | ||
<td style="text-align: center; vertical-align: middle;"> | ||
<div x-data="available_dropdown"> | ||
<div class="dropdown" x-bind:class="{'is-active': showDropdown}"> | ||
<div class="dropdown-trigger"> | ||
<button class="button is-small is-rounded" style="white-space: normal;" | ||
x-bind:class="{ | ||
'is-success': status === 'Now', | ||
'is-warning': status === 'Scheduled', | ||
'is-danger': status === 'Hidden', | ||
}" | ||
@click="showDropdown = !showDropdown" | ||
@click.outside="showDropdown = false" | ||
aria-haspopup="true" x-bind:aria-controls="`dropdown-menu${index}`"> | ||
<span x-text="status === 'Scheduled' ? `Scheduled: ${formatDate(ctx.available)}` : status"></span> | ||
<svg aria-hidden="true" class="icon is-small"><use href="#svg_chevron_down" /></svg> | ||
</button> | ||
</div> | ||
<div class="dropdown-menu" x-bind:id="`dropdown-menu${index}`" role="menu"> | ||
<div class="dropdown-content has-text-left"> | ||
<a href="#" class="dropdown-item" @click.prevent="ctx.available = '0001-01-01'">Now</a> | ||
<a href="#" class="dropdown-item" @click.prevent="chooseScheduled">Scheduled</a> | ||
<a href="#" class="dropdown-item" @click.prevent="ctx.available = '9999-12-31'">Hidden</a> | ||
</div> | ||
</div> | ||
</div> | ||
<div class="modal" x-bind:class="{ 'is-active': showModal }" @keydown.escape.window="showModal = false"> | ||
<div class="modal-background" @click="showModal = false"></div> | ||
<div class="modal-content"> | ||
<div class="box has-text-left"> | ||
<h3 class="title is-4">Schedule '<span x-text="ctx.name"></span>'</h3> | ||
<p>When scheduled for a certain date, a context becomes available when that date is reached anywhere on Earth (UTC+12).</p> | ||
<form @submit.prevent="ctx.available = newDate; showModal = false"> | ||
<div class="field is-grouped" style="justify-content: center;"> | ||
<p class="control"> | ||
<input type="date" class="input is-large" x-model="newDate" x-bind:min="min_date_str"> | ||
</p> | ||
<p class="control"> | ||
<button type="submit" class="button is-large is-link">OK</button> | ||
</p> | ||
<p class="control"> | ||
<button type="submit" @click.prevent="showModal = false" class="button is-large">Cancel</button> | ||
</p> | ||
</div> | ||
</form> | ||
</div> | ||
</div> | ||
<button type="button" class="modal-close is-large" aria-label="close" @click="showModal = false"></button> | ||
</div> | ||
</div> | ||
</td> | ||
<td class="has-text-centered"> | ||
<form method="post"> | ||
<span x-data="{ | ||
link_URL: '{{ url_for('helper.help_form', class_id=auth['class_id'], ctx_name='__replace__', _external=True) }}'.replace('__replace__', encodeURIComponent(ctx.name)), | ||
copied: false, | ||
showLinkModal: false, | ||
showModal() { | ||
this.copied = false; | ||
this.showLinkModal = true; | ||
}, | ||
copy_url() { | ||
navigator.clipboard.writeText(this.link_URL); | ||
this.copied = true; | ||
}, | ||
}"> | ||
<button x-bind:title="`link to '${ctx.name}'`" class="button is-white is-small has-text-grey" type="button" @click="showModal"> | ||
<svg aria-hidden="true" class="icon is-small"><use href="#svg_link" /></svg> | ||
</button> | ||
<div class="modal" x-bind:class="{'is-active': showLinkModal}" @keydown.escape.window="showLinkModal = false;"> | ||
<div class="modal-background" @click="showLinkModal = false;"></div> | ||
<div class="modal-content"> | ||
<div class="box has-text-left"> | ||
<h3 class="title is-4">Link to '<span x-text="ctx.name"></span>'</h3> | ||
<p>This link will take your students directly to the request page with the '<span x-text="ctx.name"></span>' context pre-selected.</p> | ||
<div style="display: flex; flex-wrap: wrap; gap: 1em;"> | ||
<div class="control"> | ||
<span class="input is-size-5" style="max-width: 100%; overflow-x: auto;" x-text="link_URL"></span> | ||
</div> | ||
<div class="control"> | ||
<button type="button" class="button icon-text is-size-5" x-bind:class="copied ? 'is-success' : 'is-link'" @click="copy_url"> | ||
<svg aria-hidden="true" class="icon is-right"><use href="#svg_copy" /></svg> | ||
<span x-text="copied ? 'copied' : 'copy'"></span> | ||
</button> | ||
</div> | ||
</div> | ||
<div class="mt-4"> | ||
<p>The link will let students access this context even if it is currently hidden.</p> | ||
<p class="has-text-danger-dark">Students must have joined this class before they use the link.</p> | ||
<p class="has-text-danger-dark">If the class connects using LTI, students must log in via LTI before they use the link.</p> | ||
</div> | ||
</div> | ||
</div> | ||
<button type="button" class="modal-close is-large" aria-label="close" @click="showLinkModal = false;"></button> | ||
</div> | ||
</span> | ||
<button x-bind:title="`copy '${ctx.name}'`" class="button is-white is-small has-text-grey" type="submit" x-bind:formaction="'{{ url_for('context_config.copy_context') }}/' + ctx.id"> | ||
<svg aria-hidden="true" class="icon is-small"><use href="#svg_copy" /></svg> | ||
</button> | ||
<button x-bind:title="`delete '${ctx.name}'`" class="button is-white is-small has-text-danger" type="submit" x-bind:formaction="'{{ url_for('context_config.delete_context') }}/' + ctx.id" @click="$event => confirm('Are you sure you want to delete \'' + ctx.name + '\'?') || $event.preventDefault()"> | ||
<svg aria-hidden="true" class="icon is-small"><use href="#svg_trash" /></svg> | ||
</button> | ||
</form> | ||
</td> | ||
</tr> | ||
</template> | ||
<tr x-show="items.length == 0"><td colspan=4 class="has-text-centered"><i>No contexts defined.</i></td></tr> | ||
</tbody> | ||
</table> | ||
<div colspan=4 class="has-text-centered"> | ||
<a class="button is-light is-link is-small" href="{{ url_for('context_config.new_context_form') }}"> | ||
<span class="icon"> | ||
<svg aria-hidden="true"><use href="#svg_plus" /></svg> | ||
</span> | ||
<span>Create new context</span> | ||
</a> | ||
</div> | ||
</div> |