Skip to content

Commit 0d86a96

Browse files
Merge pull request #28 from offendingcommit/feat/dialectic-playground
feat(web): add dialectic reasoning playground
2 parents f52961a + 2340e65 commit 0d86a96

10 files changed

Lines changed: 644 additions & 16 deletions
64.2 KB
Loading
223 KB
Loading
85.4 KB
Loading
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* One-off screenshot script for the dialectic playground.
3+
* Run with: pnpm exec playwright test packages/web/e2e/playground.screenshots.ts
4+
* Outputs are written to docs/screenshots/.
5+
*/
6+
7+
import { mkdirSync } from "node:fs";
8+
import { dirname, resolve } from "node:path";
9+
import { fileURLToPath } from "node:url";
10+
import { test } from "@playwright/test";
11+
12+
const __filename = fileURLToPath(import.meta.url);
13+
const __dirname = dirname(__filename);
14+
const OUT_DIR = resolve(__dirname, "../../../docs/screenshots");
15+
16+
const STORE_KEY = "openconcho:instances";
17+
const STORE_VALUE = JSON.stringify({
18+
instances: [
19+
{
20+
id: "demo-inst",
21+
name: "Demo Honcho",
22+
baseUrl: "http://localhost:8001",
23+
token: "",
24+
},
25+
],
26+
activeId: "demo-inst",
27+
});
28+
29+
const WORKSPACE = "demo-workspace";
30+
const PEER = "alice@example.com";
31+
32+
// Per-level mocked latency (ms) and answer.
33+
const FIXTURES: Record<string, { delayMs: number; content: string }> = {
34+
minimal: {
35+
delayMs: 140,
36+
content:
37+
"Quick gist: Alice prefers async standups, dislikes meetings on Mondays, and tracks priorities in Linear.",
38+
},
39+
low: {
40+
delayMs: 410,
41+
content:
42+
"Alice runs the platform team. She prefers async standups, batches code review in the afternoons, and pushes back on meetings before 10am. Linear is her source of truth for priorities.",
43+
},
44+
medium: {
45+
delayMs: 1180,
46+
content:
47+
"Alice leads the platform team and operates on async-by-default. Three recurring patterns:\n\n• Async over sync — she explicitly skips standups in favor of written status posts on Wednesdays.\n• Deep-work mornings — meetings before 10am are pushed back; she protects 9–11am for coding.\n• Single-source-of-truth in Linear — anything not tracked there is treated as not happening.",
48+
},
49+
high: {
50+
delayMs: 2410,
51+
content:
52+
"Alice's working model has stayed remarkably stable over the last three months. She leads platform, treats async writing as the default communication mode, and resists synchronous coordination unless a decision is actively blocked. Three concrete patterns recur:\n\n1. Async-first standups — Wednesday written status, no daily sync.\n2. Morning deep work — calendar protected 9–11am, meetings pushed past 10.\n3. Linear as system-of-record — verbal commitments she hasn't written into Linear are treated as not real.\n\nShe also pushes back hard on cross-team meetings without a clear decision owner.",
53+
},
54+
max: {
55+
delayMs: 3920,
56+
content:
57+
"Across her recent sessions Alice consistently surfaces three reinforcing patterns and one tension worth flagging.\n\nPatterns:\n1. Async-first communication — explicit preference for written status (Wednesday Linear updates) over standups; she's said \"if it's not in Linear it isn't real\" in three separate threads.\n2. Protected morning deep-work — calendar is blocked 9–11am every weekday; she'll move meetings rather than break the block.\n3. Decision-owner gating — she refuses cross-team meetings without a named decision owner; this has come up six times since March.\n\nTension to flag: Alice's async-default occasionally collides with newer hires who prefer synchronous onboarding. She's aware of this — last month she experimented with a weekly 30-min office hour — but the data is too thin to call it resolved.",
58+
},
59+
};
60+
61+
// Default baseURL comes from playwright.config.ts (localhost:5173); override
62+
// with PLAYWRIGHT_BASE_URL=http://localhost:5184 if regenerating screenshots
63+
// against a worktree dev server on a different port.
64+
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL;
65+
66+
test.use({
67+
viewport: { width: 1600, height: 1000 },
68+
...(BASE_URL ? { baseURL: BASE_URL } : {}),
69+
});
70+
71+
test("playground screenshots", async ({ page }) => {
72+
mkdirSync(OUT_DIR, { recursive: true });
73+
74+
await page.addInitScript(
75+
([key, value]) => {
76+
window.localStorage.setItem(key, value);
77+
},
78+
[STORE_KEY, STORE_VALUE],
79+
);
80+
81+
// Mock the Honcho health probe so the SPA doesn't show a disconnected banner.
82+
await page.route("**/v3/health*", (route) =>
83+
route.fulfill({
84+
status: 200,
85+
contentType: "application/json",
86+
body: JSON.stringify({ status: "ok" }),
87+
}),
88+
);
89+
90+
// Mock the chat POST with per-level fixtures.
91+
await page.route("**/v3/workspaces/*/peers/*/chat", async (route) => {
92+
const body = JSON.parse(route.request().postData() ?? "{}") as {
93+
reasoning_level?: keyof typeof FIXTURES;
94+
};
95+
const level = body.reasoning_level ?? "low";
96+
const fx = FIXTURES[level];
97+
await new Promise((r) => setTimeout(r, fx.delayMs));
98+
await route.fulfill({
99+
status: 200,
100+
contentType: "application/json",
101+
body: JSON.stringify({ content: fx.content }),
102+
});
103+
});
104+
105+
// 1. Idle: empty playground.
106+
await page.goto(`/workspaces/${WORKSPACE}/peers/${encodeURIComponent(PEER)}/playground`);
107+
await page.waitForSelector('[data-testid="column-minimal"]');
108+
await page.screenshot({
109+
path: `${OUT_DIR}/playground-idle.png`,
110+
fullPage: false,
111+
});
112+
113+
// 2. Mid-flight: type a query, fire, capture while columns are still pending.
114+
await page.getByLabel("Query").fill("What patterns does Alice show across her recent sessions?");
115+
await page.getByLabel("Run selected levels").click();
116+
await page.waitForSelector('[data-testid="column-minimal"][data-status="success"]');
117+
// minimal returns at ~140ms; capture now so medium/high/max are still pending.
118+
await page.screenshot({
119+
path: `${OUT_DIR}/playground-running.png`,
120+
fullPage: false,
121+
});
122+
123+
// 3. Settled: wait for max to finish.
124+
await page.waitForSelector('[data-testid="column-max"][data-status="success"]', {
125+
timeout: 10_000,
126+
});
127+
await page.screenshot({
128+
path: `${OUT_DIR}/playground-results.png`,
129+
fullPage: false,
130+
});
131+
});

