Skip to content

Add user interface of the console manager to moderate the language distribution #1

Draft
nahcnuj wants to merge 18 commits intomainfrom
console
Draft

Add user interface of the console manager to moderate the language distribution #1
nahcnuj wants to merge 18 commits intomainfrom
console

Conversation

@nahcnuj
Copy link
Copy Markdown
Owner

@nahcnuj nahcnuj commented Feb 11, 2026

No description provided.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a small JSON API surface (speech/game/meta + Markov distribution) and a new console UI screen that visualizes the Markov chain language distribution as a tree, to support console-based monitoring/management of the streamer.

Changes:

  • Introduce routes/api/* with CORS-enabled endpoints including /api/distribution.
  • Track last spoken text in MakaMujo and expose it via /api/speech.
  • Replace the console’s API tester view with a distribution-tree UI and update console styling/deps.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
routes/api/index.ts Adds CORS-enabled API route map for speech/game/distribution/meta.
routes/api/distribution.ts Implements /api/distribution by parsing talkModel.toJSON().
routes/api/distribution.test.ts Adds unit tests for the distribution handler.
lib/Agent/index.ts Tracks lastSpeech for API exposure.
index.ts Wires new API router into the Bun server.
console/src/index.css New console styling for the distribution tree UI.
console/src/frontend.tsx Ensures global CSS is loaded.
console/src/components/DistributionTree.tsx Implements recursive layout + rendering of distribution tree.
console/src/App.tsx Fetches distribution and renders the tree UI.
console/package.json Bumps React + types versions.
console/bun.lock Lockfile updates for dependency bumps.
.vscode/settings.json Adds a TSX formatter default for VS Code.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +15 to +21
export default function distribution(streamer: { talkModel: { toJSON(): string } }) {
return {
OPTIONS: async () => new Response(null, { status: 204, headers: corsHeaders }),
GET: async (_req?: Request, _server?: any) => {
try {
const json = JSON.parse(streamer.talkModel.toJSON());
return jsonWithCors(json.model ?? {});
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint serializes the entire talk model (toJSON()) and then immediately parses it (JSON.parse) for every request. For large Markov models this is expensive and can become a CPU/memory hotspot. Consider returning the serialized JSON directly (and extracting model on the client), caching the parsed distribution, or adding a talkModel.getDistribution() API to avoid stringify+parse per call.

Suggested change
export default function distribution(streamer: { talkModel: { toJSON(): string } }) {
return {
OPTIONS: async () => new Response(null, { status: 204, headers: corsHeaders }),
GET: async (_req?: Request, _server?: any) => {
try {
const json = JSON.parse(streamer.talkModel.toJSON());
return jsonWithCors(json.model ?? {});
export default function distribution(streamer: { talkModel: { toJSON(): string; getDistribution?: () => unknown } }) {
// Cache the distribution so we don't repeatedly serialize/parse the full model.
let cachedDistribution: unknown | null = null;
return {
OPTIONS: async () => new Response(null, { status: 204, headers: corsHeaders }),
GET: async (_req?: Request, _server?: any) => {
try {
// Prefer a direct distribution API if available.
if (typeof streamer.talkModel.getDistribution === 'function') {
const dist = streamer.talkModel.getDistribution();
return jsonWithCors(dist ?? {});
}
// Fallback: compute and cache the distribution from the serialized model.
if (cachedDistribution === null) {
const json = JSON.parse(streamer.talkModel.toJSON()) as { model?: unknown };
cachedDistribution = json.model ?? {};
}
return jsonWithCors(cachedDistribution);

Copilot uses AI. Check for mistakes.
Comment thread console/src/App.tsx
Comment on lines +12 to +13
const base = process.env.NODE_ENV !== 'production' ? 'http://localhost:8777' : 'http://localhost:7777';
fetch(`${base}/api/distribution`)
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The console fetch URL is hard-coded to http://localhost:... and switches based on process.env.NODE_ENV. In production the console server runs over HTTPS and likely not on the same host/port as the API, so this will fail (mixed-content + wrong host) and may even crash if process is undefined in the browser bundle. Use a configurable public env var (e.g. BUN_PUBLIC_API_BASE) and/or a same-origin relative URL with a reverse proxy.

Suggested change
const base = process.env.NODE_ENV !== 'production' ? 'http://localhost:8777' : 'http://localhost:7777';
fetch(`${base}/api/distribution`)
fetch("/api/distribution")

Copilot uses AI. Check for mistakes.
Comment thread console/src/App.tsx
Comment on lines +1 to +5
import { CandidateList } from "./components/DistributionTree";

type WeightedCandidates = Record<string, number>;
type Distribution = Record<string, WeightedCandidates>;

Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DistributionTree.tsx exports WeightedCandidates/Distribution, but App.tsx redefines the same types locally. This can drift over time and makes refactors harder. Prefer importing the shared types (or moving them to a dedicated types module).

Suggested change
import { CandidateList } from "./components/DistributionTree";
type WeightedCandidates = Record<string, number>;
type Distribution = Record<string, WeightedCandidates>;
import { CandidateList, WeightedCandidates, Distribution } from "./components/DistributionTree";

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +74
export const CandidateList = ({ candidates, dist, depth, layout }: CandidateListProps) => {
const resolvedLayout = layout ?? buildLayout(candidates, dist, depth);
const listStyle: CSSProperties = {
height: resolvedLayout.listHeight,
...(depth === 0 ? { "--weight-unit": `${WEIGHT_UNIT}px`, "--row-gap": `${ROW_GAP}px`, "--min-node-height": `${MIN_NODE_HEIGHT}px` } : {}),
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildLayout(...) is recomputed when layout is not provided (root call) and recursively traverses/sorts candidates. With a large distribution this can be slow on re-renders. Consider memoizing the computed layout (e.g., useMemo keyed by dist/rootCandidates) to keep the UI responsive.

Copilot uses AI. Check for mistakes.
Comment thread console/src/App.tsx
Comment on lines +43 to +47
<section className="tree-panel">
<div className="tree-panel-header">
<div>
<h2>Distribution Tree</h2>
</div>
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR title mentions "moderate" the language distribution, but this UI/API appear read-only (GET distribution and render a tree) with no controls/endpoints to edit weights or persist changes. If moderation is out of scope, consider updating the title/description; otherwise add the missing mutation flow.

Copilot uses AI. Check for mistakes.
Comment thread routes/api/index.ts
Comment on lines +35 to +36
const body = await req.json();
streamer.onAir(body.data);
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POST /api/meta calls await req.json() without error handling. If the client sends invalid JSON (or a non-JSON content type), this handler will throw and the response will likely miss the CORS headers (breaking browser clients). Wrap JSON parsing/validation in try/catch and return a CORS JSON error response on failure.

Suggested change
const body = await req.json();
streamer.onAir(body.data);
let body: any;
try {
body = await req.json();
} catch (_err) {
return jsonWithCors({ error: 'Invalid JSON body' }, 400);
}
if (!body || typeof body !== 'object' || !('data' in body)) {
return jsonWithCors({ error: 'Missing "data" in request body' }, 400);
}
streamer.onAir((body as any).data);

Copilot uses AI. Check for mistakes.
Comment thread routes/api/index.ts
Comment on lines +4 to +8
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corsHeaders (and the jsonWithCors helper right below) are duplicated here and in routes/api/distribution.ts. Consider extracting a shared helper so future header changes don't diverge across endpoints.

Copilot uses AI. Check for mistakes.
Comment thread routes/api/index.ts
Comment on lines +4 to +39
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};

const jsonWithCors = (obj: unknown, status = 200) => new Response(JSON.stringify(obj), {
status,
headers: {
'Content-Type': 'application/json;charset=utf-8',
...corsHeaders,
},
});

export default function api(streamer: MakaMujo) {
return {
'/api/speech': {
GET: async () => jsonWithCors({ speech: streamer.lastSpeech }),
OPTIONS: async () => new Response(null, { status: 204, headers: corsHeaders }),
},

'/api/game': {
GET: async () => jsonWithCors(streamer.playing ?? {}),
OPTIONS: async () => new Response(null, { status: 204, headers: corsHeaders }),
},

'/api/distribution': distribution(streamer),

'/api/meta': {
GET: () => jsonWithCors(streamer.streamState),
POST: async (req: Request) => {
const body = await req.json();
streamer.onAir(body.data);
return jsonWithCors({});
},
OPTIONS: async () => new Response(null, { status: 204, headers: corsHeaders }),
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CORS is configured with Access-Control-Allow-Origin: *. Since this API is intended for a management console, consider restricting allowed origins (and/or requiring auth) to avoid any third-party site reading these endpoints from a victim's browser.

Suggested change
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
const jsonWithCors = (obj: unknown, status = 200) => new Response(JSON.stringify(obj), {
status,
headers: {
'Content-Type': 'application/json;charset=utf-8',
...corsHeaders,
},
});
export default function api(streamer: MakaMujo) {
return {
'/api/speech': {
GET: async () => jsonWithCors({ speech: streamer.lastSpeech }),
OPTIONS: async () => new Response(null, { status: 204, headers: corsHeaders }),
},
'/api/game': {
GET: async () => jsonWithCors(streamer.playing ?? {}),
OPTIONS: async () => new Response(null, { status: 204, headers: corsHeaders }),
},
'/api/distribution': distribution(streamer),
'/api/meta': {
GET: () => jsonWithCors(streamer.streamState),
POST: async (req: Request) => {
const body = await req.json();
streamer.onAir(body.data);
return jsonWithCors({});
},
OPTIONS: async () => new Response(null, { status: 204, headers: corsHeaders }),
// Restrict CORS to a small set of trusted origins (management console frontends).
// Adjust this list to match the actual origins that should be allowed.
const ALLOWED_ORIGINS: readonly string[] = [
"https://your-management-console.example.com",
];
const buildCorsHeaders = (origin: string | null): HeadersInit => {
const headers: Record<string, string> = {
"Access-Control-Allow-Methods": "GET,POST,PUT,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
if (origin && ALLOWED_ORIGINS.includes(origin)) {
headers["Access-Control-Allow-Origin"] = origin;
}
return headers;
};
const jsonWithCors = (obj: unknown, status = 200, req?: Request) =>
new Response(JSON.stringify(obj), {
status,
headers: {
"Content-Type": "application/json;charset=utf-8",
...buildCorsHeaders(req ? req.headers.get("Origin") : null),
},
});
export default function api(streamer: MakaMujo) {
return {
'/api/speech': {
GET: async (req: Request) => jsonWithCors({ speech: streamer.lastSpeech }, 200, req),
OPTIONS: async (req: Request) =>
new Response(null, {
status: 204,
headers: buildCorsHeaders(req.headers.get('Origin')),
}),
},
'/api/game': {
GET: async (req: Request) => jsonWithCors(streamer.playing ?? {}, 200, req),
OPTIONS: async (req: Request) =>
new Response(null, {
status: 204,
headers: buildCorsHeaders(req.headers.get('Origin')),
}),
},
'/api/distribution': distribution(streamer),
'/api/meta': {
GET: (req: Request) => jsonWithCors(streamer.streamState, 200, req),
POST: async (req: Request) => {
const body = await req.json();
streamer.onAir(body.data);
return jsonWithCors({}, 200, req);
},
OPTIONS: async (req: Request) =>
new Response(null, {
status: 204,
headers: buildCorsHeaders(req.headers.get('Origin')),
}),

Copilot uses AI. Check for mistakes.
Comment thread routes/api/index.ts

'/api/meta': {
GET: () => jsonWithCors(streamer.streamState),
POST: async (req: Request) => {
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/meta supports a state-changing POST while CORS is configured with Access-Control-Allow-Origin: *. This enables cross-origin requests to mutate server state. Restrict CORS for this route (or require an auth token / validate Origin) before exposing it beyond localhost.

Suggested change
POST: async (req: Request) => {
POST: async (req: Request) => {
const origin = req.headers.get('origin');
// Only allow state-changing POST requests from localhost origins
if (origin && !/^https?:\/\/localhost(?::\d+)?$/i.test(origin)) {
return new Response(JSON.stringify({ error: 'Forbidden' }), {
status: 403,
headers: {
'Content-Type': 'application/json;charset=utf-8',
...corsHeaders,
},
});
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants