Web Interface ok

This commit is contained in:
Magnus Bender 2024-10-29 15:22:32 +01:00
parent 86adf41308
commit 04ccd488f8
Signed by: bender
GPG Key ID: 5149A211831F2BD7
10 changed files with 275 additions and 64 deletions

View File

@ -148,7 +148,7 @@ class DB():
sender=row['sender'],
recipient=row['recipient'],
time=int(datetime.strptime(row['time'], self._DB_TIME_FORMAT).timestamp()),
message=AgentMessage.model_validate_json(row['json']),
message=AgentMessage.model_validate_json(row['json'], context={"require_file_exists":False}),
processed=row['processed']
)

View File

@ -18,7 +18,8 @@ from fastapi.templating import Jinja2Templates
from ums.management.db import DB
from ums.utils import AgentMessage
from ums.utils import (AgentMessage, RiddleDataType,
list_shared_data, list_shared_schema, SHARE_PATH)
class Interface():
@ -97,5 +98,9 @@ class Interface():
def new(request: Request):
return self.template.TemplateResponse(
'new.html',
{"request" : request, "AgentMessage" : AgentMessage}
{"request" : request,
"AgentMessage" : AgentMessage, "RiddleDataType": RiddleDataType,
"shared_data" : list_shared_data(), "shared_schema" : list_shared_schema(),
"SHARE_PATH" : SHARE_PATH
}
)

View File

