Skip to content
Merged
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
Binary file added workshop-ui/public/apple-icon.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 workshop-ui/public/favicon.ico
Binary file not shown.
3 changes: 3 additions & 0 deletions workshop-ui/public/icon0.svg
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 workshop-ui/public/icon1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions workshop-ui/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "CoderDojo AI Workshops",
"short_name": "CD AI",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file added workshop-ui/public/web-app-manifest-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions workshop-ui/src/app/api/auth/me/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getAppSessionFromRequest, validateAppSession } from "@/lib/session"
import { NextRequest, NextResponse } from "next/server"

/**
* @route GET /api/auth/me
* @desc Get current authentication status
*
* @access Public
*/
export async function GET(request: NextRequest) {
// Check current authentication status
const response = NextResponse.json({ authenticated: false })

try {
const session = await getAppSessionFromRequest(request, response)

if (await validateAppSession(session)) {
return NextResponse.json({
authenticated: true,
workshopName: session.workshopName,
})
} else {
return NextResponse.json({}, { status: 401 })
}
} catch (error) {
console.error('Session check error:', error)
return NextResponse.json({}, { status: 401 })
}
}
90 changes: 90 additions & 0 deletions workshop-ui/src/app/api/auth/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { NextRequest, NextResponse } from 'next/server'
import { getAppSessionFromRequest, validateAccessCode, validateAppSession } from '@/lib/session'
import { readWorkshops } from '@/lib/workshopService'
import { Workshop } from '@/lib/workshop-schema'

/**
* @route POST /api/auth
* @desc Handle login and logout actions
* @body { action: 'login' | 'logout', code?: string }
* @response 200 { success: boolean } or 400/401 { error: string }
* @access Public
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { action, code } = body
const response = NextResponse.json({ success: true })

if (action === 'login') {
if (await validateAccessCode(code)) {
// Set session variables
const session = await getAppSessionFromRequest(request, response)
session.accessCode = code
session.isAuthenticated = true
session.recheckCode = new Date(Date.now() + 1000 * 60 * 30) // recheck code in 30 minutes

// Find a workshop associated with this access code
const workshops = await readWorkshops()
const workshop = workshops.find((w: Workshop) => w.code === code)
if (workshop) {
session.workshopName = workshop.title
} else {
session.workshopName = 'Unknown Workshop'
}
console.log(`User logged in to workshop: ${JSON.stringify(session)}`)
await session.save()

return response
} else {
// report invalid code to client
return NextResponse.json(
{ error: 'Invalid access code' },
{ status: 401 }
)
}
} else if (action === 'logout') {
// Clear the session
const session = await getAppSessionFromRequest(request, response)
session.destroy()

return response
}

return NextResponse.json(
{ error: 'Invalid action' },
{ status: 400 }
)
} catch (error) {
console.error('Auth API error:', error)
return NextResponse.json(
{ error: 'Invalid request' },
{ status: 400 }
)
}
}

/**
* @route GET /api/auth
* @desc Get current authentication status
* @response { authenticated: boolean }
* @access Public
*/
export async function GET(request: NextRequest) {
// Check current authentication status
const response = NextResponse.json({ authenticated: false })

try {
const session = await getAppSessionFromRequest(request, response)
const isAuthenticated = await validateAppSession(session)

return NextResponse.json({
authenticated: isAuthenticated
})
} catch (error) {
console.error('Session check error:', error)
return NextResponse.json({
authenticated: false
})
}
}
19 changes: 17 additions & 2 deletions workshop-ui/src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import OpenAI, { AzureOpenAI } from 'openai';
import fs from 'fs';
import path from 'path';
import { getSession } from '@/lib/session';
import { getAppSessionFromRequest, getChatSession, validateAppSession } from '@/lib/session';
import { randomUUID } from 'crypto';
import '@/lib/files';
import { trace, Span } from '@opentelemetry/api';
Expand All @@ -16,8 +16,23 @@ const tracer = trace.getTracer('ai-workshop-chat');
// TODO: Persist this to a database later
const sessionResponseMap = new Map<string, { previousResponseId: string; sessionInstanceId: string }>();

