aegida-console / docs / superpowers / plans / 2026-07-26-corporate-ai-chat.md
2026-07-26-corporate-ai-chat.md
Raw

Corporate AI Chat Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a ChatGPT-style corporate chat UI with one-day header-based JWT authentication, local chat history, three model routes, and a single streaming AI Gateway adapter.

Architecture: A client-side auth gate validates a JWT from localStorage against protected Next.js Route Handlers before rendering the chat. Server-only auth and gateway modules normalize security, validation, and streaming while focused React components own the responsive chat UI and device-local state.

Tech Stack: Next.js 16.2, React 19.2, TypeScript 5, Tailwind CSS 4, shadcn/Base UI, Lucide React, jose, Vitest, React Testing Library, JSDOM.

Global Constraints

  • Use one GatewayAdapter; provider-specific logic must not exist in this application.
  • Supported route IDs are exactly chatgpt, deepseek, and qwen.
  • JWT is HS256, sent only as Authorization: Bearer <JWT>, and expires exactly 24 hours after issue.
  • JWT is persisted under ai-control-center:auth-token; do not use cookies.
  • Test credentials are demo@ai-control.local and Demo1234!, supplied through server-only environment variables.
  • The user JWT must never be forwarded to AI Gateway.
  • Chat history is device-local and namespaced by authenticated user ID.
  • Render model output as plain text with preserved line breaks; never inject raw HTML.
  • A request allows at most 100 messages, 32,000 characters per message, and 128,000 characters total.
  • Preserve the existing Next.js, npm, shadcn, and Tailwind structure.

