Use query parameters for year, day and mode. Handle file stream message.
This commit is contained in:
parent
cb69fea7ac
commit
eb64edb22f
@ -6,7 +6,8 @@ import subprocess
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal, TypeAlias
|
from typing import Any, Literal, TypeAlias
|
||||||
|
|
||||||
from fastapi import FastAPI, Form
|
from fastapi import FastAPI, Form, HTTPException
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
from sse_starlette import EventSourceResponse
|
from sse_starlette import EventSourceResponse
|
||||||
|
|
||||||
DataMode: TypeAlias = Literal["holt59", "tests"]
|
DataMode: TypeAlias = Literal["holt59", "tests"]
|
||||||
@ -14,6 +15,7 @@ DataMode: TypeAlias = Literal["holt59", "tests"]
|
|||||||
LOGGER = logging.getLogger("uvicorn.info")
|
LOGGER = logging.getLogger("uvicorn.info")
|
||||||
|
|
||||||
GIT_AOC_PATH = Path("/code")
|
GIT_AOC_PATH = Path("/code")
|
||||||
|
AOC_FILES_PATH = Path("/files")
|
||||||
|
|
||||||
|
|
||||||
def update_repository():
|
def update_repository():
|
||||||
@ -54,6 +56,16 @@ async def data(year: int, day: int, mode: DataMode = "tests"):
|
|||||||
return {"content": read_test_file(year, day, mode)}
|
return {"content": read_test_file(year, day, mode)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/file")
|
||||||
|
async def file(name: str):
|
||||||
|
path = AOC_FILES_PATH.joinpath(name).resolve()
|
||||||
|
|
||||||
|
if path.parent != AOC_FILES_PATH:
|
||||||
|
raise HTTPException(403)
|
||||||
|
|
||||||
|
return FileResponse(path)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/submit-sse")
|
@app.post("/submit-sse")
|
||||||
async def submit_sse(year: int = Form(), day: int = Form(), input: str = Form()):
|
async def submit_sse(year: int = Form(), day: int = Form(), input: str = Form()):
|
||||||
data = input.rstrip().replace("\r\n", "\n")
|
data = input.rstrip().replace("\r\n", "\n")
|
||||||
@ -62,6 +74,8 @@ async def submit_sse(year: int = Form(), day: int = Form(), input: str = Form())
|
|||||||
"holt59-aoc",
|
"holt59-aoc",
|
||||||
"--stdin",
|
"--stdin",
|
||||||
"--api",
|
"--api",
|
||||||
|
"--output",
|
||||||
|
AOC_FILES_PATH,
|
||||||
"--year",
|
"--year",
|
||||||
str(year),
|
str(year),
|
||||||
str(day),
|
str(day),
|
||||||
|
@ -13,13 +13,16 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@viz-js/viz": "^3.11.0",
|
||||||
|
"ansi-up": "^1.0.0",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
"sse.js": "^2.5.0",
|
"sse.js": "^2.5.0",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node22": "^22.0.0",
|
"@tsconfig/node22": "^22.0.0",
|
||||||
|
@ -1,134 +1,3 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
import ResultVue from '@/components/ResultVue.vue'
|
|
||||||
|
|
||||||
import { loadData } from '@/api';
|
|
||||||
import { range } from 'lodash';
|
|
||||||
import { onMounted, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
const inAocPeriod = (date: DateTime) => {
|
|
||||||
return !(date.day > 25 || date.month != 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentInput = ref("");
|
|
||||||
const showLogs = ref(false);
|
|
||||||
|
|
||||||
const today = DateTime.now();
|
|
||||||
const maxYear = today.month == 12 ? today.year : today.year - 1;
|
|
||||||
|
|
||||||
const initialDay = ref(inAocPeriod(today) ? today.day : 1);
|
|
||||||
const initialYear = ref(maxYear);
|
|
||||||
|
|
||||||
const currentDay = ref(initialDay.value);
|
|
||||||
const currentYear = ref(maxYear);
|
|
||||||
|
|
||||||
const years = range(2015, maxYear + 1);
|
|
||||||
const days = range(1, 26);
|
|
||||||
|
|
||||||
// handle current day clicked
|
|
||||||
const todayClicked = () => {
|
|
||||||
const today = DateTime.now();
|
|
||||||
|
|
||||||
initialDay.value = inAocPeriod(today) ? today.day : 1;
|
|
||||||
initialYear.value = today.month == 12 ? today.year : today.year - 1;
|
|
||||||
|
|
||||||
currentDay.value = initialDay.value;
|
|
||||||
currentYear.value = initialYear.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle load data + load data of current day on startup
|
|
||||||
const dataLoading = ref(false);
|
|
||||||
const lastInputLoaded = ref("");
|
|
||||||
const lastModeLoaded = ref<"holt59" | "tests">("tests");
|
|
||||||
|
|
||||||
const loadDataClicked = async (mode: "holt59" | "tests" = "tests") => {
|
|
||||||
dataLoading.value = true;
|
|
||||||
currentInput.value = await loadData(currentYear.value, currentDay.value, mode);
|
|
||||||
dataLoading.value = false;
|
|
||||||
|
|
||||||
lastModeLoaded.value = mode;
|
|
||||||
lastInputLoaded.value = currentInput.value;
|
|
||||||
|
|
||||||
};
|
|
||||||
onMounted(loadDataClicked);
|
|
||||||
|
|
||||||
// handle change of day/year
|
|
||||||
watch([currentDay, currentYear], () => {
|
|
||||||
if (currentInput.value == lastInputLoaded.value) {
|
|
||||||
loadDataClicked(lastModeLoaded.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// handle submit
|
|
||||||
const resultComponent = ref<typeof ResultVue>();
|
|
||||||
const submitRunning = ref(false);
|
|
||||||
|
|
||||||
const submitClicked = async () => {
|
|
||||||
submitRunning.value = true;
|
|
||||||
await resultComponent.value!.submit(currentYear.value, currentDay.value, currentInput.value, () => { });
|
|
||||||
submitRunning.value = false;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="container mt-3">
|
<RouterView />
|
||||||
<h1>Advent-Of-Code — Holt59</h1>
|
|
||||||
<hr />
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<ResultVue ref="resultComponent" :show-logs="showLogs" />
|
|
||||||
<section class="container d-flex flex-column flex-grow-1 position-relative">
|
|
||||||
<form method="POST" class="d-flex flex-column flex-grow-1">
|
|
||||||
<div class="row mb-2 align-items-center">
|
|
||||||
<div class="col-sm-2">
|
|
||||||
<button class="form-control btn btn-outline-info" @click.prevent="todayClicked"
|
|
||||||
:disabled="initialYear == currentYear && initialDay == currentDay">Today</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
<select class="form-select" autocomplete="off" v-model="currentYear">
|
|
||||||
<option v-for="year in years" :key="year" :value="year">{{ year }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
<select class="form-select" autocomplete="off" v-model="currentDay">
|
|
||||||
<option v-for="day in days" :key="day" :value="day">{{ day }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
<button class="form-control btn btn-success" id="load-data"
|
|
||||||
@click.prevent="() => { loadDataClicked('tests') }"
|
|
||||||
@contextmenu.prevent="() => { loadDataClicked('holt59') }">
|
|
||||||
<span class="spinner-border spinner-border-sm" :class="{ 'd-none': !dataLoading }" role="status"
|
|
||||||
aria-hidden="true"></span>
|
|
||||||
<span class="submit-text" :class="{ 'd-none': dataLoading }">Test Data</span></button>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="showLogs" v-model="showLogs">
|
|
||||||
<label class="form-check-label" for="showLogs">
|
|
||||||
Show Logs
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2">
|
|
||||||
<button type="submit" class="form-control btn btn-primary" @click.prevent="submitClicked">
|
|
||||||
<span class="spinner-border spinner-border-sm" :class="{ 'd-none': !submitRunning }" role="status"
|
|
||||||
aria-hidden="true"></span>
|
|
||||||
<span class="submit-text" :class="{ 'd-none': submitRunning }">Go!</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<textarea class="area-input flex-grow-1 w-100" v-model="currentInput"></textarea>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.area-input {
|
|
||||||
resize: none;
|
|
||||||
font-family: var(--bs-font-monospace);
|
|
||||||
font-size: .9em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -32,16 +32,22 @@ export const submit = async (year: number, day: number, input: string, startCall
|
|||||||
});
|
});
|
||||||
|
|
||||||
eventSource.addEventListener('message', function (e: { data: string }) {
|
eventSource.addEventListener('message', function (e: { data: string }) {
|
||||||
const data = JSON.parse(e.data);
|
try {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
|
||||||
if (data.event == "complete") {
|
if (data.event == "complete") {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
|
}
|
||||||
|
else if (data.event == "start") {
|
||||||
|
startCallback(DateTime.fromISO(data.data.time), data.data.commit);
|
||||||
|
}
|
||||||
|
else if (data.event == "stream") {
|
||||||
|
streamCallback(data.data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (data.event == "start") {
|
catch (exception) {
|
||||||
startCallback(DateTime.fromISO(data.data.time), data.data.commit);
|
console.log(e.data);
|
||||||
}
|
console.log(exception);
|
||||||
else if (data.event == "stream") {
|
|
||||||
streamCallback(data.data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,9 +40,20 @@ type _LogStream = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
type _FileStream = {
|
||||||
|
type: "file";
|
||||||
|
time: string;
|
||||||
|
content: {
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
type _ErrorStream = {
|
type _ErrorStream = {
|
||||||
type: "error";
|
type: "error";
|
||||||
|
time: string;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StreamMessage = _AnswerStream | _ProgressStream | _LogStream | _ErrorStream;
|
export type StreamMessage = _AnswerStream | _ProgressStream | _LogStream | _ErrorStream | _FileStream;
|
||||||
|
188
front/src/components/MainPage.vue
Normal file
188
front/src/components/MainPage.vue
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
import ResultVue from '@/components/ResultVue.vue'
|
||||||
|
|
||||||
|
import { loadData } from '@/api';
|
||||||
|
import { range } from 'lodash';
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const inAocPeriod = (date: DateTime) => {
|
||||||
|
return !(date.day > 25 || date.month != 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentInput = ref("");
|
||||||
|
const showLogs = ref(false);
|
||||||
|
|
||||||
|
const today = DateTime.now();
|
||||||
|
const maxYear = today.month == 12 ? today.year : today.year - 1;
|
||||||
|
|
||||||
|
const years = range(2015, maxYear + 1);
|
||||||
|
const days = range(1, 26);
|
||||||
|
|
||||||
|
const initialDay = ref(1);
|
||||||
|
const initialYear = ref(maxYear);
|
||||||
|
|
||||||
|
const currentDay = ref(1);
|
||||||
|
const currentYear = ref(maxYear);
|
||||||
|
const currentLoadMode = ref<"holt59" | "tests">("tests");
|
||||||
|
|
||||||
|
// mounted
|
||||||
|
onMounted(() => {
|
||||||
|
const query = router.currentRoute.value.query;
|
||||||
|
|
||||||
|
const day = parseInt(query.day as string);
|
||||||
|
if (day && day >= 1 && day <= 25) {
|
||||||
|
initialDay.value = day;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
initialDay.value = inAocPeriod(today) ? today.day as number : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = parseInt(query.year as string);
|
||||||
|
if (year && year >= 2015 && year <= maxYear) {
|
||||||
|
initialYear.value = year;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
initialYear.value = maxYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = query.data as string;
|
||||||
|
if (["tests", "holt59"].includes(mode)) {
|
||||||
|
currentLoadMode.value = mode as "tests" | "holt59";
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDay.value = initialDay.value;
|
||||||
|
currentYear.value = initialYear.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// handle current day clicked
|
||||||
|
const todayClicked = () => {
|
||||||
|
const today = DateTime.now();
|
||||||
|
|
||||||
|
initialDay.value = inAocPeriod(today) ? today.day : 1;
|
||||||
|
initialYear.value = today.month == 12 ? today.year : today.year - 1;
|
||||||
|
|
||||||
|
currentDay.value = initialDay.value;
|
||||||
|
currentYear.value = initialYear.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle load data + load data of current day on startup
|
||||||
|
const dataLoading = ref(false);
|
||||||
|
const lastInputLoaded = ref("");
|
||||||
|
const lastModeLoaded = ref<"holt59" | "tests">("tests");
|
||||||
|
|
||||||
|
const loadDataClicked = async () => {
|
||||||
|
dataLoading.value = true;
|
||||||
|
currentInput.value = await loadData(currentYear.value, currentDay.value, currentLoadMode.value);
|
||||||
|
dataLoading.value = false;
|
||||||
|
|
||||||
|
lastModeLoaded.value = currentLoadMode.value;
|
||||||
|
lastInputLoaded.value = currentInput.value;
|
||||||
|
|
||||||
|
};
|
||||||
|
onMounted(loadDataClicked);
|
||||||
|
|
||||||
|
// handle change of day/year
|
||||||
|
watch([currentDay, currentYear], () => {
|
||||||
|
if (currentInput.value == lastInputLoaded.value && currentLoadMode.value == lastModeLoaded.value) {
|
||||||
|
loadDataClicked();
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({ name: router.currentRoute.value.name, query: { "year": currentYear.value, "day": currentDay.value, "data": currentLoadMode.value } });
|
||||||
|
});
|
||||||
|
watch([currentLoadMode], () => {
|
||||||
|
loadDataClicked();
|
||||||
|
router.push({ name: router.currentRoute.value.name, query: { "year": currentYear.value, "day": currentDay.value, "data": currentLoadMode.value } });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// handle submit
|
||||||
|
const resultComponent = ref<typeof ResultVue>();
|
||||||
|
const submitRunning = ref(false);
|
||||||
|
|
||||||
|
const submitClicked = async () => {
|
||||||
|
submitRunning.value = true;
|
||||||
|
await resultComponent.value!.submit(currentYear.value, currentDay.value, currentInput.value, () => { });
|
||||||
|
submitRunning.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="container mt-3">
|
||||||
|
<h1>Advent-Of-Code — Holt59</h1>
|
||||||
|
<hr />
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<ResultVue ref="resultComponent" :show-logs="showLogs" />
|
||||||
|
<section class="container d-flex flex-column flex-grow-1 position-relative">
|
||||||
|
<form method="POST" class="d-flex flex-column flex-grow-1">
|
||||||
|
<div class="row mb-2 align-items-center">
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<button class="form-control btn btn-outline-info" @click.prevent="todayClicked"
|
||||||
|
:disabled="initialYear == currentYear && initialDay == currentDay">Today</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<select class="form-select" autocomplete="off" v-model="currentYear">
|
||||||
|
<option v-for="year in years" :key="year" :value="year">{{ year }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<select class="form-select" autocomplete="off" v-model="currentDay">
|
||||||
|
<option v-for="day in days" :key="day" :value="day">{{ day }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<div class="btn-group w-100">
|
||||||
|
<button class="form-control btn btn-success" id="load-data"
|
||||||
|
@click.prevent="() => { loadDataClicked() }">
|
||||||
|
<span class="spinner-border spinner-border-sm" :class="{ 'd-none': !dataLoading }"
|
||||||
|
role="status" aria-hidden="true"></span>
|
||||||
|
<span class="submit-text" :class="{ 'd-none': dataLoading }">Load ({{ currentLoadMode
|
||||||
|
}})</span></button>
|
||||||
|
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<span class="visually-hidden">Toggle Dropdown</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#"
|
||||||
|
@click.prevent="() => { currentLoadMode = 'tests' }">Tests</a>
|
||||||
|
</li>
|
||||||
|
<li><a class="dropdown-item" href="#"
|
||||||
|
@click.prevent="() => { currentLoadMode = 'holt59' }">Holt59</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="showLogs" v-model="showLogs">
|
||||||
|
<label class="form-check-label" for="showLogs">
|
||||||
|
Show Logs
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<button type="submit" class="form-control btn btn-primary" @click.prevent="submitClicked">
|
||||||
|
<span class="spinner-border spinner-border-sm" :class="{ 'd-none': !submitRunning }"
|
||||||
|
role="status" aria-hidden="true"></span>
|
||||||
|
<span class="submit-text" :class="{ 'd-none': submitRunning }">Go!</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea class="area-input flex-grow-1 w-100" v-model="currentInput"></textarea>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.area-input {
|
||||||
|
resize: none;
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
font-size: .9em;
|
||||||
|
}
|
||||||
|
</style>
|
@ -3,8 +3,11 @@
|
|||||||
import * as Api from '@/api';
|
import * as Api from '@/api';
|
||||||
import type { StreamMessage } from '@/api/models';
|
import type { StreamMessage } from '@/api/models';
|
||||||
import type { DateTime } from 'luxon';
|
import type { DateTime } from 'luxon';
|
||||||
import { h, reactive, ref, type Reactive, type VNode } from 'vue';
|
import { h, reactive, ref, watch, type Reactive, type VNode } from 'vue';
|
||||||
|
import { AnsiUp } from 'ansi-up';
|
||||||
import ResultProgressLine from './ResultProgressLine.vue';
|
import ResultProgressLine from './ResultProgressLine.vue';
|
||||||
|
import * as boostrap from 'bootstrap';
|
||||||
|
import { instance } from "@viz-js/viz";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
showLogs: boolean
|
showLogs: boolean
|
||||||
@ -24,6 +27,16 @@ const contentRows = ref<Array<() => VNode>>([]);
|
|||||||
const contentProgress: Map<number, Reactive<{ progress: number }>> = new Map();
|
const contentProgress: Map<number, Reactive<{ progress: number }>> = new Map();
|
||||||
const contentErrors = ref("");
|
const contentErrors = ref("");
|
||||||
|
|
||||||
|
const fileModal = ref<HTMLDivElement>();
|
||||||
|
const bootstrapFileModal = ref<boostrap.Modal>();
|
||||||
|
const currentFilename = ref("");
|
||||||
|
const currentFilecontent = ref<Element>();
|
||||||
|
|
||||||
|
watch([fileModal], () => {
|
||||||
|
// @ts-expect-error bad typing
|
||||||
|
bootstrapFileModal.value = new boostrap.Modal(fileModal.value, {});
|
||||||
|
})
|
||||||
|
|
||||||
const addRow = (node: () => VNode) => {
|
const addRow = (node: () => VNode) => {
|
||||||
contentRows.value = contentRows.value.concat(node);
|
contentRows.value = contentRows.value.concat(node);
|
||||||
}
|
}
|
||||||
@ -35,6 +48,32 @@ const startCallback = (time: DateTime, commit: string) => {
|
|||||||
addRow(() => h("p", {}, ["Computing part 1... "]));
|
addRow(() => h("p", {}, ["Computing part 1... "]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileBuilders: { [extension: string]: (content: string) => Promise<Element> } = {
|
||||||
|
txt: async (content: string) => {
|
||||||
|
const ansiUp = new AnsiUp();
|
||||||
|
ansiUp.escapeForHtml = false;
|
||||||
|
const ansiHtml = ansiUp.ansi_to_html(content.replaceAll("\n", "<br/>"));
|
||||||
|
|
||||||
|
const codeElement = document.createElement("code")
|
||||||
|
codeElement.classList.add("d-block")
|
||||||
|
codeElement.style.fontSize = ".84em"
|
||||||
|
codeElement.innerHTML = ansiHtml
|
||||||
|
|
||||||
|
return codeElement
|
||||||
|
},
|
||||||
|
dot: async (content: string) => {
|
||||||
|
return instance().then(viz => {
|
||||||
|
const e = viz.renderSVGElement(content)
|
||||||
|
e.style.maxWidth = "100%";
|
||||||
|
e.style.height = "auto";
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLogs = 100;
|
||||||
|
const logCounter = ref(0);
|
||||||
|
|
||||||
const streamCallback = (message: StreamMessage) => {
|
const streamCallback = (message: StreamMessage) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "progress-start":
|
case "progress-start":
|
||||||
@ -49,24 +88,54 @@ const streamCallback = (message: StreamMessage) => {
|
|||||||
contentProgress.get(message.content.counter)!.progress = 100;
|
contentProgress.get(message.content.counter)!.progress = 100;
|
||||||
break;
|
break;
|
||||||
case "log":
|
case "log":
|
||||||
addRow(() => h("p", { "class": { "log": true, "d-none": !props.showLogs } }, [`[${message.content.level}] ${message.time}: ${message.content.message}`]))
|
logCounter.value += 1;
|
||||||
|
if (logCounter.value < maxLogs) {
|
||||||
|
addRow(() => h("p", { "class": { "log": true, "d-none": !props.showLogs } }, [`[${message.content.level}] ${message.time}: ${message.content.message}`]))
|
||||||
|
}
|
||||||
|
else if (logCounter.value == maxLogs) {
|
||||||
|
addRow(() => h("p", { "class": { "log": true, "d-none": !props.showLogs } }, [`[${message.content.level}] ${message.time}: Too many logs, capturing stopped.`]))
|
||||||
|
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
contentErrors.value = contentErrors.value + message.message;
|
contentErrors.value = contentErrors.value + message.message;
|
||||||
break;
|
break;
|
||||||
|
case "file":
|
||||||
|
addRow(() => h(
|
||||||
|
"p", {}, [`File `, h('a', {
|
||||||
|
href: Api.urlFor(`file?name=${message.content.filename}`),
|
||||||
|
target: "_blank",
|
||||||
|
onClick: async (e) => {
|
||||||
|
const href = (e.target as HTMLAnchorElement).href as string;
|
||||||
|
const extension = href.substring(href.lastIndexOf(".") + 1)
|
||||||
|
console.log(extension)
|
||||||
|
|
||||||
|
if (!fileBuilders.hasOwnProperty(extension)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
currentFilename.value = href.split("=")[1];
|
||||||
|
currentFilecontent.value = await fetch(href).then(r => r.text()).then(fileBuilders[extension])
|
||||||
|
|
||||||
|
bootstrapFileModal.value!.show();
|
||||||
|
}
|
||||||
|
}, [message.content.filename]), ` (${message.content.size} bytes) available.`],
|
||||||
|
));
|
||||||
|
break;
|
||||||
case "answer": {
|
case "answer": {
|
||||||
const value = message.content.value;
|
const value = message.content.value;
|
||||||
if (value.trim().indexOf("\n") >= 0) {
|
if (value.trim().indexOf("\n") >= 0) {
|
||||||
addRow(() => h(
|
addRow(() => h(
|
||||||
"p", {}, [`Answer ${message.content.answer} is`,
|
"p", {}, [`Answer ${message.content.answer} is`,
|
||||||
h('pre', {}, [value]),
|
h('pre', {}, [value]),
|
||||||
`...found in ${message.content.answerTime_s}s.`]
|
`...found in ${message.content.answerTime_s} s.`]
|
||||||
));
|
));
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
addRow(() => h(
|
addRow(() => h(
|
||||||
"p", {}, [`Answer ${message.content.answer} is '${message.content.value}', found in ${message.content.answerTime_s}s.`]
|
"p", {}, [`Answer ${message.content.answer} is '${message.content.value}', found in ${message.content.answerTime_s} s.`]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +148,7 @@ const streamCallback = (message: StreamMessage) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submit = async (year: number, day: number, input: string) => {
|
const submit = async (year: number, day: number, input: string) => {
|
||||||
|
logCounter.value = 0;
|
||||||
answer1Value.value = null;
|
answer1Value.value = null;
|
||||||
answer2Value.value = null;
|
answer2Value.value = null;
|
||||||
contentErrors.value = "";
|
contentErrors.value = "";
|
||||||
@ -111,18 +181,19 @@ defineExpose({ submit });
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div class="modal fade" id="logsModal" tabindex="-1" aria-labelledby="logsModalLabel" aria-hidden="true">
|
<div class="modal fade" id="logsModal" tabindex="-1" aria-labelledby="logsModalLabel" aria-hidden="true"
|
||||||
|
ref="fileModal">
|
||||||
<div class="modal-dialog modal-xl">
|
<div class="modal-dialog modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h1 class="modal-title fs-5" id="logsModalLabel">Logs</h1>
|
<h1 class="modal-title fs-5" id="logsModalLabel">{{ currentFilename }}</h1>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body text-center" v-html="currentFilecontent?.outerHTML">
|
||||||
<code class="stderr text-danger" id="data-logs"><pre>{{ '' }}</pre></code>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
@ -6,9 +6,23 @@ import 'bootstrap-icons/font/bootstrap-icons.css'
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import MainPage from '@/components/MainPage.vue'
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: MainPage
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@ -10,6 +10,11 @@
|
|||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2021",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
|
@ -769,6 +769,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz#d1491f678ee3af899f7ae57d9c21dc52a65c7133"
|
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz#d1491f678ee3af899f7ae57d9c21dc52a65c7133"
|
||||||
integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==
|
integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==
|
||||||
|
|
||||||
|
"@viz-js/viz@^3.11.0":
|
||||||
|
version "3.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@viz-js/viz/-/viz-3.11.0.tgz#516a3fb644ca871762bcb86e2b41b6d107869c20"
|
||||||
|
integrity sha512-3zoKLQUqShIhTPvBAIIgJUf5wO9aY0q+Ftzw1u26KkJX1OJjT7Z5VUqgML2GIzXJYFgjqS6a2VREMwrgChuubA==
|
||||||
|
|
||||||
"@volar/language-core@2.4.10", "@volar/language-core@~2.4.8":
|
"@volar/language-core@2.4.10", "@volar/language-core@~2.4.8":
|
||||||
version "2.4.10"
|
version "2.4.10"
|
||||||
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.10.tgz#7d57c29d27f7bce2fa7eb9f3a1fc053a3e28e53f"
|
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.10.tgz#7d57c29d27f7bce2fa7eb9f3a1fc053a3e28e53f"
|
||||||
@ -872,7 +877,7 @@
|
|||||||
de-indent "^1.0.2"
|
de-indent "^1.0.2"
|
||||||
he "^1.2.0"
|
he "^1.2.0"
|
||||||
|
|
||||||
"@vue/devtools-api@^6.6.3":
|
"@vue/devtools-api@^6.6.3", "@vue/devtools-api@^6.6.4":
|
||||||
version "6.6.4"
|
version "6.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||||
@ -1026,6 +1031,11 @@ ansi-styles@^6.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||||
|
|
||||||
|
ansi-up@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-up/-/ansi-up-1.0.0.tgz#235aa25b95f997291f2f6335e95485cae339a356"
|
||||||
|
integrity sha512-UItk+l+fKaa9th5dM/1uqeJGS7QfEPutfQB8kGBGUZo2GhF6u4q40bGa5+cQI7Jscs8L6FP8ue6+GflduCbLRQ==
|
||||||
|
|
||||||
anymatch@~3.1.2:
|
anymatch@~3.1.2:
|
||||||
version "3.1.3"
|
version "3.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
|
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
|
||||||
@ -2424,6 +2434,13 @@ vue-eslint-parser@^9.4.3:
|
|||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
semver "^7.3.6"
|
semver "^7.3.6"
|
||||||
|
|
||||||
|
vue-router@4:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14"
|
||||||
|
integrity sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==
|
||||||
|
dependencies:
|
||||||
|
"@vue/devtools-api" "^6.6.4"
|
||||||
|
|
||||||
vue-tsc@^2.1.10:
|
vue-tsc@^2.1.10:
|
||||||
version "2.1.10"
|
version "2.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.1.10.tgz#4d61a64e5fad763b8b40c1884259fd48986f0b4e"
|
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.1.10.tgz#4d61a64e5fad763b8b40c1884259fd48986f0b4e"
|
||||||
|
Loading…
Reference in New Issue
Block a user