/**
* @route POST /api/chat
* @desc Send a chat message
* @body { message: string, resetConversation: boolean }
* @urlParam exercise: string
* @response 200 { response: string } or 400 { error: string }
* @access Protected (any authenticated user/workshop)
*/
export async function POST(request: NextRequest) {
try {
// Validate authentication
const response = NextResponse.next();
const appSession = await getAppSessionFromRequest(request, response);
if (!await validateAppSession(appSession)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

// Get body payload and query string parameters
const { message, resetConversation } = await request.json();
if (!message || typeof message !== 'string') {
Expand All @@ -36,7 +51,7 @@ export async function POST(request: NextRequest) {
const exerciseData = exerciseResult.value;

// Get or create session ID
const session = await getSession();
const session = await getChatSession();
let sessionId = session.sessionId;

if (!sessionId) {
Expand Down
15 changes: 15 additions & 0 deletions workshop-ui/src/app/api/exercises/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import { getExerciseByNameWithResponse } from '@/lib/exercise-file-manager';
import { getAppSessionFromRequest, validateAppSession } from '@/lib/session';

type ExerciseResponse = {
title: string;
Expand All @@ -12,11 +13,25 @@ type ExerciseResponse = {
data_files_content?: { [filename: string]: string };
};

/**
* @route GET /api/exercises/:id
* @desc Get exercise metadata by ID (folder name)
* @query includeDataFileContent: boolean (optional, default false) - whether to include the content of data files
* @response 200 { title, folder, system_prompt_file, welcome_message (if exists), data_files, data_files_content (if requested) } or 404 { error: string }
* @access Protected (any authenticated user/workshop)
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Validate authentication
const nextResponse = NextResponse.next();
const appSession = await getAppSessionFromRequest(request, nextResponse);
if (!await validateAppSession(appSession)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { id: exerciseId } = await params;

// Extract query parameter
Expand Down
27 changes: 21 additions & 6 deletions workshop-ui/src/app/api/exercises/[id]/system-prompt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,43 @@ import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import { getExerciseByNameWithResponse } from '@/lib/exercise-file-manager';
import { getAppSessionFromRequest, validateAppSession } from '@/lib/session';

/**
* @route GET /api/exercises/:id/system-prompt
* @desc Get the system prompt content for a specific exercise by ID (folder name)
* @query id
* @response 200 text/plain or 500/404 { error: string }
* @access Protected (any authenticated user/workshop)
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Validate authentication
const response = NextResponse.next();
const appSession = await getAppSessionFromRequest(request, response);
if (!await validateAppSession(appSession)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { id: exerciseId } = await params;

const exerciseResult = await getExerciseByNameWithResponse(exerciseId);
if (!exerciseResult.success) {
return exerciseResult.error;
}

const exercise = exerciseResult.value;


// Read the system prompt file
const systemPromptPath = path.join(process.cwd(), 'prompts', exercise.folder, exercise.system_prompt_file);

try {
const systemPrompt = await fs.promises.readFile(systemPromptPath, 'utf8');

// Return as plain text
return new NextResponse(systemPrompt, {
headers: {
Expand All @@ -34,7 +49,7 @@ export async function GET(
console.error('Error reading system prompt file:', fileError);
return NextResponse.json({ error: 'System prompt file not found' }, { status: 404 });
}

} catch (error) {
console.error('Error retrieving system prompt:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
Expand Down
15 changes: 15 additions & 0 deletions workshop-ui/src/app/api/exercises/[id]/task-sheet/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,26 @@ import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import { getExerciseByNameWithResponse } from '@/lib/exercise-file-manager';
import { getAppSessionFromRequest, validateAppSession } from '@/lib/session';

/**
* @route GET /api/exercises/:id/task-sheet
* @desc Get the task sheet content for a specific exercise by ID (folder name)
* @query id
* @response 200 text/plain or 500/404 { error: string }
* @access Protected (any authenticated user/workshop)
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
// Validate authentication
const nextResponse = NextResponse.next();
const appSession = await getAppSessionFromRequest(request, nextResponse);
if (!await validateAppSession(appSession)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

try {
const { id: exerciseId } = await params;

Expand Down
Loading