diff --git a/backend/src/holt59/aoc/app.py b/backend/src/holt59/aoc/app.py index d868bfe..77f22ac 100644 --- a/backend/src/holt59/aoc/app.py +++ b/backend/src/holt59/aoc/app.py @@ -6,7 +6,8 @@ import subprocess from pathlib import Path 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 DataMode: TypeAlias = Literal["holt59", "tests"] @@ -14,6 +15,7 @@ DataMode: TypeAlias = Literal["holt59", "tests"] LOGGER = logging.getLogger("uvicorn.info") GIT_AOC_PATH = Path("/code") +AOC_FILES_PATH = Path("/files") 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)} +@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") async def submit_sse(year: int = Form(), day: int = Form(), input: str = Form()): 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", "--stdin", "--api", + "--output", + AOC_FILES_PATH, "--year", str(year), str(day), diff --git a/front/package.json b/front/package.json index cd047c8..cb0d809 100644 --- a/front/package.json +++ b/front/package.json @@ -13,13 +13,16 @@ "format": "prettier --write src/" }, "dependencies": { + "@viz-js/viz": "^3.11.0", + "ansi-up": "^1.0.0", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", "lodash": "^4.17.21", "luxon": "^3.5.0", "pinia": "^2.2.6", "sse.js": "^2.5.0", - "vue": "^3.5.13" + "vue": "^3.5.13", + "vue-router": "4" }, "devDependencies": { "@tsconfig/node22": "^22.0.0", diff --git a/front/src/App.vue b/front/src/App.vue index e5549a9..7c2aa3f 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -1,134 +1,3 @@ - - - - diff --git a/front/src/api/index.ts b/front/src/api/index.ts index 658d7eb..b2053f9 100644 --- a/front/src/api/index.ts +++ b/front/src/api/index.ts @@ -32,16 +32,22 @@ export const submit = async (year: number, day: number, input: string, startCall }); eventSource.addEventListener('message', function (e: { data: string }) { - const data = JSON.parse(e.data); + try { + const data = JSON.parse(e.data); - if (data.event == "complete") { - resolve(null) + if (data.event == "complete") { + 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") { - startCallback(DateTime.fromISO(data.data.time), data.data.commit); - } - else if (data.event == "stream") { - streamCallback(data.data); + catch (exception) { + console.log(e.data); + console.log(exception); } }); diff --git a/front/src/api/models.ts b/front/src/api/models.ts index 492a0b9..3be8103 100644 --- a/front/src/api/models.ts +++ b/front/src/api/models.ts @@ -40,9 +40,20 @@ type _LogStream = { } }; + +type _FileStream = { + type: "file"; + time: string; + content: { + filename: string; + size: number; + } +}; + type _ErrorStream = { type: "error"; + time: string; message: string; }; -export type StreamMessage = _AnswerStream | _ProgressStream | _LogStream | _ErrorStream; +export type StreamMessage = _AnswerStream | _ProgressStream | _LogStream | _ErrorStream | _FileStream; diff --git a/front/src/components/MainPage.vue b/front/src/components/MainPage.vue new file mode 100644 index 0000000..c30d482 --- /dev/null +++ b/front/src/components/MainPage.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/front/src/components/ResultVue.vue b/front/src/components/ResultVue.vue index 4536e70..26c2746 100644 --- a/front/src/components/ResultVue.vue +++ b/front/src/components/ResultVue.vue @@ -3,8 +3,11 @@ import * as Api from '@/api'; import type { StreamMessage } from '@/api/models'; 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 * as boostrap from 'bootstrap'; +import { instance } from "@viz-js/viz"; const props = defineProps<{ showLogs: boolean @@ -24,6 +27,16 @@ const contentRows = ref VNode>>([]); const contentProgress: Map> = new Map(); const contentErrors = ref(""); +const fileModal = ref(); +const bootstrapFileModal = ref(); +const currentFilename = ref(""); +const currentFilecontent = ref(); + +watch([fileModal], () => { + // @ts-expect-error bad typing + bootstrapFileModal.value = new boostrap.Modal(fileModal.value, {}); +}) + const addRow = (node: () => VNode) => { contentRows.value = contentRows.value.concat(node); } @@ -35,6 +48,32 @@ const startCallback = (time: DateTime, commit: string) => { addRow(() => h("p", {}, ["Computing part 1... "])); } +const fileBuilders: { [extension: string]: (content: string) => Promise } = { + txt: async (content: string) => { + const ansiUp = new AnsiUp(); + ansiUp.escapeForHtml = false; + const ansiHtml = ansiUp.ansi_to_html(content.replaceAll("\n", "
")); + + 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) => { switch (message.type) { case "progress-start": @@ -49,24 +88,54 @@ const streamCallback = (message: StreamMessage) => { contentProgress.get(message.content.counter)!.progress = 100; break; 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; case "error": contentErrors.value = contentErrors.value + message.message; 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": { const value = message.content.value; if (value.trim().indexOf("\n") >= 0) { addRow(() => h( "p", {}, [`Answer ${message.content.answer} is`, h('pre', {}, [value]), - `...found in ${message.content.answerTime_s}s.`] + `...found in ${message.content.answerTime_s} s.`] )); - } else { 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) => { + logCounter.value = 0; answer1Value.value = null; answer2Value.value = null; contentErrors.value = ""; @@ -111,18 +181,19 @@ defineExpose({ submit }); -