File Structure

  • lib/auth/types.ts: serializable public user identity shared by server and client.
  • lib/auth/server.ts: JWT signing, verification, credentials comparison, and Bearer extraction.
  • lib/auth/client.ts: browser token persistence and authenticated fetch helper.
  • app/api/auth/login/route.ts: credential exchange for a one-day JWT.
  • app/api/auth/me/route.ts: protected identity probe used during startup.
  • components/auth/auth-gate.tsx: startup auth state machine and login/chat switching.
  • components/auth/login-screen.tsx: accessible corporate login form.
  • lib/chat/types.ts: shared model, message, conversation, and request types.
  • lib/chat/models.ts: model display configuration only.
  • lib/chat/storage.ts: validated, user-namespaced localStorage persistence.
  • lib/chat/validation.ts: runtime validation and request limits.
  • lib/chat/gateway.ts: single HTTP/mock streaming adapter.
  • app/api/chat/route.ts: authenticated validated streaming endpoint.
  • hooks/use-chat.ts: chat lifecycle, streaming, cancellation, retry, and persistence.
  • components/chat/*: focused sidebar, header, empty state, message list, and composer views.
  • components/ui/input.tsx, components/ui/textarea.tsx: missing shadcn form primitives.
  • app/page.tsx: thin entry point that renders the auth gate.
  • app/globals.css: product visual system and responsive base behavior.
  • app/layout.tsx: Russian metadata and document language.
  • next.config.ts: CSP and security response headers.

Task 1: Test Harness and Server Authentication

Files:

  • Modify: package.json
  • Modify: package-lock.json
  • Modify: .gitignore
  • Create: .env.example
  • Create locally, do not commit: .env.local
  • Create: vitest.config.mts
  • Create: vitest.setup.ts
  • Create: lib/auth/types.ts
  • Create: lib/auth/server.ts
  • Create: lib/auth/server.test.ts
  • Create: app/api/auth/login/route.ts
  • Create: app/api/auth/me/route.ts
  • Create: app/api/auth/routes.test.ts

Interfaces:

  • Produces: AuthIdentity, VerifiedAuthToken, signAuthToken(user), verifyAuthToken(token), authenticateRequest(request), and JSON auth routes.

  • Consumes: server-only AUTH_USER_ID, AUTH_EMAIL, AUTH_PASSWORD, and JWT_SECRET.

  • Step 1: Install auth and test dependencies and add scripts

Run:

npm install jose server-only
npm install --save-dev vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event vite-tsconfig-paths

Add scripts:

"test": "vitest run",
"test:watch": "vitest"
  • Step 2: Configure Vitest and environment templates

Create vitest.config.mts:

import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [tsconfigPaths(), react()],
  test: {
    environment: "jsdom",
    setupFiles: ["./vitest.setup.ts"],
    clearMocks: true,
  },
});

Create vitest.setup.ts:

import "@testing-library/jest-dom/vitest";

Add !.env.example after .env* in .gitignore. Commit this template:

AUTH_USER_ID=demo-user
AUTH_EMAIL=demo@ai-control.local
AUTH_PASSWORD=change-me
JWT_SECRET=replace-with-at-least-32-random-characters
AI_GATEWAY_MOCK=true
AI_GATEWAY_URL=
AI_GATEWAY_API_KEY=

Create the ignored .env.local for immediate local use:

AUTH_USER_ID=demo-user
AUTH_EMAIL=demo@ai-control.local
AUTH_PASSWORD=Demo1234!
JWT_SECRET=ai-control-center-local-development-secret-2026
AI_GATEWAY_MOCK=true
  • Step 3: Write failing JWT and Bearer tests

Create tests that pin the public API:

it("issues an HS256 token for exactly 24 hours", async () => {
  const token = await signAuthToken({ id: "demo-user", email: "demo@ai-control.local" });
  const user = await verifyAuthToken(token);
  expect(user).toMatchObject({ id: "demo-user", email: "demo@ai-control.local" });
  expect(user.exp - user.iat).toBe(86_400);
});

it("rejects missing and malformed bearer authorization", async () => {
  await expect(authenticateRequest(new Request("http://localhost/api/chat"))).rejects.toMatchObject({ status: 401 });
  await expect(authenticateRequest(new Request("http://localhost/api/chat", {
    headers: { Authorization: "Token invalid" },
  }))).rejects.toMatchObject({ status: 401 });
});
  • Step 4: Run the auth tests and verify failure

Run: npm test -- lib/auth/server.test.ts

Expected: FAIL because lib/auth/server.ts does not exist.

  • Step 5: Implement the server-only auth module

Use jose with an explicit algorithm allowlist and stable claims:

import "server-only";
import { SignJWT, jwtVerify } from "jose";
import type { AuthIdentity } from "@/lib/auth/types";

export type VerifiedAuthToken = AuthIdentity & { iat: number; exp: number };

export async function signAuthToken(user: AuthIdentity): Promise<string> {
  return new SignJWT({ email: user.email })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(user.id)
    .setIssuedAt()
    .setExpirationTime("24h")
    .sign(new TextEncoder().encode(requireEnv("JWT_SECRET")));
}

export async function verifyAuthToken(token: string): Promise<VerifiedAuthToken> {
  const { payload } = await jwtVerify(token, new TextEncoder().encode(requireEnv("JWT_SECRET")), {
    algorithms: ["HS256"],
  });
  if (!payload.sub || typeof payload.email !== "string" || !payload.iat || !payload.exp) throw unauthorized();
  return { id: payload.sub, email: payload.email, iat: payload.iat, exp: payload.exp };
}

Define AuthIdentity as { id: string; email: string } in lib/auth/types.ts. Add credentialsMatch(email, password) using constant-time string comparisons and authenticateRequest(request) requiring the exact case-insensitive Bearer scheme with a non-empty token. Normalize all verification failures to an AuthError with status 401.

  • Step 6: Write failing route tests

Cover successful login, generic invalid-credentials response, malformed JSON, and /api/auth/me with a valid/invalid Bearer token:

const response = await login(new Request("http://localhost/api/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "demo@ai-control.local", password: "Demo1234!" }),
}));
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ token: expect.any(String), user: { id: "demo-user" } });
  • Step 7: Implement login and identity Route Handlers

POST /api/auth/login returns { token, user: { id, email } } on success, { error: "Неверная почта или пароль" } with 401 on mismatch, and { error: "Некорректный запрос" } with 400 on invalid JSON. GET /api/auth/me returns { user: { id, email } } after authenticateRequest and a generic 401 otherwise. Both responses use Cache-Control: no-store.

  • Step 8: Run auth tests and commit

Run: npm test -- lib/auth/server.test.ts app/api/auth/routes.test.ts

Expected: PASS.

Commit:

git add package.json package-lock.json .gitignore .env.example vitest.config.mts vitest.setup.ts lib/auth app/api/auth
git commit -m "feat: add header-based JWT authentication"

Task 2: Auth Bootstrap and Login Screen

Files:

  • Create: lib/auth/client.ts
  • Create: components/auth/login-screen.tsx
  • Create: components/auth/auth-gate.tsx
  • Create: components/auth/auth-gate.test.tsx
  • Create: components/chat/chat-app.tsx as the authenticated shell, expanded in Task 5
  • Create: components/ui/input.tsx
  • Modify: app/page.tsx

Interfaces:

  • Consumes: GET /api/auth/me, POST /api/auth/login, AuthIdentity.

  • Produces: AuthGate, authFetch(input, init), getStoredToken(), clearStoredToken().

  • Step 1: Write failing auth-gate tests

Use fake localStorage and mocked fetch to pin three states:

it("never renders login while a stored token is being validated", async () => {
  localStorage.setItem(AUTH_TOKEN_KEY, "valid-token");
  vi.mocked(fetch).mockReturnValue(new Promise(() => undefined));
  render(<AuthGate />);
  expect(screen.getByLabelText("Проверяем доступ")).toBeInTheDocument();
  expect(screen.queryByRole("heading", { name: "Вход" })).not.toBeInTheDocument();
});

it("opens the chat after a valid identity response", async () => {
  localStorage.setItem(AUTH_TOKEN_KEY, "valid-token");
  vi.mocked(fetch).mockResolvedValue(jsonResponse({ user: { id: "demo-user", email: "demo@ai-control.local" } }));
  render(<AuthGate />);
  expect(await screen.findByText("Новый чат")).toBeInTheDocument();
});

Also verify absent/invalid tokens show login, successful login stores the token, every identity request includes Authorization: Bearer valid-token, and logout removes it.

  • Step 2: Run the auth-gate tests and verify failure

Run: npm test -- components/auth/auth-gate.test.tsx

Expected: FAIL because the auth client and components do not exist.

  • Step 3: Implement browser token helpers

Create a focused module:

export const AUTH_TOKEN_KEY = "ai-control-center:auth-token";

export function getStoredToken() {
  return window.localStorage.getItem(AUTH_TOKEN_KEY);
}

export async function authFetch(input: RequestInfo | URL, init: RequestInit = {}) {
  const token = getStoredToken();
  const headers = new Headers(init.headers);
  if (token) headers.set("Authorization", `Bearer ${token}`);
  return fetch(input, { ...init, headers });
}

Also implement storeToken and clearStoredToken without accessing window at module initialization.

  • Step 4: Implement the login screen and auth state machine

AuthGate is a client component with checking, anonymous, and authenticated states. On mount it validates any stored token through authFetch("/api/auth/me", { cache: "no-store" }); 401 clears storage. Successful login stores the returned token and user. Logout clears the token and in-memory user.

LoginScreen uses labeled email/password inputs, autoComplete, submit loading state, disabled empty submit, and an aria-live="polite" generic error. The heading is Вход, the product name is AI Control Center, and the primary action is Продолжить.

Create the initial ChatApp({ user, onLogout }) authenticated shell with the visible Новый чат label and a working logout action. Task 5 replaces its body with the complete chat experience without changing its public props.

  • Step 5: Run tests and commit

Run: npm test -- components/auth/auth-gate.test.tsx

Expected: PASS.

Commit:

git add lib/auth/client.ts components/auth components/chat/chat-app.tsx components/ui/input.tsx app/page.tsx
git commit -m "feat: add persistent login experience"

Task 3: Chat Domain and User-Scoped Persistence

Files:

  • Create: lib/chat/types.ts
  • Create: lib/chat/models.ts
  • Create: lib/chat/storage.ts
  • Create: lib/chat/storage.test.ts

Interfaces:

  • Produces: ModelId, ChatMessage, Conversation, ChatRequest, MODELS, loadConversations(userId), saveConversations(userId, conversations).

  • Consumes: authenticated user.id from AuthGate.

  • Step 1: Write failing storage and model tests

it("namespaces conversations by authenticated user", () => {
  saveConversations("user-a", [conversation]);
  expect(loadConversations("user-a")).toEqual([conversation]);
  expect(loadConversations("user-b")).toEqual([]);
});

it("discards malformed persisted data", () => {
  localStorage.setItem(storageKey("demo-user"), "{not-json");
  expect(loadConversations("demo-user")).toEqual([]);
});

it("contains only gateway route identifiers", () => {
  expect(MODELS.map((model) => model.id)).toEqual(["chatgpt", "deepseek", "qwen"]);
});
  • Step 2: Run the domain tests and verify failure

Run: npm test -- lib/chat/storage.test.ts

Expected: FAIL because the chat domain modules do not exist.

  • Step 3: Implement types and model display configuration
export type ModelId = "chatgpt" | "deepseek" | "qwen";
export type MessageRole = "user" | "assistant";
export type MessageStatus = "streaming" | "complete" | "error" | "stopped";

export type ChatMessage = {
  id: string;
  role: MessageRole;
  content: string;
  createdAt: string;
  status: MessageStatus;
};

export type Conversation = {
  id: string;
  title: string;
  modelId: ModelId;
  updatedAt: string;
  messages: ChatMessage[];
};

Create MODELS with Russian descriptions: ChatGPT as a universal assistant, DeepSeek for reasoning/code, and Qwen for multilingual work. Do not include provider API endpoints or concrete upstream model versions.

  • Step 4: Implement defensive local storage

Use ai-control-center:chats:${encodeURIComponent(userId)}. Parse unknown JSON with explicit shape checks for IDs, roles, statuses, dates, and supported model IDs. Sort loaded conversations newest-first, cap persistence at 50 conversations, and return [] on browser storage exceptions.

  • Step 5: Run tests and commit

Run: npm test -- lib/chat/storage.test.ts

Expected: PASS.

Commit:

git add lib/chat
git commit -m "feat: add local chat domain"

Task 4: Single Streaming AI Gateway Adapter

Files:

  • Create: lib/chat/validation.ts
  • Create: lib/chat/validation.test.ts
  • Create: lib/chat/gateway.ts
  • Create: lib/chat/gateway.test.ts
  • Create: app/api/chat/route.ts
  • Create: app/api/chat/route.test.ts

Interfaces:

  • Consumes: ChatRequest, authenticateRequest(request), AI_GATEWAY_URL, AI_GATEWAY_API_KEY, and AI_GATEWAY_MOCK.

  • Produces: validateChatRequest(value), streamGatewayResponse(request, signal), authenticated POST /api/chat text stream.

  • Step 1: Write failing request validation tests

Cover the happy path and each exact limit:

expect(validateChatRequest({ model: "chatgpt", messages: [{ role: "user", content: "Привет" }] })).toEqual({
  model: "chatgpt",
  messages: [{ role: "user", content: "Привет" }],
});
expect(() => validateChatRequest({ model: "other", messages: [] })).toThrow(ChatValidationError);
expect(() => validateChatRequest({ model: "qwen", messages: Array.from({ length: 101 }, () => ({ role: "user", content: "x" })) })).toThrow(ChatValidationError);

Also test a 32,001-character message and a 128,001-character total.

  • Step 2: Run validation tests and verify failure

Run: npm test -- lib/chat/validation.test.ts

Expected: FAIL because validation is absent.

  • Step 3: Implement strict validation

Reject non-objects, unknown models, empty arrays, unsupported roles, empty/whitespace-only content, more than 100 messages, more than 32,000 characters in one message, and more than 128,000 total characters. Return a new normalized object containing only model, role, and content.

  • Step 4: Write failing gateway tests
it("sends one normalized request to the configured gateway", async () => {
  process.env.AI_GATEWAY_URL = "https://gateway.example/v1/chat";
  process.env.AI_GATEWAY_API_KEY = "gateway-secret";
  vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("answer")));
  const stream = await streamGatewayResponse(request, new AbortController().signal);
  expect(fetch).toHaveBeenCalledWith("https://gateway.example/v1/chat", expect.objectContaining({
    method: "POST",
    headers: expect.objectContaining({ Authorization: "Bearer gateway-secret" }),
    body: JSON.stringify(request),
  }));
  expect(await new Response(stream).text()).toBe("answer");
});

Also verify the outgoing request never contains the user's JWT, forwards the provided AbortSignal, returns deterministic chunks in mock mode, emits 503 when unconfigured, and normalizes non-2xx gateway responses without leaking their body.

  • Step 5: Implement the one-adapter transport

Define GatewayError with safe status and message. streamGatewayResponse(request, signal) selects HTTP when AI_GATEWAY_URL is present, otherwise mock only when AI_GATEWAY_MOCK === "true". HTTP sends JSON and an optional gateway key. Mock mode returns a UTF-8 ReadableStream whose Russian response mentions the selected display model and echoes no secrets; its timers stop when the signal aborts.

  • Step 6: Write failing chat route tests

Verify 401 is returned before body validation, 400 for invalid authenticated payloads, 503 for missing transport, and a successful response has Content-Type: text/plain; charset=utf-8, Cache-Control: no-store, and streamed text.

  • Step 7: Implement the protected chat route
export async function POST(request: Request) {
  try {
    await authenticateRequest(request);
    const chatRequest = validateChatRequest(await request.json());
    const stream = await streamGatewayResponse(chatRequest, request.signal);
    return new Response(stream, {
      headers: {
        "Content-Type": "text/plain; charset=utf-8",
        "Cache-Control": "no-store",
        "X-Content-Type-Options": "nosniff",
      },
    });
  } catch (error) {
    return chatErrorResponse(error);
  }
}

Ensure JSON parse failures become 400, auth errors remain 401, configuration/gateway errors are safe 503/502, and unknown errors return generic 500.

  • Step 8: Run gateway tests and commit

Run: npm test -- lib/chat/validation.test.ts lib/chat/gateway.test.ts app/api/chat/route.test.ts

Expected: PASS.

Commit:

git add lib/chat app/api/chat
git commit -m "feat: add streaming AI Gateway adapter"

Task 5: Chat Experience and Streaming State

Files:

  • Create: hooks/use-chat.ts
  • Create: hooks/use-chat.test.tsx
  • Modify: components/chat/chat-app.tsx
  • Create: components/chat/chat-app.test.tsx
  • Create: components/chat/chat-sidebar.tsx
  • Create: components/chat/chat-header.tsx
  • Create: components/chat/model-selector.tsx
  • Create: components/chat/chat-empty-state.tsx
  • Create: components/chat/chat-message-list.tsx
  • Create: components/chat/chat-message.tsx
  • Create: components/chat/chat-composer.tsx
  • Create: components/ui/textarea.tsx
  • Modify: components/auth/auth-gate.tsx

Interfaces:

  • Consumes: authenticated user, authFetch, chat types, MODELS, storage helpers, and POST /api/chat text stream.

  • Produces: useChat(userId), ChatApp({ user, onLogout }), complete desktop/mobile chat UI.

  • Step 1: Write failing streaming hook tests

Use a ReadableStream test response and pin lifecycle behavior:

it("adds the user message and streams assistant chunks", async () => {
  vi.mocked(authFetch).mockResolvedValue(streamResponse(["Привет", ", мир"]));
  const { result } = renderHook(() => useChat("demo-user"));
  await act(() => result.current.sendMessage("Тест"));
  expect(result.current.activeConversation?.messages.map((message) => message.content)).toEqual(["Тест", "Привет, мир"]);
  expect(result.current.activeConversation?.messages.at(-1)?.status).toBe("complete");
});

Also test first-message title truncation, model changes applying to later requests, AbortController.abort() on stop, retry replacing the errored assistant message, 401 calling onUnauthorized, storage persistence, and rejection of empty text.

  • Step 2: Run hook tests and verify failure

Run: npm test -- hooks/use-chat.test.tsx

Expected: FAIL because useChat does not exist.

  • Step 3: Implement the chat hook

Return this stable interface:

type UseChatResult = {
  conversations: Conversation[];
  activeConversation: Conversation | null;
  activeModelId: ModelId;
  isGenerating: boolean;
  selectConversation(id: string): void;
  newConversation(): void;
  deleteConversation(id: string): void;
  setModel(id: ModelId): void;
  sendMessage(content: string): Promise<void>;
  stopGenerating(): void;
  retryLast(): Promise<void>;
};

Use immutable state updates, crypto.randomUUID(), a retained AbortController, streaming response.body.getReader(), TextDecoder, and persistence after each meaningful state transition. Treat 401 as session loss and other failures as retryable assistant errors.

  • Step 4: Write failing component interaction tests

Verify the product renders a new-chat empty state, model selection, Enter send, Shift+Enter newline, disabled empty send, stop button during streaming, copy action, retry action, sidebar conversation selection/deletion, mobile menu toggle, and logout.

await user.type(screen.getByLabelText("Сообщение"), "Составь план{enter}");
expect(sendMessage).toHaveBeenCalledWith("Составь план");
expect(screen.getByRole("button", { name: "Остановить генерацию" })).toBeInTheDocument();
  • Step 5: Implement focused chat components

Use Button, Input, and Textarea shadcn primitives plus Lucide icons. The desktop sidebar is 272px wide; mobile uses an overlay drawer. The header contains a shadcn-style model trigger/menu and mobile menu action. The empty state says Чем я могу помочь? with three realistic prompt suggestions. User messages use subtle right-aligned bubbles; assistant messages use a clean full-width reading column with copy/retry controls. The composer is centered, grows up to 200px, and includes the model disclaimer.

Use real buttons with accessible names, focus-visible styling, aria-live="polite" for incoming text, and aria-busy during generation. Preserve whitespace with CSS instead of an HTML renderer.

  • Step 6: Connect authenticated user and logout

Render <ChatApp user={user} onLogout={logout} /> only in AuthGate's authenticated state. Pass an onUnauthorized callback into useChat so a 401 clears the token and returns to login without rendering stale chat controls.

  • Step 7: Run UI tests and commit

Run: npm test -- hooks/use-chat.test.tsx components/chat/chat-app.test.tsx components/auth/auth-gate.test.tsx

Expected: PASS.

Commit:

git add hooks components/chat components/ui/textarea.tsx components/auth/auth-gate.tsx
git commit -m "feat: build responsive streaming chat"

Task 6: Product Styling, Security Headers, and Final Verification

Files:

  • Modify: app/globals.css
  • Modify: app/layout.tsx
  • Modify: next.config.ts
  • Modify: README.md
  • Create: public/og.png

Interfaces:

  • Consumes: complete auth/chat UI.

  • Produces: finished responsive product, security headers, operational setup instructions, and social preview metadata.

  • Step 1: Add a failing security configuration assertion

Create next-config.test.ts and assert the exported headers include Content-Security-Policy, Referrer-Policy, X-Content-Type-Options, and X-Frame-Options for all routes. The CSP must include default-src 'self', object-src 'none', base-uri 'self', frame-ancestors 'none', and no third-party origins.

  • Step 2: Run the security test and verify failure

Run: npm test -- next-config.test.ts

Expected: FAIL because next.config.ts has no headers.

  • Step 3: Finish the visual system and metadata

Replace starter metadata with Russian product metadata:

export const metadata: Metadata = {
  title: "AI Control Center",
  description: "Корпоративный интерфейс для работы с ИИ-моделями и агентами.",
};

Set <html lang="ru">. In globals.css, define the warm neutral app background, sidebar surface, readable message width, composer shadow, overlay animation, responsive breakpoints, reduced-motion behavior, and full-height body. Keep both light and dark token definitions coherent even though the first screen defaults to light.

  • Step 4: Add static CSP and security headers

Use next.config.ts headers() rather than a nonce proxy so static rendering remains available. Allow only self-hosted scripts, fonts, and network requests; permit Next/React development requirements conditionally ('unsafe-inline' and development-only 'unsafe-eval'). Add object-src 'none', base-uri 'self', form-action 'self', frame-ancestors 'none', X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, and X-Frame-Options: DENY.

  • Step 5: Generate and validate one product social card

Create exactly one landscape public/og.png representing the finished AI Control Center: neutral ChatGPT-like application frame, sidebar, model selector, message composer, Russian title AI Control Center, and no provider logos. Inspect the image once; retry only if text is unusable. Add Open Graph and X metadata only after the asset passes inspection.

  • Step 6: Replace the starter README

Document npm install, .env.local variable names, the test login, AI_GATEWAY_MOCK=true, the exact Gateway request/stream contract, and npm run dev, npm test, npm run lint, npm run build. Explicitly warn that JWT_SECRET, AUTH_PASSWORD, and gateway keys must not be committed.

  • Step 7: Run all automated verification

Run individually and fix only actual failures:

npm test
npm run lint
npm run build

Expected: all commands exit 0 with no failing tests or TypeScript/build errors.

  • Step 8: Run runtime API smoke checks without browser automation

Start npm run dev in a retained session. Send a login request, call /api/auth/me and /api/chat with the returned Bearer token, and confirm the mock chat response streams as plain text. Repeat /api/auth/me without the header and confirm 401. Stop the server after verification. Browser clicking and screenshots are omitted because they were not requested; component tests cover the corresponding interactions.

  • Step 9: Commit the finished product
git add app components hooks lib next.config.ts next-config.test.ts public/og.png README.md
git commit -m "feat: complete corporate AI chat experience"