An interactive storytelling application powered by Next.js and Google's generative AI models.
acto
is an AI-powered interactive storytelling application. Users can start with an initial scenario (either chosen or generated) and make choices that influence the direction of the narrative. The application uses Google's generative AI models (via the @google/genai
SDK) to generate story passages, subsequent choices, and relevant imagery based on the user's input and the story's history. It also features Text-to-Speech (TTS) capabilities using Google Cloud TTS to read passages aloud.
- AI-Generated Narrative: Unique story passages and scenarios crafted by Google AI (via
@google/genai
) based on user choices and story history (prompt refined for coherence and engaging descriptions). - Dynamic Choices: AI generates 3 relevant, distinct choices for the player at each step, influencing the story progression.
- Starting Scenarios: Generates diverse starting points for new stories across different genres (prompt refined for variation and conciseness).
- AI-Generated Images: Images generated (via
@google/genai
) based on the narrative content, reflecting the specified genre, tone, and visual style. - Text-to-Speech (TTS): Reads story passages aloud using Google Cloud TTS. Audio begins playing automatically when ready.
- Stateful Interaction: The application maintains the story history (summary + recent steps) to provide context for the AI.
- User Authentication: (Optional) Secure login via GitHub, Google, and Discord OAuth using NextAuth.
- Rate Limiting: Per-user daily limits for AI text, image, and TTS generation implemented using SQLite.
- Data Persistence: Uses SQLite (
better-sqlite3
) for user data and rate limit tracking. - Responsive Design: Optimized for both desktop and mobile devices using Tailwind CSS.
- Modern UI: Clean interface built with React and Next.js.
- Enhanced & Responsive Story UI: Image-centric layout adapting to different screen sizes, integrated minimal audio controls, subtle glow effect, and fullscreen option.
- Improved Landscape/Fullscreen View: Enhanced CSS for near edge-to-edge image experience on mobile landscape.
- Robust Validation: Uses Zod for validating AI responses.
- State Management: Uses
zustand
withimmer
andpersist
(custom pruning localStorage) for managing client-side application state. - Continuous Deployment: Automatic deployment to Fly.io via GitHub Actions.
- Admin Panel: (Optional) Secure area for administrators to view application data.
- Testing: Includes unit/integration tests (Vitest) and end-to-end tests (Playwright).
You can save your current story progress at any time using the "Save Story" option in the user menu (available when logged in).
This will download a .zip
file containing:
story.json
: A structured representation of your story history, including passages, summaries, choice text, and media file references.prompt_log.json
: A log file detailing the prompts sent to the LLM and the key parts of the response (passage, choices, image prompt, summary) for each step. Useful for debugging or understanding AI behavior.media/
folder: Contains the generated images (.png
) and audio files (.mp3
) for each step.
- Next.js: Latest version using App Router
- React: Latest major version
- Tailwind CSS: Utility-first CSS framework
- TypeScript: Strong typing for code quality
- next-auth: Authentication (GitHub, Google, Discord) (Optional)
@google/genai
: Google AI SDK integration (Text & Image Generation)@google-cloud/text-to-speech
: Google Cloud Text-to-Speech- SQLite (
better-sqlite3
): Database storage (user data, rate limits) - Zod: Schema validation (especially for AI responses)
- zustand / immer / zustand/middleware: Client-side state management with persistence
- @ducanh2912/next-pwa: Progressive Web App features
- Playwright: End-to-end testing
- Vitest / React Testing Library: Unit/Integration testing
- ESLint / Prettier: Linting & Formatting
- Husky / lint-staged: Git hooks
- Fly.io: Deployment platform
- Turbopack: (Optional, used with
npm run dev
)
- (Optional) Sign in: Use GitHub, Google, or Discord authentication.
- Start Story: Choose from AI-generated starting scenarios or begin a default story.
- Receive Passage & Choices: The AI generates the current part of the story and presents 3 choices.
- Make Choice: Select an action/dialogue option.
- AI Responds: The application sends the relevant story history (summary + recent steps) and style context to the AI. The AI generates the outcome, the next passage, new choices, an image prompt, and an updated summary.
- Repeat: Continue making choices and progressing the AI-generated narrative.
The quality of the generated story heavily relies on the prompts sent to the AI. Key strategies include:
- Structured Output: Requesting responses in a specific JSON format using Zod schemas for validation ensures predictable data handling.
- Contextual Awareness: The prompt dynamically includes:
- Style Hints: Genre, Tone, and Visual Style selections.
- Long-Term Context: The AI's previously generated summary of the story so far.
- Short-Term Context: The passages and choices from the last 5 steps.
- Targeted Instructions: Explicit instructions guide the AI on generating engaging passages, distinct choices, relevant image prompts (matching the passage, genre, tone, and style), and concise summaries.
- Efficiency: Initial scenario context is only included in the very first prompt to avoid redundancy.
acto
implements strategies to manage AI API costs:
- Rate Limiting:
- Uses a per-user daily counter stored in the
rate_limits_user
SQLite table (resets at UTC midnight). - Applies separate limits for different Google API types (text generation via
@google/genai
, image generation via@google/genai
, and Google Cloud TTS). - Requires users to be logged in (via NextAuth) to make rate-limited API calls.
- Default limits are defined in
lib/rateLimitSqlite.ts
(e.g., 100 text requests/day, 100 image requests/day, 100 TTS requests/day). - Adjust limits directly in
lib/rateLimitSqlite.ts
or consider moving them to environment variables. - Exceeding the limit returns an error to the user and logs details.
- Uses a per-user daily counter stored in the
- Payload Optimization: Only essential history data (passage, choice, summary) is sent from the client to the server action to avoid exceeding payload size limits.
- Node.js: Version 20 or higher (Check
.nvmrc
). - npm: Package manager.
- Git: For cloning.
- API Keys & Credentials: Obtain necessary keys/secrets (see Environment Variables below).
-
Clone:
git clone https://github.com/rgilks/acto.git # Or your fork cd acto
-
Install:
npm install
Installs dependencies.
-
Initialize Development Environment (First Time):
After installing dependencies for the first time, run:
npm run init:dev
This script performs essential one-time setup for local development:
- Installs Git hooks using Husky (for pre-commit/pre-push checks).
- Downloads the necessary browser binaries for Playwright end-to-end tests.
-
Configure Environment Variables:
- Copy
.env.example
to.env.local
:cp .env.example .env.local
- Edit
.env.local
and fill in the required values. See.env.example
for comments.
Required for Core Functionality:
GOOGLE_AI_API_KEY
: For Google AI (@google/genai
SDK - used for both Text & Image generation). Get from Google AI Studio.GOOGLE_APP_CREDS_JSON
: Contains the single-line JSON content of your Google Cloud service account key file. Required for Cloud Text-to-Speech. Generate the single line usingjq -c . < /path/to/your/keyfile.json
and paste the raw output into.env.local
.
Required for Authentication (if used):
AUTH_SECRET
: Generate withopenssl rand -base64 32
.NEXTAUTH_URL=http://localhost:3000
- OAuth credentials (
GITHUB_ID
/SECRET
, etc.) for enabled providers.
Optional (Remove if not used):
ADMIN_EMAILS
/ALLOWED_EMAILS
: For admin/waiting list access.
- Copy
-
Run Dev Server:
npm run dev
-
Open App: http://localhost:3000
This application is configured for automatic deployment to Fly.io via a GitHub Actions workflow (.github/workflows/fly.yml
).
Deployment Process:
- Trigger: Deployments are automatically triggered on every push to the
main
branch. You can also trigger a deploy manually via the "Actions" tab in GitHub ("Fly Deploy" workflow -> "Run workflow"). - Workflow Steps: The GitHub Action will:
- Check out the code.
- Set up Node.js and install dependencies using
npm ci
. - Run code quality checks (
npm run verify
) and tests (npm test
). - Set up the
flyctl
CLI. - Deploy the application using
flyctl deploy --remote-only
, building the Docker image on Fly.io's infrastructure.
- Secrets: The deployment requires the
FLY_API_TOKEN
secret to be configured in your GitHub repository settings (Settings
>Secrets and variables
>Actions
).
Required Application Secrets on Fly.io:
Ensure the following secrets are set on your Fly.io app dashboard (fly secrets set <KEY>=<VALUE>
). These are needed by the running application, not the build process itself:
GOOGLE_AI_API_KEY
: For Google AI SDK.GOOGLE_APP_CREDS_JSON
: Single-line JSON service account key for Cloud TTS.AUTH_SECRET
: Required if using NextAuth (openssl rand -base64 32
).NEXTAUTH_URL=https://<your-fly-app-name>.fly.dev
: Required if using NextAuth.- OAuth Provider Secrets (
GITHUB_ID
,GITHUB_SECRET
, etc.): Required for specific NextAuth providers. ADMIN_EMAILS
/ALLOWED_EMAILS
: Optional, for restricted access modes.
(Manual Deployment): While automated deployment is recommended, you can still deploy manually from your local machine using fly deploy
after logging in with fly auth login
and ensuring your local .fly/launch.toml
is configured. Remember to set the required secrets locally as well if building locally.
acto
can be configured to operate in a restricted access mode, functioning like a waiting list system.
- Activation: This mode is implicitly activated whenever the
ALLOWED_EMAILS
environment variable is set and contains at least one email address. - Access Control: When active, only users whose email addresses are present in either the
ALLOWED_EMAILS
orADMIN_EMAILS
environment variables will be allowed to sign in or complete the sign-up process. - User Experience: Users attempting to sign in who are not on either list will be redirected to a
/pending-approval
page indicating they are on the waiting list. - Configuration:
- Add non-admin allowed emails to the
ALLOWED_EMAILS
variable in your.env.local
file (for local development) or as a Fly.io secret (for production), separated by commas. - Add admin emails to the
ADMIN_EMAILS
variable (these users gain access regardless of theALLOWED_EMAILS
list). - Important: If you update these secrets on Fly.io, you must redeploy the application (
fly deploy
) for the changes to take effect.
- Add non-admin allowed emails to the
- Disabling: To allow anyone to sign up, simply leave the
ALLOWED_EMAILS
environment variable unset or empty.
To improve performance and reduce unnecessary API calls for visitors who are not logged in, the application now displays a static, hardcoded list of starting scenarios (app/components/StoryStory.tsx
). These have been updated to offer a more diverse and concise set of unique starting points.
The application is configured as a PWA using @ducanh2912/next-pwa
. Users on compatible browsers may be prompted to install the app to their home screen or desktop via a custom, styled prompt (app/components/PWAInstall.tsx
) for easier access and a more app-like experience.
Key scripts defined in package.json
:
# Run dev server (with Turbopack)
npm run dev
# Build for production
npm run build
# Start production server locally
npm run start
# Check formatting & linting
npm run verify
# Fix formatting & linting, run type checks, unit tests, e2e tests
npm run check
# Run unit/integration tests (Vitest)
npm run test
# Run Vitest in watch mode
npm run test:watch
# Run Vitest with coverage report
npm run test:coverage
# Run end-to-end tests (Playwright)
npm run test:e2e
# Check for dependency updates
npm run deps
# Update dependencies interactively
npm run deps:update
# Remove node_modules, lockfile, build artifacts
npm run nuke
- Co-location: Test files (
*.test.ts
,*.test.tsx
) live alongside the source files they test. - Unit/Integration: Vitest and React Testing Library (
npm test
) test components and utility functions. - End-to-End: Playwright (
npm run test:e2e
) checks full user flows through the story.- See E2E Authentication Setup below if testing authenticated features.
- Git Hooks: Husky and lint-staged automatically run checks:
- Pre-commit: Formats staged files (
prettier
) and runs related Vitest tests (test:quick
). - Pre-push: Runs
npm run preview-build
to ensure a preview build succeeds before pushing. (See.husky/pre-push
)
- Pre-commit: Formats staged files (
- AI Costs: Monitor Google AI/Cloud dashboards closely for usage and costs.
- Rate Limits: Adjust limits based on expected traffic, budget, and AI response times.
- Security: Review input handling, especially if user input influences AI prompts. Consider authentication/authorization for saving stories.
- Scalability: Adjust Fly.io machine specs/count in
fly.toml
. Database performance might become a factor if storing large amounts of story history. - Database Backups: Implement a backup strategy for the SQLite volume on Fly.io.
- Prompt Engineering: Continuously refine prompts in
app/actions/story.ts
for better narrative quality, consistency, and JSON adherence.
- AI Prompts: Adjust prompts within
buildStoryPrompt
andgenerateStartingScenariosAction
inapp/actions/story.ts
to change the storytelling style, tone, genre focus, etc. - Story Structure: Modify the requested JSON structure in prompts and the corresponding Zod schemas (
lib/domain/schemas.ts
) if different story elements are desired. - UI/UX: Modify Tailwind classes and component structure in
app/components/
. - Rate Limits: Adjust limits in the relevant action files.
- Auth Providers: Add/remove providers in
lib/authOptions.ts
(or equivalent auth setup file) and update environment variables.
/
├── app/ # Next.js App Router
│ ├── [lang]/ # Language-specific routes (if i18n is kept)
│ │ ├── page.tsx # Main story page component
│ │ └── layout.tsx # Layout for story routes
│ ├── actions/ # Server Actions
│ │ └── story.ts # Core story logic, AI interaction, state updates
│ │ └── tts.ts # Text-to-speech action (optional)
│ ├── api/ # API routes (e.g., auth callbacks)
│ ├── components/ # Shared React components (UI elements)
│ ├── store/ # Zustand state stores (e.g., storyStore)
│ ├── admin/ # Admin panel components/routes (optional)
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Root page (e.g., landing or redirect)
│ └── globals.css # Global styles
├── lib/ # Shared libraries/utilities
│ ├── db.ts # Database connection & schema setup (verify schema)
│ ├── authOptions.ts # NextAuth configuration (if used)
│ ├── modelConfig.ts # AI model configuration & selection
│ ├── domain/ # Domain schemas (Zod, e.g., StorySceneSchema)
│ └── ...
├── public/ # Static assets (images, icons)
├── data/ # SQLite database file (local development)
├── docs/ # Documentation files (e.g., state diagrams - review relevance)
├── test/ # Test configurations and utilities
│ └── e2e/ # Playwright E2E tests & auth state
├── .env.example # Example environment variables
├── next.config.js # Next.js configuration (verify filename)
├── tailwind.config.js # Tailwind CSS configuration (verify filename)
├── tsconfig.json # TypeScript configuration
├── playwright.config.js # Playwright configuration (verify filename)
├── fly.toml / Dockerfile # Deployment configuration
├── package.json # Project dependencies and scripts
└── README.md # This file
Contributions welcome!
- Fork the repository.
- Create branch:
git checkout -b feature/your-feature
. - Commit changes:
git commit -m 'Add cool story element'
. - Push:
git push origin feature/your-feature
. - Open Pull Request.
Accessible at /admin
for users whose email is in ADMIN_EMAILS
.
Features:
- View data from
users
andrate_limits_user
tables. - Basic pagination.
- Requires login; redirects non-admins.
Setup:
- Set
ADMIN_EMAILS
environment variable locally (.env.local
) and in deployment (e.g., Fly.io secrets), comma-separated.
(This section is relevant if testing features requiring login, like the admin panel or user-specific story saves. Review test/e2e/
tests.)
Certain Playwright end-to-end tests (especially those involving /admin
access or user-specific behavior) require pre-generated authentication state to simulate logged-in users.
These state files (test/e2e/auth/*.storageState.json
) contain session information and are not committed to Git (see .gitignore
).
Prerequisites:
- At least one OAuth provider (GitHub, Google, Discord) is configured in your
.env.local
. - The
ADMIN_EMAILS
variable is set in your.env.local
with the email of your designated admin test user. - You have access to both an admin test account and a non-admin test account for one of the configured OAuth providers.
To generate/update the state files locally (Manual Process):
- Ensure Files Exist: If they don't already exist, create empty files named exactly:
test/e2e/auth/admin.storageState.json
test/e2e/auth/nonAdmin.storageState.json
- Run App: Start the development server:
npm run dev
- Login as Admin: Navigate to
http://localhost:3000
and log in as the admin user through one of the configured OAuth providers. - Get Admin Cookie: Open browser dev tools (usually right-click -> Inspect -> Application/Storage tab).
- Find Cookies for
http://localhost:3000
. - Locate the session cookie (e.g.,
next-auth.session-token
or potentiallyauthjs.session-token
- verify the exact name). - Copy its value.
- Find Cookies for
- Update
admin.storageState.json
: Open the file and update thecookies
array. It should look like this (replace placeholders):Important: Ensure the{ "cookies": [ { "name": "next-auth.session-token", // VERIFY THIS COOKIE NAME "value": "YOUR_ADMIN_TOKEN_VALUE_HERE", // PASTE THE COPIED VALUE "domain": "localhost", "path": "/", "expires": -1, // Or the actual expiration Unix timestamp (in seconds) if not -1 "httpOnly": true, "secure": false, // Usually false for localhost "sameSite": "Lax" // Verify if different, e.g., "None" or "Strict" } ], "origins": [ { "origin": "http://localhost:3000", "localStorage": [] // Add any relevant localStorage items if needed by tests } ] }
name
,domain
,path
, andvalue
are correct. Theexpires
field is often-1
for session cookies (expires when browser closes) or a Unix timestamp (in seconds) for persistent cookies. - Log Out & Login as Non-Admin: Log out from the application, then log in as a regular non-admin user.
- Get Non-Admin Cookie: Repeat step 4 for the non-admin user, verifying the cookie name and copying its value.
- Update
nonAdmin.storageState.json
: Paste the non-admin token value and cookie details into this file, using the same JSON structure as above.
Troubleshooting E2E Auth Failures:
- Symptom: Tests for authenticated areas (like
/admin
) fail because the page redirects to the homepage or login page. You might see errors where the test expects content from the admin page (e.g., an "acto admin" heading) but finds content from the homepage (e.g., the main "acto" heading). - Cause: The
*.storageState.json
files (especially the cookievalue
) are likely stale, invalid, or expired. - Solution: Carefully repeat the "To generate/update the state files locally" steps above to refresh the cookie values.
- Double-check the cookie name and value are accurately copied.
- Ensure the domain is
localhost
and path is/
. - If your OAuth provider has short session times, you might need to regenerate these files more frequently.
- Test Modification: The
test/e2e/admin-panel.spec.ts
includes a check that attempts to provide a more specific error if it detects a redirect from/admin
, guiding you to this section.
Verification: After updating the state files, run npm run test:e2e
(or npm run check
which includes E2E tests). Tests requiring authentication should now pass.
Uses SQLite via better-sqlite3
. The database file is data/acto.sqlite
locally, and stored on a persistent volume (/data/acto.sqlite
) in production (Fly.io).
# Navigate to project root
sqlite3 data/acto.sqlite
Useful commands: .tables
, SELECT * FROM users LIMIT 5;
, .schema
, .quit
.
See lib/db.ts
for the definitive schema initialization code. The core tables include:
-- Users table
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_id TEXT NOT NULL,
provider TEXT NOT NULL,
name TEXT,
email TEXT,
image TEXT,
first_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
language TEXT DEFAULT 'en',
UNIQUE(provider_id, provider)
);
-- Rate Limiting table per user/api_type
CREATE TABLE IF NOT EXISTS rate_limits_user (
user_id INTEGER NOT NULL,
api_type TEXT NOT NULL, -- e.g., 'text', 'image', 'tts'
window_start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
request_count INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (user_id, api_type),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_users_last_login ON users(last_login DESC);
CREATE INDEX IF NOT EXISTS idx_rate_limits_user_window ON rate_limits_user(user_id, api_type, window_start_time DESC);
(Note: A saved_stories
table might exist if that feature is implemented; check lib/db.ts
.)
- Database Connection: Ensure
data/
dir exists locally. On Fly, check volume mount (fly.toml
) and status (fly status
). Verify schema inlib/db.ts
matches code expectations. - Auth Errors: Verify
.env.local
/ Fly secrets (AUTH_SECRET
, provider IDs/secrets,NEXTAUTH_URL
). Ensure OAuth callback URLs match. - API Key Errors: Check AI provider keys in env/secrets. Ensure billing/quotas are sufficient. Check
lib/modelConfig.ts
. - AI Errors: Check console logs for errors from the AI API. Ensure the AI is returning valid JSON matching the expected Zod schema in
app/actions/story.ts
. Refine prompts if needed. - Rate Limit Errors: Wait for the daily limit to reset (UTC midnight) or adjust limits in
lib/rateLimitSqlite.ts
if necessary. Checkrate_limits_user
table for current counts. - Admin Access Denied: Confirm logged-in user's email is EXACTLY in
ADMIN_EMAILS
. Check Fly secrets value. - Deployment Issues: Examine GitHub Actions logs and
fly logs --app <your-app-name>
. - State Management Issues: Use React DevTools/Zustand DevTools to inspect story state.
MIT License. See LICENSE file.
- Accessibility Fix: Resolved an
aria-hidden
focus issue related to the user menu dropdown.
(End of File)