@ -22,4 +22,6 @@ from ums.utils.types import (
from ums.utils.const import *
from ums.utils.request import ManagementRequest
from ums.utils.request import ManagementRequest
from ums.utils.functions import list_shared_data, list_shared_schema

46
ums/utils/functions.py Normal file
View File

@ -0,0 +1,46 @@
import os
from typing import List, Callable
from ums.utils.const import SHARE_PATH
def list_path(path:str) -> List[str]:
if os.path.isdir(path):
items = []
for item in os.listdir(path):
full = os.path.join(path, item)
if os.path.isdir(full):
items.extend(list_path(full))
elif os.path.isfile(full):
items.append(full)
return items
else:
return []
def list_shared(filter:Callable=lambda p,n,e: True) -> List[str]:
files = []
for f in list_path(SHARE_PATH):
r = f[len(SHARE_PATH)+1:]
if r[0] == '.' or '/.' in r:
# hidden files
continue
if '/' in r and '.' in r:
path, name, ending = r[:r.rfind('/')], r[r.rfind('/')+1:r.rfind('.')], r[r.rfind('.')+1:]
elif '/' in r:
path, name, ending = r[:r.rfind('/')], r[r.rfind('/')+1:], ""
elif '.' in r:
path, name, ending = "", r[:r.rfind('.')], r[r.rfind('.')+1:]
else:
path, name, ending = "", r, ""
if filter(path, name, ending):
files.append(r)
return files
def list_shared_data():
return list_shared(lambda p,n,e: e != "json")
def list_shared_schema():
return list_shared(lambda p,n,e: e == "json")

View File

@ -92,11 +92,15 @@ import os
from enum import Enum
from typing import List
from typing import List, Any
from typing_extensions import Annotated
from pydantic import BaseModel
from pydantic.functional_validators import AfterValidator
from pydantic import (
BaseModel,
ValidationError, ValidationInfo,
ValidatorFunctionWrapHandler
)
from pydantic.functional_validators import WrapValidator, AfterValidator
from ums.utils.const import SHARE_PATH
@ -127,7 +131,22 @@ def _check_data_file(file_name:str) -> str:
return file_name
def _ignore_file_missing(
v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
) -> str:
try:
return handler(v)
except ValidationError:
if not info.context is None and \
"require_file_exists" in info.context and \
info.context["require_file_exists"] == False and \
isinstance(v, str):
return "missing:{}".format(v)
else:
raise
class RiddleData(RiddleInformation):
"""
A data item to be used to solve the riddle
"""
@ -137,7 +156,7 @@ class RiddleData(RiddleInformation):
The type of the data item.
"""
file_plain: Annotated[str, AfterValidator(_check_data_file)]
file_plain: Annotated[str, AfterValidator(_check_data_file), WrapValidator(_ignore_file_missing)]
"""
The plain file (as path to file system) without any processing.
@ -145,7 +164,7 @@ class RiddleData(RiddleInformation):
The file must exist.
"""
file_extracted: Annotated[str, AfterValidator(_check_data_file)] | None = None
file_extracted: Annotated[str, AfterValidator(_check_data_file), WrapValidator(_ignore_file_missing)] | None = None
"""
The processed files (as path to file system), i.e., a schematic file containing all extracted informations.
@ -153,6 +172,8 @@ class RiddleData(RiddleInformation):
The file must exist.
"""
class RiddleSolution(RiddleInformation):
"""
A solution of a riddle.

117
web/public/static/new.js Normal file
View File

@ -0,0 +1,117 @@
var fileLast = 0;
function renderFile(cnt, type="", file_extracted="", file_plain=""){
var template = $("#filesTemplate").html();
template = template.replaceAll('{cnt}', cnt)
.replaceAll('{file_extracted}', file_extracted)
.replaceAll('{file_extracted}', file_extracted)
.replaceAll('{file_plain}', file_plain);
['text', 'image', 'audio'].forEach((t)=>{
template = template.replaceAll('{'+t+'_selected}', t === type ? 'selected' : '' );
});
$("#filesRender").append(template);
$("button#removeFile"+cnt).click( () => {
$("#filesRow" + cnt).remove();
update_json_message()
});
fileLast = cnt;
}
$("#addFile").click(() => renderFile(++fileLast));
function update_json_message(){
let message = JSON.parse(basic_message);
var store_values = {}
$(".message-attribute").each((_,v)=>{
let el = $(v);
let name = el.attr('name');
let val = el.attr("type") == "checkbox" ? el.prop('checked') : el.val();
// optional fields (empty => null)
if( name.endsWith("file_extracted") && val.length == 0 ){
val = null;
}
store_values[name] = val;
let curr_msg = message;
let last_ob = {};
let last_name = "";
name.split('.').forEach((e)=>{
last_ob = curr_msg;
last_name = e;
if( !curr_msg.hasOwnProperty(e) ){
curr_msg[e] = {}
}
curr_msg = curr_msg[e];
});
last_ob[last_name] = val;
});
localStorage.setItem("new_riddle", JSON.stringify(store_values))
// fix array for data
if(message.hasOwnProperty('data')){
var data_items = message['data'];
message['data'] = [];
Object.keys(data_items).forEach((v)=>{
if( v !== null){
message['data'].push(data_items[v]);
}
});
}
$("#message_content").val(JSON.stringify(message, null, 2));
}
function load_last_values(){
if(localStorage.getItem("new_riddle") !== null){
var items = JSON.parse(localStorage.getItem("new_riddle"))
console.log(items)
Object.keys(items).forEach((k)=>{
// if data, create the file select for this id
if(k.startsWith('data.') && $("[name='"+k+"']").length == 0){
renderFile(parseInt(k.substring(5, k.indexOf('.',5))))
}
let el = $("[name='"+k+"']");
if(el.attr("type") == "checkbox"){
el.prop('checked', items[k]);
}
else{
el.val(items[k]);
}
});
update_json_message()
}
}
load_last_values();
$( document ).on( "change", ".message-attribute", update_json_message)
$( document ).on( "click", ".message-attribute[type=checkbox]", update_json_message)
function send_json_message(json_str){
$.ajax(
"/message",
{
contentType : 'application/json',
type : 'POST',
data: json_str,
},
).then((d) => {
$("#message_sent .modal-title").text("Message Sent");
$("#message_sent .modal-body").html('<pre>'+JSON.stringify(d, null, 2)+'</pre>');
new bootstrap.Modal('#message_sent').show();
}).fail((d)=>{
$("#message_sent .modal-title").text("Error Sending Message");
$("#message_sent .modal-body").html('<pre>'+JSON.stringify(d, null, 2)+'</pre>');
new bootstrap.Modal('#message_sent').show();
});
}
$("#send_message").click(
() => send_json_message($("#message_content").val())
);

View File

@ -1,5 +1,3 @@
function pagination_link(name, value){
let link_args = db_args;
if (name && value){
@ -35,53 +33,6 @@ if(sessionStorage.hasOwnProperty('auto_refresh') && sessionStorage.getItem('auto
enable_auto_refresh()
}
function update_json_message(){
let message = JSON.parse(basic_message);
var store_values = {}
$(".message-attribute").each((_,v)=>{
let el = $(v);
let name = el.attr('name');
let val = el.attr("type") == "checkbox" ? el.prop('checked') : el.val();
store_values[name] = val;
let curr_msg = message;
let last_ob = {};
let last_name = "";
name.split('.').forEach((e)=>{
last_ob = curr_msg;
last_name = e;
curr_msg = curr_msg[e];
});
last_ob[last_name] = val;
});
localStorage.setItem("new_riddle", JSON.stringify(store_values))
$("#message_content").val(JSON.stringify(message, null, 2));
}
function load_last_values(){
if(localStorage.getItem("new_riddle") !== null){
var items = JSON.parse(localStorage.getItem("new_riddle"))
Object.keys(items).forEach((k)=>{
let el = $("[name='"+k+"']");
if(el.attr("type") == "checkbox"){
el.prop('checked', items[k]);
}
else{
el.val(items[k]);
}
});
update_json_message()
}
}
if(typeof basic_message != 'undefined'){
load_last_values();
}
$(".message-attribute").change(update_json_message);
$(".message-attribute[type=checkbox]").click(update_json_message);
function send_json_message(json_str){
$.ajax(
"/message",
@ -94,12 +45,12 @@ function send_json_message(json_str){
$("#message_sent .modal-title").text("Message Sent");
$("#message_sent .modal-body").html('<pre>'+JSON.stringify(d, null, 2)+'</pre>');
new bootstrap.Modal('#message_sent').show();
}).fail((d)=>{
$("#message_sent .modal-title").text("Error Sending Message");
$("#message_sent .modal-body").html('<pre>'+JSON.stringify(d, null, 2)+'</pre>');
new bootstrap.Modal('#message_sent').show();
});
}
$("#send_message").click(
() => send_json_message($("#message_content").val())
);
$(".send_message_again").click(
(e) => send_json_message( $("pre#row_message_raw_"+$(e.target).attr('idx')).text() )
);
);

View File

@ -43,6 +43,7 @@
{% block maincontent %}
{% endblock %}
</div>
<script src="/static/main.js"></script>
{% block morefoot %}
{% endblock %}
</body>
</html>

View File

@ -33,6 +33,13 @@
<textarea class="form-control message-attribute" name="riddle.context" id="message_context" rows="2"></textarea>
</div>
<div class="mb-3">
<label for="message_context" class="form-label">Riddle Context</label>
<textarea class="form-control message-attribute" name="riddle.context" id="message_context" rows="2"></textarea>
</div>
<h3>Steps</h3>
<div class="mb-3">
<div class="form-check form-check-inline">
<input class="form-check-input message-attribute" name="status.extract.required" type="checkbox" id="message_extract" value="true" checked>
@ -48,6 +55,61 @@
</div>
</div>
<h3>Files</h3>
<p>
Manually add files to the shared directory and then select them here to assign them to a riddle.
Shared directory is <code>{{SHARE_PATH}}</code> which should be linked to the host using docker to, e.g., <code>./data/share</code>.
</p>
<datalist id="dataFiles">
{% for f in shared_data %}
<option value="{{f}}">
{% endfor %}
</datalist>
<datalist id="schemaFiles">
{% for f in shared_schema %}
<option value="{{f}}">
{% endfor %}
</datalist>
<template id="filesTemplate">
<div class="mb-3 row" id="filesRow{cnt}">
<div class="col col-sm-1">
{cnt}
</div>
<div class="col">
<label for="fileType{cnt}" class="form-label">File Type</label>
<select id="fileType{cnt}" name="data.{cnt}.type" class="form-select message-attribute">
<option>Select Type</option>
{% for t in RiddleDataType %}
<option value="{{t.value}}" { {{- t.value -}} _selected}>{{t.name}}</option>
{% endfor %}
</select>
</div>
<div class="col">
<label for="inputFile{cnt}" class="form-label">Input File</label>
<input class="form-control message-attribute" name="data.{cnt}.file_plain"
list="dataFiles" id="inputFile{cnt}" value="{file_plain}" placeholder="Type to search files">
</div>
<div class="col">
<label for="schemaFile{cnt}" class="form-label">Schema File</label>
<input class="form-control message-attribute" name="data.{cnt}.file_extracted"
list="schemaFiles" id="schemaFile{cnt}" value="{file_extracted}" placeholder="Type to search files">
</div>
<div class="col col-sm-1">
<button type="button" id="removeFile{cnt}" class="btn btn-danger">Remove</button>
</div>
</div>
</template>
<div id="filesRender">
</div>
<div class="mb-3 row">
<div class="col col-sm-1">
<button type="button" id="addFile" class="btn btn-secondary">Add</button>
</div>
</div>
<h2>JSON Representation</h2>
<div class="mb-3">
@ -67,4 +129,7 @@
<script>
const basic_message = '{{ AgentMessage(id="",riddle={"context":"","question":""}).model_dump_json()|safe }}';
</script>
{% endblock %}
{% block morefoot %}
<script src="/static/new.js"></script>
{% endblock %}

View File

@ -138,4 +138,7 @@
const db_args = JSON.parse('{{ db_args|tojson }}');
const db_total = {{ db.len(**db_args) }};
</script>
{% endblock %}
{% block morefoot %}
<script src="/static/table.js"></script>
{% endblock %}