Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions components/CodeEditor.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { defaultCode, jsdoc } from "~/other/defaultCode.js";
import { CodeVersion } from "~/other/botCodeStore";

Check failure on line 3 in components/CodeEditor.vue

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`

Check failure on line 3 in components/CodeEditor.vue

View workflow job for this annotation

GitHub Actions / test

'CodeVersion' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.

Check failure on line 3 in components/CodeEditor.vue

View workflow job for this annotation

GitHub Actions / test

'CodeVersion' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.

if (import.meta.client) {
// configure monaco editor on client side
Expand Down Expand Up @@ -33,8 +34,21 @@
code: user.value?.body.code || defaultCode,
codeHasErrors: false,
showAPIReference: false,
showVersionHistory: false,
codeVersions: [] as CodeVersion[],
});

// Fetch code versions when needed
async function fetchCodeVersions() {
if (user.value?.body.id) {

Check failure on line 43 in components/CodeEditor.vue

View workflow job for this annotation

GitHub Actions / test

Property 'id' does not exist on type '{ code: string | undefined; username: string; }'.
const response = await fetch(`/api/bot/versions?userId=${user.value.body.id}`);

Check failure on line 44 in components/CodeEditor.vue

View workflow job for this annotation

GitHub Actions / test

Property 'id' does not exist on type '{ code: string | undefined; username: string; }'.
if (response.ok) {
const data = await response.json();
state.codeVersions = data.versions;
}
}
}

function onSubmit(event: Event) {
event.preventDefault();
fetch("/api/bot/submit", {
Expand All @@ -57,6 +71,20 @@
function onCloseAPIReferenceModal() {
state.showAPIReference = false;
}

function onShowVersionHistoryClick() {
fetchCodeVersions().then(() => {
state.showVersionHistory = true;
});
}

function onCloseVersionHistoryModal() {
state.showVersionHistory = false;
}

function onRestoreVersion(code: string) {
state.code = code;
}
</script>

<template>
Expand All @@ -73,6 +101,9 @@
<ButtonLink @click="onRestoreDefaultCodeClick">
restore example code
</ButtonLink>
<ButtonLink @click="onShowVersionHistoryClick">
version history
</ButtonLink>
<ButtonLink @click="onShowAPIReferenceClick">
API reference
</ButtonLink>
Expand Down Expand Up @@ -117,4 +148,15 @@
class="flex-grow"
/>
</ModalDialog>
<ModalDialog
:open="state.showVersionHistory"
:on-close="onCloseVersionHistoryModal"
extra-modal-class="w-[800px]"
>
<CodeVersionHistory
:versions="state.codeVersions"
:on-close="onCloseVersionHistoryModal"
:on-restore="onRestoreVersion"
/>
</ModalDialog>
</template>
90 changes: 90 additions & 0 deletions components/CodeVersionHistory.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<script setup lang="ts">
import { CodeVersion } from "~/other/botCodeStore";

Check failure on line 2 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`

Check failure on line 2 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / test

'CodeVersion' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.

const props = defineProps<{
versions: CodeVersion[];
onClose: () => void;
onRestore: (code: string) => void;
}>();

const selectedVersionIndex = ref(0);

const formattedVersions = computed(() => {
return props.versions.map((version, index) => {
const date = new Date(version.timestamp);
return {
...version,
formattedDate: date.toLocaleString(),
isSelected: index === selectedVersionIndex.value

Check failure on line 18 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
};
});
});

function selectVersion(index: number) {
selectedVersionIndex.value = index;
}

function restoreSelectedVersion() {
if (props.versions.length > 0) {
props.onRestore(props.versions[selectedVersionIndex.value].code);

Check failure on line 29 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / test

Object is possibly 'undefined'.
props.onClose();
}
}
</script>

<template>
<div class="flex h-full">
<!-- Left side: Code preview -->
<div class="w-2/3 h-full pr-4">
<MonacoEditor
v-if="versions.length > 0"
v-model="versions[selectedVersionIndex].code"

Check failure on line 41 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Unexpected mutation of "versions" prop

Check failure on line 41 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / test

Object is possibly 'undefined'.
lang="javascript"
:options="{
readOnly: true,
readOnlyMessage: { value: 'Cannot edit version preview' },
smoothScrolling: true,
scrollBeyondLastLine: false,
minimap: { enabled: false },
}"
class="h-full"
/>
<div v-else class="flex items-center justify-center h-full bg-gray-100">

Check warning on line 52 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

'class' should be on a new line
<p class="text-gray-500">No previous versions found</p>

Check warning on line 53 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Expected 1 line break after opening tag (`<p>`), but no line breaks found

Check warning on line 53 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Expected 1 line break before closing tag (`</p>`), but no line breaks found
</div>
</div>

Check failure on line 56 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
<!-- Right side: Version list -->
<div class="w-1/3 h-full overflow-y-auto border-l pl-4">
<h3 class="text-lg font-semibold mb-4">Version History</h3>

Check warning on line 59 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Expected 1 line break after opening tag (`<h3>`), but no line breaks found

Check warning on line 59 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Expected 1 line break before closing tag (`</h3>`), but no line breaks found

Check failure on line 60 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
<div v-if="versions.length === 0" class="text-gray-500">

Check warning on line 61 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

'class' should be on a new line
No previous versions available
</div>

Check failure on line 64 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
<ul v-else class="space-y-2">

Check warning on line 65 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

'class' should be on a new line
<li

Check failure on line 66 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
v-for="(version, index) in formattedVersions"

Check failure on line 67 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
:key="version.timestamp"
@click="selectVersion(index)"
:class="[

Check warning on line 70 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Attribute ":class" should go before "@click"
'p-2 cursor-pointer rounded transition-colors',
version.isSelected ? 'bg-blue-100' : 'hover:bg-gray-100'

Check failure on line 72 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
]"
>
<div class="font-medium">{{ version.formattedDate }}</div>

Check warning on line 75 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Expected 1 line break after opening tag (`<div>`), but no line breaks found

Check warning on line 75 in components/CodeVersionHistory.vue

View workflow job for this annotation

GitHub Actions / lint

Expected 1 line break before closing tag (`</div>`), but no line breaks found
</li>
</ul>

<div class="mt-6 flex justify-end">
<button
v-if="versions.length > 0"
@click="restoreSelectedVersion"
class="h-10 px-6 font-semibold shadow bg-black text-white hover:bg-gray-800 transition"
>
Restore This Version
</button>
</div>
</div>
</div>
</template>
60 changes: 59 additions & 1 deletion other/botCodeStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import EventEmitter from "node:events";
import fs from "node:fs";
import path from "node:path";

const BOT_CODE_DIR = process.env.BOT_CODE_DIR || "./bot-code";

Expand All @@ -10,6 +11,12 @@
userId: number;
}

export interface CodeVersion {
code: string;
timestamp: number;
id: string;
}

export type BotCodes = Record<string, BotCode>;

let STORE: BotCodes = {};
Expand All @@ -27,7 +34,11 @@
const id = Object.values(STORE).find(botCode => botCode.username === username)?.id || Math.random().toString(36).substring(5);
const botCode = { id, code, username, userId };
STORE[id] = botCode;

// Save both the latest version and a timestamped version
saveBot(botCode);
saveBotVersion(botCode);

botEventEmitter.emit("update", STORE);
}

Expand All @@ -44,14 +55,50 @@
return () => botEventEmitter.off("update", onUpdate);
}

export function getUserCodeVersions(userId: number): CodeVersion[] {
if (!fs.existsSync(BOT_CODE_DIR)) {
return [];
}

const files = fs.readdirSync(BOT_CODE_DIR);
const versionFiles = files.filter(file => {
const parts = file.split("-");
return parts.length >= 4 && parts[2] === userId.toString() && parts[3].includes("version");

Check failure on line 66 in other/botCodeStore.ts

View workflow job for this annotation

GitHub Actions / test

Object is possibly 'undefined'.
});

return versionFiles.map(file => {

Check failure on line 69 in other/botCodeStore.ts

View workflow job for this annotation

GitHub Actions / test

Type '{ code: string; timestamp: number; id: string | undefined; }[]' is not assignable to type 'CodeVersion[]'.
const code = fs.readFileSync(path.join(BOT_CODE_DIR, file), "utf8");
const parts = file.split("-");
const timestamp = parseInt(parts[3].replace("version", "").replace(".js", ""));

Check failure on line 72 in other/botCodeStore.ts

View workflow job for this annotation

GitHub Actions / test

Object is possibly 'undefined'.
const id = parts[1];

return {
code,
timestamp,
id
};
}).sort((a, b) => b.timestamp - a.timestamp); // Sort newest first
}

export function getLatestUserCode(userId: number): string | null {
const userBot = Object.values(STORE).find(bot => bot.userId === userId);
if (userBot) {
return userBot.code;
}
return null;
}

function loadBots() {
if (!fs.existsSync(BOT_CODE_DIR)) {
return;
}

const files = fs.readdirSync(BOT_CODE_DIR);

// Only load the latest versions (non-versioned files)
const latestFiles = files.filter(file => !file.includes("version"));

for (const file of files) {
for (const file of latestFiles) {
const code = fs.readFileSync(`${BOT_CODE_DIR}/${file}`, "utf8");
const username = file.split("-")[0];
const id = file.split("-")[1];
Expand All @@ -72,4 +119,15 @@
fs.writeFileSync(botCodeFile, code);
}

function saveBotVersion({ id, code, username, userId }: BotCode) {
if (!fs.existsSync(BOT_CODE_DIR)) {
fs.mkdirSync(BOT_CODE_DIR);
}

const timestamp = Date.now();
const versionFile = `${BOT_CODE_DIR}/${username}-${id}-${userId}-version${timestamp}.js`;

fs.writeFileSync(versionFile, code);
}

loadBots();
27 changes: 27 additions & 0 deletions server/api/bot/versions.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getUserCodeVersions } from "~/other/botCodeStore";

export default defineEventHandler(async (event) => {
const query = getQuery(event);
const userId = parseInt(query.userId as string);

if (isNaN(userId)) {
throw createError({
statusCode: 400,
statusMessage: "Invalid user ID",
});
}

// Check if the requesting user is the same as the user whose versions are being requested
if (event.context.user.id !== userId) {
throw createError({
statusCode: 403,
statusMessage: "Unauthorized to access other users' code versions",
});
}

const versions = getUserCodeVersions(userId);

return {
versions,
};
});