import React, {
memo,
useCallback,
useEffect,
useReducer,
useRef,
useState
} from "react";
const API_BASE_URL = "http://localhost:4000";
const initialState = {
runId: null,
status: "idle",
output: "",
error: null,
metadata: null
};
function reducer(state, action) {
switch (action.type) {
case "RUN_STARTED": {
return {
runId: action.runId,
status: "streaming",
output: "",
error: null,
metadata: null
};
}
case "METADATA_RECEIVED": {
return {
...state,
metadata: action.metadata
};
}
case "TOKENS_FLUSHED": {
return {
...state,
output: state.output + action.text
};
}
case "RUN_COMPLETED": {
return {
...state,
status: "completed"
};
}
case "RUN_CANCELLED": {
return {
...state,
status: "cancelled"
};
}
case "RUN_ERROR": {
return {
...state,
status: "error",
error: action.error
};
}
case "RESET": {
return initialState;
}
default:
return state;
}
}
export function PromptPlayground() {
const [prompt, setPrompt] = useState("");
const [model, setModel] = useState("placeholder-model");
const [temperature, setTemperature] = useState(0.7);
const [maxTokens, setMaxTokens] = useState(256);
const [state, dispatch] = useReducer(reducer, initialState);
const eventSourceRef = useRef(null);
const tokenBufferRef = useRef("");
const flushTimerRef = useRef(null);
const currentRunIdRef = useRef(null);
const isStreaming = state.status === "streaming";
const flushTokenBuffer = useCallback(() => {
const bufferedText = tokenBufferRef.current;
if (!bufferedText) {
return;
}
tokenBufferRef.current = "";
dispatch({
type: "TOKENS_FLUSHED",
text: bufferedText
});
}, []);
const scheduleTokenFlush = useCallback(() => {
if (flushTimerRef.current) {
return;
}
flushTimerRef.current = window.setTimeout(() => {
flushTimerRef.current = null;
flushTokenBuffer();
}, 50);
}, [flushTokenBuffer]);
const closeEventSource = useCallback(() => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
}, []);
const startSSE = useCallback(
(runId) => {
closeEventSource();
const eventSource = new EventSource(
`${API_BASE_URL}/api/runs/${runId}/events`
);
eventSourceRef.current = eventSource;
eventSource.addEventListener("metadata", (event) => {
const data = JSON.parse(event.data);
dispatch({
type: "METADATA_RECEIVED",
metadata: data
});
});
eventSource.addEventListener("token", (event) => {
const data = JSON.parse(event.data);
tokenBufferRef.current += data.token;
scheduleTokenFlush();
});
eventSource.addEventListener("done", () => {
flushTokenBuffer();
closeEventSource();
dispatch({
type: "RUN_COMPLETED"
});
});
eventSource.addEventListener("cancelled", () => {
flushTokenBuffer();
closeEventSource();
dispatch({
type: "RUN_CANCELLED"
});
});
eventSource.onerror = () => {
flushTokenBuffer();
closeEventSource();
dispatch({
type: "RUN_ERROR",
error: "SSE connection failed"
});
};
},
[closeEventSource, flushTokenBuffer, scheduleTokenFlush]
);
const handleSubmit = useCallback(
async (event) => {
event.preventDefault();
const trimmedPrompt = prompt.trim();
if (!trimmedPrompt || isStreaming) {
return;
}
dispatch({
type: "RESET"
});
try {
const response = await fetch(`${API_BASE_URL}/api/runs`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: trimmedPrompt,
model,
temperature: Number(temperature),
maxTokens: Number(maxTokens)
})
});
if (!response.ok) {
throw new Error("Failed to create prompt run");
}
const data = await response.json();
currentRunIdRef.current = data.runId;
dispatch({
type: "RUN_STARTED",
runId: data.runId
});
startSSE(data.runId);
} catch (error) {
dispatch({
type: "RUN_ERROR",
error: error.message
});
}
},
[prompt, model, temperature, maxTokens, isStreaming, startSSE]
);
const handleCancel = useCallback(async () => {
const runId = currentRunIdRef.current;
if (!runId) {
return;
}
try {
await fetch(`${API_BASE_URL}/api/runs/${runId}/cancel`, {
method: "POST"
});
} finally {
flushTokenBuffer();
closeEventSource();
dispatch({
type: "RUN_CANCELLED"
});
}
}, [closeEventSource, flushTokenBuffer]);
useEffect(() => {
return () => {
closeEventSource();
if (flushTimerRef.current) {
window.clearTimeout(flushTimerRef.current);
}
};
}, [closeEventSource]);
return (
<main className="page">
<section className="playground">
<Header />
<div className="layout">
<PromptPanel
prompt={prompt}
model={model}
temperature={temperature}
maxTokens={maxTokens}
isStreaming={isStreaming}
onPromptChange={setPrompt}
onModelChange={setModel}
onTemperatureChange={setTemperature}
onMaxTokensChange={setMaxTokens}
onSubmit={handleSubmit}
onCancel={handleCancel}
/>
<OutputPanel
status={state.status}
output={state.output}
error={state.error}
metadata={state.metadata}
/>
</div>
</section>
</main>
);
}
const Header = memo(function Header() {
return (
<header className="header">
<div>
<h1>Prompt Playground</h1>
<p>React + Node.js SSE streaming playground</p>
</div>
</header>
);
});
const PromptPanel = memo(function PromptPanel({
prompt,
model,
temperature,
maxTokens,
isStreaming,
onPromptChange,
onModelChange,
onTemperatureChange,
onMaxTokensChange,
onSubmit,
onCancel
}) {
return (
<form className="panel prompt-panel" onSubmit={onSubmit}>
<div className="field">
<label htmlFor="model">Model</label>
<select
id="model"
value={model}
disabled={isStreaming}
onChange={(event) => onModelChange(event.target.value)}
>
<option value="placeholder-model">placeholder-model</option>
<option value="fast-model">fast-model</option>
<option value="reasoning-model">reasoning-model</option>
</select>
</div>
<div className="field">
<label htmlFor="temperature">
Temperature: {Number(temperature).toFixed(1)}
</label>
<input
id="temperature"
type="range"
min="0"
max="2"
step="0.1"
value={temperature}
disabled={isStreaming}
onChange={(event) => onTemperatureChange(event.target.value)}
/>
</div>
<div className="field">
<label htmlFor="maxTokens">Max tokens</label>
<input
id="maxTokens"
type="number"
min="1"
max="4096"
value={maxTokens}
disabled={isStreaming}
onChange={(event) => onMaxTokensChange(event.target.value)}
/>
</div>
<div className="field field-grow">
<label htmlFor="prompt">Prompt</label>
<textarea
id="prompt"
value={prompt}
disabled={isStreaming}
placeholder="Ask the model to explain, generate, summarize, or reason..."
onChange={(event) => onPromptChange(event.target.value)}
/>
</div>
<div className="actions">
<button
type="submit"
disabled={isStreaming || prompt.trim().length === 0}
>
{isStreaming ? "Streaming..." : "Run"}
</button>
<button
type="button"
className="secondary"
disabled={!isStreaming}
onClick={onCancel}
>
Cancel
</button>
</div>
</form>
);
});
const OutputPanel = memo(function OutputPanel({
status,
output,
error,
metadata
}) {
return (
<section className="panel output-panel">
<div className="output-header">
<div>
<h2>Response</h2>
<StatusBadge status={status} />
</div>
{metadata ? (
<div className="metadata">
<span>{metadata.model}</span>
<span>temp {metadata.temperature}</span>
<span>max {metadata.maxTokens}</span>
</div>
) : null}
</div>
{error ? <div className="error">{error}</div> : null}
<pre className="output">
{output || "The streamed response will appear here..."}
</pre>
</section>
);
});
const StatusBadge = memo(function StatusBadge({ status }) {
return <span className={`status status-${status}`}>{status}</span>;
});