Skip to content
Open
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This monorepo has two separate applications. If it's ambiguous which one to use,
- `backend`: Python FastAPI server

- **Experiment app** (see [experiment/CLAUDE.md](experiment/CLAUDE.md))
- `experiment`: Separate Next.js application (does not use frontend/backend)
- `experiment`: Separate TanStack Start application (does not use frontend/backend)

<!-- BACKLOG.MD GUIDELINES START -->
# Instructions for the usage of Backlog.md CLI Tool
Expand Down
8 changes: 4 additions & 4 deletions experiment/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
# testing
/coverage

# next.js
/.next/
/out/
# TanStack Start / Vite
/.output/
/dist/
/routeTree.gen.ts

# production
/build
Expand Down Expand Up @@ -43,4 +44,3 @@ logs/

# typescript
*.tsbuildinfo
next-env.d.ts
28 changes: 16 additions & 12 deletions experiment/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

**IMPORTANT: You are working in `/experiment`. The code in `/backend` and `/frontend` is NOT relevant to this project.**

This is a separate Next.js application for experimentation. It does not depend on or interact with the main writing-tools app.
This is a separate TanStack Start application for experimentation. It does not depend on or interact with the main writing-tools app.

## Quick Facts

- **Framework**: Next.js 15 (App Router)
- **Framework**: TanStack Start (powered by Vite + TanStack Router)
- **Language**: TypeScript
- **Package Manager**: `npm` (NOT `uv`)
- **Styling**: Tailwind CSS
Expand Down Expand Up @@ -78,12 +78,12 @@ The experiment supports multiple configurable scenarios. Each scenario includes
- `lib/logging.ts` - Event logging utilities

### API Routes
- `app/api/chat/route.ts` - Chat endpoint (GPT-5.2 with scenario-specific system prompt)
- `app/api/writing-support/route.ts` - AI writing suggestions
- `app/api/log/route.ts` - Event logging endpoint
- `app/api/chat.ts` - Chat endpoint (GPT-5.2 with scenario-specific system prompt)
- `app/api/writing-support.ts` - AI writing suggestions
- `app/api/log.ts` - Event logging endpoint

### Pages (IMPORTANT: Don't confuse these!)
- `app/page.tsx` - **Standalone demo** for AI writing assistance only (NO chat, NOT used in study)
- `app/index.tsx` - **Standalone demo** for AI writing assistance only (NO chat, NOT used in study)
- `components/study/TaskPage.tsx` - **Actual study task page** with collapsible chat + AI panel

### Timing for the Simulated Colleague
Expand All @@ -108,7 +108,7 @@ cd experiment
npm install
```

Create `.env.local`:
Create `.env`:
```
OPENAI_API_KEY=sk-...
```
Expand All @@ -125,9 +125,13 @@ Open http://localhost:3000
```
experiment/
├── app/
│ ├── api/ # API routes (chat, writing-support)
│ ├── layout.tsx # Root layout
│ └── page.tsx # Main page
│ ├── __root.tsx # Root layout (TanStack Start)
│ ├── index.tsx # Home page route
│ ├── study.tsx # Study page route
│ ├── api/ # API routes (chat, writing-support, log)
│ └── globals.css # Global styles
├── router.tsx # TanStack Router configuration
├── vite.config.ts # Vite + TanStack Start config
├── components/ # React components
├── contexts/ # Context providers
├── lib/ # Utilities
Expand Down Expand Up @@ -156,8 +160,8 @@ npm test # Run tests

## Key Files

- **API Routes**: `app/api/` (chat, writing-support endpoints)
- **Demo Page**: `app/page.tsx` (standalone AI demo, NO chat)
- **API Routes**: `app/api/` (chat, writing-support, log endpoints)
- **Demo Page**: `app/index.tsx` (standalone AI demo, NO chat)
- **Study Task Page**: `components/study/TaskPage.tsx` (the actual study with chat)
- **Components**: `components/` folder

Expand Down
20 changes: 8 additions & 12 deletions experiment/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@ COPY . .
ARG GIT_COMMIT=unknown

# Set environment variables for build
ENV NEXT_PUBLIC_GIT_COMMIT=${GIT_COMMIT}
ENV DOCKER_BUILD=true
ENV VITE_GIT_COMMIT=${GIT_COMMIT}

# Build the Next.js application
# Build the application
RUN npm run build

# Stage 3: Production runtime
FROM node:24-slim AS runner
# Note: unlike the backend Dockerfile, this container doesn't set WORKDIR to a subfolder of /app
WORKDIR /app