packages/web/src/api/queries.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,15 +279,29 @@ export function useSearchPeer(workspaceId: string, peerId: string) {
279279
});
280280
}
281281

282-
export function useChat(workspaceId: string, peerId: string) {
282+
export type ReasoningLevel = "minimal" | "low" | "medium" | "high" | "max";
283+
284+
export const REASONING_LEVELS: readonly ReasoningLevel[] = [
285+
"minimal",
286+
"low",
287+
"medium",
288+
"high",
289+
"max",
290+
] as const;
291+
292+
export function useChat(
293+
workspaceId: string,
294+
peerId: string,
295+
reasoningLevel: ReasoningLevel = "low",
296+
) {
283297
const qc = useQueryClient();
284298
return useMutation({
285299
mutationFn: async (message: string) => {
286300
const { data, error } = await client.current.POST(
287301
"/v3/workspaces/{workspace_id}/peers/{peer_id}/chat",
288302
{
289303
params: { path: { workspace_id: workspaceId, peer_id: peerId } },
290-
body: { query: message, stream: false, reasoning_level: "low" },
304+
body: { query: message, stream: false, reasoning_level: reasoningLevel },
291305
},
292306
);
293307
return data ?? err(error);

packages/web/src/components/peers/PeerDetail.tsx

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { useNavigate, useParams } from "@tanstack/react-router";
22
import { AnimatePresence, motion } from "framer-motion";
3-
import { Eye, EyeOff, MessageCircle, Save, Search, User, Users, X } from "lucide-react";
3+
import {
4+
Eye,
5+
EyeOff,
6+
FlaskConical,
7+
MessageCircle,
8+
Save,
9+
Search,
10+
User,
11+
Users,
12+
X,
13+
} from "lucide-react";
414
import { useState } from "react";
515
import {
616
usePeer,
@@ -98,19 +108,35 @@ export function PeerDetail() {
98108
</div>
99109
<Body className="leading-none">Peer identity &amp; memory</Body>
100110
</div>
101-
<Button
102-
variant="primary"
103-
onClick={() =>
104-
navigate({
105-
to: "/workspaces/$workspaceId/peers/$peerId/chat",
106-
params: { workspaceId, peerId } as never,
107-
})
108-
}
109-
className="shrink-0 rounded-xl"
110-
>
111-
<MessageCircle className="w-4 h-4" strokeWidth={1.5} />
112-
Chat
113-
</Button>
111+
<div className="flex items-center gap-2 shrink-0">
112+
<Button
113+
variant="surface"
114+
onClick={() =>
115+
navigate({
116+
to: "/workspaces/$workspaceId/peers/$peerId/playground",
117+
params: { workspaceId, peerId } as never,
118+
})
119+
}
120+
className="rounded-xl"
121+
title="Compare reasoning levels side-by-side"
122+
>
123+
<FlaskConical className="w-4 h-4" strokeWidth={1.5} />
124+
Playground
125+
</Button>
126+
<Button
127+
variant="primary"
128+
onClick={() =>
129+
navigate({
130+
to: "/workspaces/$workspaceId/peers/$peerId/chat",
131+
params: { workspaceId, peerId } as never,
132+
})
133+
}
134+
className="rounded-xl"
135+
>
136+
<MessageCircle className="w-4 h-4" strokeWidth={1.5} />
137+
Chat
138+
</Button>
139+
</div>
114140
</div>
115141
</motion.div>
116142

0 commit comments

Comments
 (0)