Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
0f26b6d
demo
RedWARd42 Apr 21, 2026
f9aca6a
the zoom reduced
chanabyte Apr 21, 2026
0720cf9
case report update
chanabyte Apr 21, 2026
9083bc6
shorter messages
RedWARd42 Apr 21, 2026
5fcfd0a
Merge branch 'demo' of https://github.com/acm-projects/Agentic-Detect…
RedWARd42 Apr 21, 2026
60772ab
case report update
chanabyte Apr 21, 2026
f4cd2d0
Merge branch 'demo' of https://github.com/acm-projects/Agentic-Detect…
chanabyte Apr 21, 2026
3e431eb
community tab
chanabyte Apr 22, 2026
7087e30
feature
chanabyte Apr 22, 2026
0ce61a0
clue
chanabyte Apr 22, 2026
7dd6edd
fixing inconsistencies
RedWARd42 Apr 22, 2026
03713ef
Merge branch 'demo' of https://github.com/acm-projects/Agentic-Detect…
RedWARd42 Apr 22, 2026
ffa2dd9
Fixing inconsistencies
RedWARd42 Apr 22, 2026
d3a16d1
ratings+ featured
chanabyte Apr 22, 2026
d9ec754
Merge branch 'demo' of https://github.com/acm-projects/Agentic-Detect…
chanabyte Apr 22, 2026
b730bca
removed accuse
chanabyte Apr 22, 2026
89ce347
accuse layout
chanabyte Apr 22, 2026
d205cc9
accuse page now red on button and title
chanabyte Apr 22, 2026
e5b137b
accuse layout
chanabyte Apr 22, 2026
fb447c3
Merge pull request #27 from acm-projects/new-accuse
chanabyte Apr 22, 2026
c022970
changes
RedWARd42 Apr 22, 2026
2d8970d
Merge branch 'demo' of https://github.com/acm-projects/Agentic-Detect…
RedWARd42 Apr 22, 2026
3c8bb66
changes
RedWARd42 Apr 22, 2026
fc475f3
accuse page
chanabyte Apr 23, 2026
f05d33e
accuse new case routing
chanabyte Apr 23, 2026
b7bfbef
Merge pull request #28 from acm-projects/new-accuse
chanabyte Apr 23, 2026
9f86413
ui color changes on interrogate
chanabyte Apr 24, 2026
0662541
Merge pull request #29 from acm-projects/new-accuse
chanabyte Apr 24, 2026
d1cd69a
ui color changes on interrogate
chanabyte Apr 24, 2026
bfc3df8
Merge pull request #30 from acm-projects/new-accuse
chanabyte Apr 24, 2026
349998c
fixed clueboard and dechoppified wordle
urmipopuri93-stack Apr 24, 2026
c5f38dc
Merge branch 'demo' of https://github.com/acm-projects/Agentic-Detect…
urmipopuri93-stack Apr 24, 2026
60c3a8a
auto-prompt button
chanabyte Apr 24, 2026
0b2a383
Merge pull request #31 from acm-projects/new-accuse
chanabyte Apr 24, 2026
e7c0fa5
archived cases
chanabyte Apr 24, 2026
74eafe8
Merge pull request #32 from acm-projects/new-accuse
chanabyte Apr 24, 2026
24459aa
report update
chanabyte Apr 24, 2026
3b1b203
Merge pull request #33 from acm-projects/new-accuse
chanabyte Apr 24, 2026
db2bb3a
report update
chanabyte Apr 24, 2026
6ae7a15
Merge pull request #34 from acm-projects/new-accuse
chanabyte Apr 24, 2026
1d586bd
location mistake
RedWARd42 Apr 25, 2026
8714ddc
Merge branch 'demo' of https://github.com/acm-projects/Agentic-Detect…
RedWARd42 Apr 25, 2026
707e00b
scan game, merge into demo later
RedWARd42 Apr 25, 2026
939d4db
Merge pull request #35 from acm-projects/scanGame
RedWARd42 Apr 25, 2026
6418358
reduce bg vol
chanabyte Apr 25, 2026
2b3e8d0
Merge pull request #36 from acm-projects/new-accuse
chanabyte Apr 25, 2026
d931313
tutorial changes
chanabyte Apr 27, 2026
b929bb9
gary
chanabyte Apr 27, 2026
bf04076
Merge pull request #37 from acm-projects/new-accuse
chanabyte Apr 27, 2026
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
Binary file added assets/black-bg.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/blueLight.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/minigame.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/normalBackground.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/shoeprint.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
205 changes: 52 additions & 153 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,32 @@ function buildInitialNotesState() {
};
}

const STATIC_COMMUNITY_CASES = [
{
caseCode: 'BEAST-001',
title: 'Beast Boy Case',
author: 'Community Spotlight',
description: 'Track a prank gone wrong at Titans Tower and uncover which clue is actually a trap.',
gameplayRating: 4.8,
updatedAt: null,
},
{
caseCode: 'ACM-001',
title: 'The Night of the Build',
author: 'Community Spotlight',
description: 'A late-night build collapses minutes before demo day. Reconstruct the timeline and expose what really broke.',
gameplayRating: 4.9,
updatedAt: null,
},
];

const STATIC_COMMUNITY_CONTRIBUTORS = [
{ name: 'Swarna', caseCount: 7, averageRating: 3.9, bestRating: 5.0 },
{ name: 'Nandy', caseCount: 6, averageRating: 3.8, bestRating: 5.0 },
{ name: 'Ryan', caseCount: 5, averageRating: 4.7, bestRating: 4.9 },
{ name: 'Urmi', caseCount: 4, averageRating: 2.6, bestRating: 4.8 },
];

// ── Routes ──
app.post('/cases/create', async (req, res) => {
try {
Expand Down Expand Up @@ -349,15 +375,14 @@ app.post('/cases/create', async (req, res) => {
};

// Case content is global and user-agnostic.
const caseResult = await casesCollection.updateOne(
await casesCollection.updateOne(
{ sessionId },
{ $setOnInsert: caseDoc },
{ upsert: true }
);
console.log('[/cases/create] cases write:', caseResult.upsertedCount, 'inserted,', caseResult.matchedCount, 'matched');

// User profile stores which cases this user has created.
const userResult = await usersCollection.updateOne(
await usersCollection.updateOne(
{ userId },
{
$setOnInsert: { userId, createdAt: now },
Expand All @@ -366,19 +391,16 @@ app.post('/cases/create', async (req, res) => {
},
{ upsert: true }
);
console.log('[/cases/create] users write:', userResult.upsertedCount, 'inserted,', userResult.matchedCount, 'matched');

// Game collection stores user-specific gameplay state.
const gameResult = await gameCollection.updateOne(
await gameCollection.updateOne(
{ sessionId, userId },
{ $setOnInsert: gameDoc },
{ upsert: true }
);
console.log('[/cases/create] game write:', gameResult.upsertedCount, 'inserted,', gameResult.matchedCount, 'matched');

res.json({ success: true, sessionId });
} catch (err) {
console.error('[/cases/create] Error:', err.message);
res.status(500).json({ error: err.message });
}
});
Expand Down Expand Up @@ -539,10 +561,8 @@ app.post('/cases/:sessionId/outcome', async (req, res) => {
featured: false,
feedbackAt: null,
},
updatedAt: nowIso(),
lastAutosavedAt: nowIso(),
},
$inc: { revision: 1 }
$inc: { revision: 1 },
}
);

Expand All @@ -558,9 +578,7 @@ app.post('/cases/:sessionId/outcome', async (req, res) => {

app.post('/cases/:sessionId/feedback', async (req, res) => {
try {
const { sessionId } = req.params;
const { gameplayRating, featured } = req.body;
const userId = String(req.body.userId ?? '').trim();

if (![1, 2, 3, 4, 5].includes(Number(gameplayRating))) {
return res.status(400).json({ error: 'gameplayRating must be 1-5' });
Expand All @@ -569,24 +587,7 @@ app.post('/cases/:sessionId/feedback', async (req, res) => {
return res.status(400).json({ error: 'featured must be boolean' });
}

const result = await gameCollection.updateOne(
userId ? { sessionId, userId } : { sessionId },
{
$set: {
'outcome.gameplayRating': Number(gameplayRating),
'outcome.featured': featured,
'outcome.feedbackAt': nowIso(),
updatedAt: nowIso(),
lastAutosavedAt: nowIso(),
},
$inc: { revision: 1 },
}
);

if (result.matchedCount === 0) {
return res.status(404).json({ error: 'Case not found' });
}

// Rating/featured feedback is intentionally static and not persisted in MongoDB.
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
Expand Down Expand Up @@ -642,95 +643,32 @@ app.get('/community/feed', async (req, res) => {
? Math.max(1, Math.min(30, requestedLimit))
: 12;

const gameDocs = await gameCollection
.find(
{ 'outcome.featured': true },
{
projection: {
_id: 0,
userId: 1,
sessionId: 1,
updatedAt: 1,
'outcome.gameplayRating': 1,
},
}
)
.sort({ updatedAt: -1 })
.limit(limit)
.toArray();

const sessionIds = [...new Set(gameDocs.map((doc) => doc.sessionId).filter(Boolean))];
const caseDocs = await casesCollection
.find(
{ sessionId: { $in: sessionIds } },
{
projection: {
_id: 0,
sessionId: 1,
caseId: 1,
'caseData.caseReport.caseId': 1,
'caseData.caseReport.caseTitle': 1,
'caseData.caseReport.officialBriefing': 1,
'caseData.caseReport.setting': 1,
},
}
)
.toArray();

const caseBySessionId = new Map(caseDocs.map((doc) => [doc.sessionId, doc]));

const cases = gameDocs.map((gameDoc) => {
const caseDoc = caseBySessionId.get(gameDoc.sessionId) ?? {};
const report = caseDoc.caseData?.caseReport ?? {};
return {
caseCode: report.caseId ?? caseDoc.caseId ?? gameDoc.sessionId,
title: report.caseTitle ?? 'Untitled Case',
author: gameDoc.userId ? `Detective ${gameDoc.userId.slice(0, 8)}` : 'Anonymous Detective',
description: report.officialBriefing ?? report.setting ?? 'No case description available.',
gameplayRating: Number(gameDoc.outcome?.gameplayRating ?? 0),
updatedAt: gameDoc.updatedAt ?? null,
};
res.json({
cases: STATIC_COMMUNITY_CASES.slice(0, limit),
contributors: STATIC_COMMUNITY_CONTRIBUTORS,
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});

const contributors = await gameCollection
.aggregate([
{
$match: {
userId: { $type: 'string', $ne: '' },
'outcome.featured': true,
'outcome.gameplayRating': { $gte: 1, $lte: 5 },
},
},
{
$group: {
_id: '$userId',
caseCount: { $sum: 1 },
averageRating: { $avg: '$outcome.gameplayRating' },
bestRating: { $max: '$outcome.gameplayRating' },
},
},
{ $sort: { averageRating: -1, caseCount: -1 } },
{ $limit: 8 },
{
$project: {
_id: 0,
userId: '$_id',
caseCount: 1,
averageRating: { $round: ['$averageRating', 2] },
bestRating: 1,
},
},
])
app.get('/cases/user/:userId', async (req, res) => {
console.log("User ID case fetching endpoint reached!!!");
console.log("userId:", req.params.userId);
try {
const { userId } = req.params;

const docs = await db.collection("cases")
.find({ userId }, { projection: { _id: 0 } })
.sort({ updatedAt: -1 })
.toArray();

const formattedContributors = contributors.map((c) => ({
name: `Detective ${String(c.userId).slice(0, 8)}`,
caseCount: Number(c.caseCount ?? 0),
averageRating: Number(c.averageRating ?? 0),
bestRating: Number(c.bestRating ?? 0),
}));
if (!docs.length) {
console.log("No cases found for this user");
return res.json([]);
}

res.json({ cases, contributors: formattedContributors });
res.json(docs);
} catch (err) {
res.status(500).json({ error: err.message });
}
Expand Down Expand Up @@ -809,45 +747,6 @@ app.get('/cases/user/:userId', async (req, res) => {
res.status(500).json({ error: err.message });
}
})

// PATCH is used as it's altering a state of an existing resource (PUT and POST are for creating new entries)
app.patch('/cases/:sessionId/star', async (req, res) => {
try {
const { sessionId } = req.params;
const { userId, isStarred } = req.body;

if (!userId || !String(userId).trim()) {
return res.status(400).json({ error: 'userId is required' });
}

if (typeof isStarred !== 'boolean') {
return res.status(400).json({ error: 'isStarred must be boolean' });
}

const result = await gameCollection.updateOne(
{
$or: [{ sessionId }, { caseId: sessionId }],
userId: String(userId).trim(),
},
{
$set: {
isStarred,
updatedAt: nowIso(),
lastAutosavedAt: nowIso(),
},
$inc: { revision: 1 },
}
);

if (result.matchedCount === 0) {
return res.status(404).json({ error: 'Case not found for this user' });
}

res.json({ success: true, sessionId, isStarred });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
/*
app.post('/case/create', async (req, res) => {
console.log('[/case/create] received:', req.body.caseId);
Expand Down
54 changes: 49 additions & 5 deletions src/Accuse.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
justify-content: center;
align-items: flex-start;
min-height: 100vh;
height: 100vh;
padding: 0;
box-sizing: border-box;
transition: background-color 2.5s ease;
overflow-y: auto;
overflow-x: hidden;
}

.accuse-flash {
Expand Down Expand Up @@ -39,21 +42,59 @@
}

.accuse-verdict {
font-size: clamp(2.2rem, 4vw, 2.6rem);
font-size: clamp(2rem, 5vw, 3.2rem);
font-family: 'Press Start 2P', cursive;
letter-spacing: -1px;
text-transform: uppercase;
margin: 2px 0;
margin: 4px 0 8px;
width: 100%;
text-align: center;
white-space: normal;
overflow-wrap: break-word;
word-break: break-word;
text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.1);
line-height: 1.1;
}

.accuse-guilty {
color: #004f0d;
/* color: #004f0d; */
color: #7a1f1f;
}

.accuse-verdict-name {
display: block;
font-size: clamp(0.95rem, 2.2vw, 1.6rem);
letter-spacing: 0.08em;
line-height: 1;
margin: 0 0 0.4rem 0;
}

.accuse-verdict-word{
display: block;
font-size: clamp(0.6rem, 1.1vw, 0.85rem);
letter-spacing: 0.08em;
line-height: 1;
margin: 0 0 0.4rem 0;
}

.accuse-verdict-stack {
display: grid;
grid-auto-flow: row;
justify-items: center;
align-items: start;
row-gap: 0.35rem;
}

.accuse-verdict-guilty {
display: block;
font-size: clamp(4rem, 15vw, 8rem);
line-height: 1;
letter-spacing: -0.06em;
margin: 0;
color: #7a1f1f;
text-shadow:
3px 3px 0 rgba(0, 0, 0, 0.12),
1px 1px 0 rgba(255, 255, 255, 0.1);
}

.accuse-innocent {
Expand Down Expand Up @@ -131,7 +172,7 @@
color: #101010;
text-align: left;
line-height: 1.8;
margin: 0 0 1.5rem;
margin: 0 0 .5rem;
width: 100%;
border: 2px solid #000000;
padding: 12px 16px;
Expand Down Expand Up @@ -248,9 +289,11 @@

.accuse-buttons .detective-button {
width: 100%;
background: #5a0a0a;
height: 60px;
background: #7a1f1f;
color: #ffffff;
border: 2px solid #2b0000;
filter: none;
}

.accuse-buttons .detective-button:hover {
Expand Down Expand Up @@ -316,6 +359,7 @@

.footer-strip {
margin-top: -14px;
margin-top: 50px;
border-top: 2px solid #000000;
padding-top: 8px;
}
Loading