# Set production environment
Expand All @@ -41,19 +39,17 @@ ARG EXP_LOGS_GID=1001

# Create non-root user for security
RUN addgroup --system --gid ${EXP_LOGS_GID} appgroup \
&& adduser --system --uid 1001 --ingroup appgroup nextjs
&& adduser --system --uid 1001 --ingroup appgroup appuser

# Copy standalone output from builder
COPY --from=builder /app/.next/standalone ./
# Copy static files (Next.js doesn't include these in standalone by default)
COPY --from=builder /app/.next/static ./.next/static
# Copy built output from builder
COPY --from=builder /app/.output ./.output
COPY --from=builder /app/public ./public

# Set correct permissions
RUN chown -R nextjs:appgroup /app
RUN chown -R appuser:appgroup /app

# Switch to non-root user
USER nextjs
USER appuser

# Expose the port
EXPOSE 3000
Expand All @@ -66,4 +62,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"

# Start the application
CMD ["node", "server.js"]
CMD ["node", ".output/server/index.mjs"]
50 changes: 50 additions & 0 deletions experiment/app/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/// <reference types="vite/client" />
import type { ReactNode } from 'react'
import {
Outlet,
createRootRoute,
HeadContent,
Scripts,
} from '@tanstack/react-router'
import { Provider } from 'jotai'
import appCss from './globals.css?url'

export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'Writing Task' },
{ name: 'description', content: 'AI-powered writing task with chat support' },
],
links: [
{ rel: 'stylesheet', href: appCss },
{ rel: 'icon', href: '/favicon.ico' },
],
}),
component: RootComponent,
})

function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}

function RootDocument({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body className="antialiased">
<Provider>
{children}
</Provider>
<Scripts />
</body>
</html>
)
}
24 changes: 24 additions & 0 deletions experiment/app/api/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createFileRoute } from '@tanstack/react-router'
import { openai } from '@ai-sdk/openai'
import { convertToModelMessages, streamText } from 'ai'
import { getScenario } from '@/lib/studyConfig'

export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const { messages, scenario: scenarioId } = await request.json()
const scenario = getScenario(scenarioId)

const result = streamText({
model: openai('gpt-5.2'),
system: scenario.chat.systemPrompt,
messages: convertToModelMessages(messages),
maxOutputTokens: 300,
})

return result.toUIMessageStreamResponse()
},
},
},
})
19 changes: 0 additions & 19 deletions experiment/app/api/chat/route.ts

This file was deleted.

67 changes: 67 additions & 0 deletions experiment/app/api/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createFileRoute } from '@tanstack/react-router'
import type { LogEntry } from '@/types/study'
import { appendFile, mkdir, realpath } from 'node:fs/promises'
import { resolve } from 'node:path'

const LOGS_DIR = resolve(process.cwd(), 'logs')

/**
* Validate username format
*/
function isValidUsername(username: string): boolean {
return /^[a-zA-Z0-9\-_]+$/.test(username) && username.length > 0;
}

export const Route = createFileRoute('/api/log')({
server: {
handlers: {
POST: async ({ request }) => {
try {
const body = await request.json();
const entry = body as LogEntry;
const username = entry.username;

// Validate username format
if (!isValidUsername(username)) {
return Response.json(
{ error: 'Invalid username format' },
{ status: 400 }
);
}

// Create logs directory if it doesn't exist and get its real path
await mkdir(LOGS_DIR, { recursive: true });
const realLogsDir = await realpath(LOGS_DIR);

// Construct log file path using validated username
const logFilePath = resolve(realLogsDir, `${username}.jsonl`);

// Verify the resolved path is within the logs directory (prevent directory traversal)
if (!logFilePath.startsWith(`${realLogsDir}/`)) {
return Response.json(
{ error: 'Invalid log file path' },
{ status: 400 }
);
}

// Append entry to participant's log file as JSONL
const logLine = JSON.stringify(entry) + '\n';

await appendFile(logFilePath, logLine, 'utf-8');

return Response.json(
{ success: true, message: 'Log entry written' },
{ status: 200 }
);
} catch (error) {
console.error('Logging error:', error);

return Response.json(
{ success: false, message: 'Internal error logging' },
{ status: 500 }
);
}
},
},
},
})
63 changes: 0 additions & 63 deletions experiment/app/api/log/route.ts

This file was deleted.

Loading
Loading