import { useQueryClient } from "@tanstack/react-query"; import { Link, useParams } from "@tanstack/react-router"; import { motion } from "framer-motion"; import { FlaskConical, Play } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { client } from "@/api/client"; import { REASONING_LEVELS, type ReasoningLevel } from "@/api/queries"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/input"; import { SectionHeading } from "@/components/ui/typography"; import { useDemo } from "@/hooks/useDemo"; // ─── Types ──────────────────────────────────────────────────────────────────── interface ColumnState { status: "idle" | "pending" | "success" | "error"; content: string | null; error: string | null; startedAt: number | null; endedAt: number | null; } type RunResult = { ok: true; content: string | null } | { ok: false; error: string }; const IDLE: ColumnState = { status: "idle", content: null, error: null, startedAt: null, endedAt: null, }; // ─── Pure helpers (exported for testing) ───────────────────────────────────── export function buildInitialColumns(): Record { return Object.fromEntries(REASONING_LEVELS.map((l) => [l, { ...IDLE }])) as Record< ReasoningLevel, ColumnState >; } export function latencyMs(col: ColumnState): number | null { if (col.startedAt == null || col.endedAt == null) return null; return col.endedAt - col.startedAt; } // ─── Run-fanout (exported for testing) ─────────────────────────────────────── export interface FanoutDeps { now: () => number; runOne: (level: ReasoningLevel, query: string) => Promise; onStart: (level: ReasoningLevel, startedAt: number) => void; onEnd: (level: ReasoningLevel, endedAt: number, result: RunResult) => void; } /** * Fires the same query at every selected level concurrently. Returns when all * settle. Per-column timing is captured via onStart/onEnd so React state updates * stay reflective of the network race, not the await order. */ export async function fanoutQuery( levels: readonly ReasoningLevel[], query: string, deps: FanoutDeps, ): Promise { await Promise.all( levels.map(async (level) => { const startedAt = deps.now(); deps.onStart(level, startedAt); try { const result = await deps.runOne(level, query); deps.onEnd(level, deps.now(), result); } catch (e) { deps.onEnd(level, deps.now(), { ok: false, error: e instanceof Error ? e.message : String(e), }); } }), ); } // ─── Component ─────────────────────────────────────────────────────────────── export function DialecticPlayground() { const { mask } = useDemo(); const { workspaceId, peerId } = useParams({ strict: false }) as { workspaceId: string; peerId: string; }; const qc = useQueryClient(); const [query, setQuery] = useState(""); const [selected, setSelected] = useState>( () => Object.fromEntries(REASONING_LEVELS.map((l) => [l, true])) as Record, ); const [columns, setColumns] = useState>(buildInitialColumns); const anyPending = useMemo( () => REASONING_LEVELS.some((l) => columns[l].status === "pending"), [columns], ); const selectedLevels = useMemo(() => REASONING_LEVELS.filter((l) => selected[l]), [selected]); const runOne = useCallback( async (level: ReasoningLevel, q: string): Promise => { const { data, error } = await client.current.POST( "/v3/workspaces/{workspace_id}/peers/{peer_id}/chat", { params: { path: { workspace_id: workspaceId, peer_id: peerId } }, body: { query: q, stream: false, reasoning_level: level }, }, ); if (error) { return { ok: false, error: typeof error === "object" ? JSON.stringify(error) : String(error), }; } const content = (data as { content?: string | null } | undefined)?.content ?? null; return { ok: true, content }; }, [workspaceId, peerId], ); const handleRun = useCallback(async () => { const trimmed = query.trim(); if (!trimmed || anyPending || selectedLevels.length === 0) return; setColumns((prev) => { const next = { ...prev }; for (const l of selectedLevels) next[l] = { ...IDLE, status: "pending" }; return next; }); await fanoutQuery(selectedLevels, trimmed, { now: () => performance.now(), runOne, onStart: (level, startedAt) => { setColumns((prev) => ({ ...prev, [level]: { ...prev[level], status: "pending", startedAt, endedAt: null }, })); }, onEnd: (level, endedAt, result) => { setColumns((prev) => ({ ...prev, [level]: { ...prev[level], endedAt, status: result.ok ? "success" : "error", content: result.ok ? result.content : null, error: result.ok ? null : result.error, }, })); }, }); qc.invalidateQueries({ queryKey: ["peer-context", workspaceId, peerId] }); }, [anyPending, peerId, qc, query, runOne, selectedLevels, workspaceId]); function toggleLevel(level: ReasoningLevel) { setSelected((prev) => ({ ...prev, [level]: !prev[level] })); } function handleKeyDown(e: React.KeyboardEvent) { if ((e.key === "Enter" && (e.metaKey || e.ctrlKey)) || (e.key === "Enter" && !e.shiftKey)) { e.preventDefault(); handleRun(); } } return (
{/* Header */}
{mask(peerId)} / Playground
Dialectic reasoning playground

Fire the same query at every reasoning level in parallel — compare answers and latency.

{/* Query input */}