Make the production compose stack deployable and disclose the AI-generated catalogue (#329)

This commit is contained in:
Aaron Po
2026-10-08 00:16:37 -04:00
parent bd0e9e4a20
commit 0e51630439
11 changed files with 409 additions and 55 deletions

View File

@@ -0,0 +1,61 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import AiDisclosureDialog from './AiDisclosureDialog';
const aiDisclosureDescription = `Tells visitors that the catalogue is AI-generated fixture data before they engage with it, as the pipeline ethics documentation requires. It opens on every full page load and closes for the rest of that load once acknowledged.`;
const meta = {
title: 'Disclosure/AiDisclosureDialog',
component: AiDisclosureDialog,
tags: ['autodocs'],
args: {
defaultOpen: true,
},
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: aiDisclosureDescription,
},
},
},
} satisfies Meta<typeof AiDisclosureDialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Open: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await waitFor(async () => {
await expect(
canvas.getByRole('heading', { name: /this content is ai-generated/i }),
).toBeVisible();
});
await expect(
canvas.getByRole('link', { name: /how this data was generated/i }),
).toBeVisible();
},
};
export const Acknowledged: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button', { name: /i understand/i }));
await expect(
canvas.queryByRole('heading', { name: /this content is ai-generated/i }),
).toBeNull();
},
};
export const InitiallyDismissed: Story = {
args: {
defaultOpen: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.queryByRole('heading', { name: /this content is ai-generated/i }),
).toBeNull();
},
};

View File

@@ -0,0 +1,79 @@
import { Sparks } from 'iconoir-react';
import { useEffect, useState } from 'react';
const ethicsDocUrl =
'https://github.com/aaronpo97/the-biergarten-app/blob/main/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md';
const AiDisclosureDialog = ({ defaultOpen = true }: { defaultOpen?: boolean }) => {
const [open, setOpen] = useState(defaultOpen);
useEffect(() => {
if (!open) {
return;
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setOpen(false);
}
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [open]);
if (!open) {
return null;
}
return (
<div
className="modal modal-open"
role="dialog"
aria-modal="true"
aria-labelledby="ai-disclosure-title"
>
<div className="modal-box max-w-md">
<div className="flex flex-col items-center gap-3 text-center">
<span className="text-primary">
<Sparks className="size-10" aria-hidden="true" />
</span>
<h2 id="ai-disclosure-title" className="text-lg font-bold">
This content is AI-generated
</h2>
<p className="text-sm text-base-content/70">
The Biergarten App is a proof of concept. Every brewery, beer, and user in
this catalogue is fixture data produced by a language model — none of it
describes a real business, person, or beer.
</p>
<div role="alert" className="alert alert-warning alert-soft text-left">
<span className="text-sm">
Brewing details, local-language text, and coordinates are known to
contain fabrications. Treat nothing here as factual.
</span>
</div>
<a
href={ethicsDocUrl}
target="_blank"
rel="noreferrer"
className="link link-primary text-sm"
>
How this data was generated
</a>
</div>
<div className="modal-action">
<button
type="button"
className="btn btn-primary w-full"
autoFocus
onClick={() => setOpen(false)}
>
I understand
</button>
</div>
</div>
</div>
);
};
export default AiDisclosureDialog;

View File

@@ -13,6 +13,7 @@ import './app.css';
import Navbar from './components/ui/navbar/Navbar';
import ToastProvider from './components/ui/toast/ToastProvider';
import { getOptionalAuth } from './features/auth/auth.server';
import AiDisclosureDialog from './features/home/components/AiDisclosureDialog';
import { defaultThemeName, parseThemeCookie } from './features/theme/themes';
export const links: Route.LinksFunction = () => [
@@ -60,6 +61,7 @@ const App = ({ loaderData }: Route.ComponentProps) => {
<>
<Navbar auth={auth} />
<ToastProvider />
<AiDisclosureDialog />
<Outlet />
</>
);