@theoremjs/studio 0.3.4-next.403.g5de27f3 → 0.3.4-next.407.gd93ec2c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser-engine.d.ts +1 -1
- package/browser-engine.js +1 -1
- package/browser-transport.d.ts +12 -0
- package/browser-transport.js +26 -7
- package/browser.d.ts +1 -1
- package/browser.js +1 -1
- package/mod.d.ts +2 -1
- package/mod.js +2 -1
- package/package.json +4 -4
- package/page/assets/{Banner-CLcFr8r9.js → Banner-B1Vat7i_.js} +2 -2
- package/page/assets/{ContextMenu-CArbXxnu.js → ContextMenu-BreVLY9J.js} +2 -2
- package/page/assets/{ToolResultCharts-DG0GfEe-.js → ToolResultCharts-BV2tbzow.js} +1 -1
- package/page/assets/{TraceInspector-DlcrrHNa.js → TraceInspector-B8NufbVX.js} +1 -1
- package/page/assets/{TraceOverview-BRgK49q4.js → TraceOverview-DoQOfiAe.js} +2 -2
- package/page/assets/{browser-engine-yDBh7tWN.js → browser-engine-BvsLwtiv.js} +6 -6
- package/page/assets/browser-transport-DxpDmXGb.js +47 -0
- package/page/assets/{export-agent-DuPZHGrj.js → export-agent-D2Vo-OSL.js} +3 -3
- package/page/assets/{guardrail-tester-xtRSrTXh.js → guardrail-tester-CuqamClw.js} +2 -2
- package/page/assets/{index-BVTCzZhh.js → index-C3mDKoGG.js} +69 -69
- package/page/assets/{live-messages-CaI0Oqem.js → live-messages-BZGmMo_v.js} +1 -1
- package/page/assets/{reader-CaGeZoFz.js → reader-paEmSrWc.js} +4 -4
- package/page/assets/{studio-code-pane-_XzNjN6P.js → studio-code-pane-B9bG0xzf.js} +1 -1
- package/page/assets/{studio-console-Da8ZVuKx.js → studio-console-1qIWsDtV.js} +3 -3
- package/page/assets/studio-decision-BdJR3HSN.js +1 -0
- package/page/assets/{studio-embedding-BaWl7liT.js → studio-embedding-CFzitRLj.js} +1 -1
- package/page/assets/studio-host-console-BzpDE2TK.js +2 -0
- package/page/assets/{studio-live-ON74y8sM.js → studio-live-BucphGSr.js} +2 -2
- package/page/assets/{studio-map-W8XbAypV.js → studio-map-BfUMbnZK.js} +1 -1
- package/page/assets/studio-trace-replay-C5cBxYG_.js +2 -0
- package/page/assets/turn-client-BZUECwPn.js +2 -0
- package/page/assets/{use-one-call-BD3tlw64.js → use-one-call-BVmuHLSm.js} +1 -1
- package/page/index.html +6 -6
- package/replay.d.ts +97 -0
- package/replay.js +387 -0
- package/server/draft-handler.js +2 -1
- package/transport.d.ts +5 -1
- package/transport.js +7 -2
- package/ui/inspector-context.d.ts +1 -1
- package/ui/lib/send-when-ready.d.ts +7 -0
- package/ui/lib/send-when-ready.js +14 -0
- package/ui/studio-connection.d.ts +1 -1
- package/ui/studio-runner.d.ts +4 -1
- package/ui/studio-runner.js +3 -2
- package/ui/studio-screen.js +111 -11
- package/ui/studio-trace-replay.d.ts +23 -0
- package/ui/studio-trace-replay.js +216 -0
- package/page/assets/browser-transport-B0GPqGzb.js +0 -47
- package/page/assets/studio-decision-CHaFsz_1.js +0 -1
- package/page/assets/studio-host-console-BuPmNkLs.js +0 -2
- package/page/assets/turn-client-BBi2anJM.js +0 -2
package/ui/studio-screen.js
CHANGED
|
@@ -21,13 +21,14 @@ import { Token } from '@astryxdesign/core/Token';
|
|
|
21
21
|
import { Tooltip } from '@astryxdesign/core/Tooltip';
|
|
22
22
|
import { TreeList } from '@astryxdesign/core/TreeList';
|
|
23
23
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
24
|
-
import { IconActivity, IconAdjustmentsHorizontal, IconArrowBarToDown, IconArrowLeft, IconBrowserShare, IconCheck, IconCloud, IconCode, IconCopy, IconCopyPlus, IconFile, IconFileSmile, IconFileZip, IconKey, IconMenu2, IconPlayerPlay, IconPlaylistX, IconPlus, IconRestore, IconRotateClockwise, IconSearch, IconShieldSearch, IconSitemap, IconSparkles, IconTool, IconX, } from '@tabler/icons-react';
|
|
24
|
+
import { IconActivity, IconAdjustmentsHorizontal, IconArrowBarToDown, IconArrowLeft, IconBrowserShare, IconCheck, IconCloud, IconCode, IconCopy, IconCopyPlus, IconDownload, IconFile, IconFileImport, IconFileSmile, IconFileZip, IconKey, IconMenu2, IconPlayerPlay, IconPlaylistX, IconPlus, IconRestore, IconRotateClockwise, IconSearch, IconShieldSearch, IconSitemap, IconSparkles, IconTool, IconX, } from '@tabler/icons-react';
|
|
25
25
|
import { profileGraphFacet } from '@theoremjs/agents';
|
|
26
26
|
import { InPlace, PaneFailure, RaisedPane, useDisclosureMotion, } from '@theoremjs/react/ui';
|
|
27
27
|
import { canonical } from "../canonical.js";
|
|
28
28
|
import { addAgent, addArchitectExample, addProvider, agentNodeId, compileWorkspace, createBlankDraft, createConsoleExampleDraft, createDecisionExampleDraft, createExampleDraft, createGeminiEmbeddingExampleDraft, createLiveExampleDraft, createNarratorExampleDraft, createStudioRunId, draftFacets, duplicateAgent, editedSince, excludeFacet, includableFacets, includeFacet, libraryDraft, localProviderAddress, mergedWithFiles, modelBindingNodeId, modelRoute, modelSharers, modelSlots, nodeOrigins, onStudioLocal, openOutcome, originLabel, originPlace, originWay, providerIdOf, providerNodeId, providerUse, rebased, removeAgent, removeLibraryTool, removeProvider, renameProvider, reopened, resetAgent, resetAll, resetLibraryTool, resetProvider, sampleToolInput, saveStudioRunPayload, scopedNodeId, setToolAllowed, sharedAsk, sharedAt, sharedEntries, studioNodeRef, studioSource, studioTree, toolSpecKeyOf, withAgentDraft, withFilesChosen, withProvider, workspaceNodeRef, workspaceRunAgent, workspaceTree, writeAsk, } from "../mod.js";
|
|
29
29
|
import { studioSurface } from "../surface.js";
|
|
30
30
|
import './studio.css';
|
|
31
|
+
import { createTraceFeed } from '@theoremjs/web/client';
|
|
31
32
|
import { lazy, memo, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
|
|
32
33
|
import { flushSync } from 'react-dom';
|
|
33
34
|
import { ActingAsButton } from "./acting-as-button.js";
|
|
@@ -40,6 +41,7 @@ import { FACET_ICON } from "./lib/facet-icons.js";
|
|
|
40
41
|
import { movePanel } from "./lib/panel-motion.js";
|
|
41
42
|
import { screenEndpoint } from "./lib/project-address.js";
|
|
42
43
|
import { recordsTraces } from "./lib/project-traces.js";
|
|
44
|
+
import { sendWhenReady } from "./lib/send-when-ready.js";
|
|
43
45
|
import { clearConversation, restoreConversation, saveConversation, } from "./lib/studio-conversation.js";
|
|
44
46
|
import { STUDIO_EXAMPLES } from "./lib/studio-examples.js";
|
|
45
47
|
import { pageInputsOf, sentPageValues, usePageValues } from "./lib/studio-page.js";
|
|
@@ -72,6 +74,8 @@ const StudioMap = lazy(() => mapPanel().then((map) => ({ default: map.StudioMap
|
|
|
72
74
|
const CodeBody = lazy(() => codePane().then((code) => ({ default: code.CodeBody })));
|
|
73
75
|
const ProviderCode = lazy(() => codePane().then((code) => ({ default: code.ProviderCode })));
|
|
74
76
|
const GuardrailTester = lazy(() => testerPanel().then((tester) => ({ default: tester.GuardrailTester })));
|
|
77
|
+
const traceReplay = () => import("./studio-trace-replay.js");
|
|
78
|
+
const TraceReplay = lazy(() => traceReplay().then((pane) => ({ default: pane.TraceReplay })));
|
|
75
79
|
const StudioSettings = lazy(() => settingsPanel().then((settings) => ({ default: settings.StudioSettings })));
|
|
76
80
|
/** A node's icon; Identity shows the profile type's once one is picked. */
|
|
77
81
|
function nodeIcon(draft, ref) {
|
|
@@ -1240,7 +1244,11 @@ function useConversationRun(mode, sealer, chatWith, actingAs, question) {
|
|
|
1240
1244
|
firstRuns.set(chatKey, runKey);
|
|
1241
1245
|
const isFirstRun = firstRuns.get(chatKey) === runKey;
|
|
1242
1246
|
const [seeded] = useState(chatWith);
|
|
1243
|
-
|
|
1247
|
+
/** A conversation started for a chat's next runner: what it resumes and draws at once. */
|
|
1248
|
+
const [resumed, setResumed] = useState();
|
|
1249
|
+
const resumes = resumed?.runKey === runKey ? resumed : undefined;
|
|
1250
|
+
const conversation = useMemo(() => (resumes ? resumes.ref : isFirstRun ? restoreConversation(chatKey, sealer) : undefined), [resumes, isFirstRun, chatKey, sealer]);
|
|
1251
|
+
const conversationView = resumes?.view;
|
|
1244
1252
|
/** A docs seed's question waits in its agent's first composer, when that resumes nothing. */
|
|
1245
1253
|
const initialText = isFirstRun && chatWith === seeded && !conversation ? question : undefined;
|
|
1246
1254
|
/**
|
|
@@ -1255,6 +1263,7 @@ function useConversationRun(mode, sealer, chatWith, actingAs, question) {
|
|
|
1255
1263
|
generation,
|
|
1256
1264
|
sealer,
|
|
1257
1265
|
conversation,
|
|
1266
|
+
conversationView,
|
|
1258
1267
|
initialText,
|
|
1259
1268
|
isUsed,
|
|
1260
1269
|
markUsed: () => {
|
|
@@ -1265,8 +1274,25 @@ function useConversationRun(mode, sealer, chatWith, actingAs, question) {
|
|
|
1265
1274
|
clearConversation(chatKey);
|
|
1266
1275
|
setCleared((counts) => ({ ...counts, [chatKey]: (counts[chatKey] ?? 0) + 1 }));
|
|
1267
1276
|
},
|
|
1277
|
+
/** Goes on in a new runner with the conversation under `ref`, drawn as `view` at once. */
|
|
1278
|
+
resume: (ref, view) => {
|
|
1279
|
+
saveConversation(chatKey, sealer, ref);
|
|
1280
|
+
const next = (cleared[chatKey] ?? 0) + 1;
|
|
1281
|
+
setResumed({ runKey: `${generation}:${chatKey}:${String(next)}`, ref, view });
|
|
1282
|
+
setCleared((counts) => ({ ...counts, [chatKey]: next }));
|
|
1283
|
+
},
|
|
1268
1284
|
setConversation,
|
|
1269
|
-
}), [
|
|
1285
|
+
}), [
|
|
1286
|
+
runKey,
|
|
1287
|
+
chatKey,
|
|
1288
|
+
generation,
|
|
1289
|
+
sealer,
|
|
1290
|
+
conversation,
|
|
1291
|
+
conversationView,
|
|
1292
|
+
initialText,
|
|
1293
|
+
isUsed,
|
|
1294
|
+
cleared,
|
|
1295
|
+
]);
|
|
1270
1296
|
}
|
|
1271
1297
|
/**
|
|
1272
1298
|
* Once, on arrival: say when a kept draft was set aside for a docs seed, when a project's files
|
|
@@ -1732,15 +1758,32 @@ function EditorBody({ state, connection, selected, editing, issues, listBadges,
|
|
|
1732
1758
|
function ClearHistoryButton({ run }) {
|
|
1733
1759
|
return (_jsx(IconButton, { label: "Clear history", variant: "ghost", icon: _jsx(Icon, { icon: IconPlaylistX, size: "sm" }), tooltip: "Clear the conversation and its traces. Your profile stays.", onClick: run.clear }));
|
|
1734
1760
|
}
|
|
1761
|
+
/** The types whose conversation a trace can hold, and so open and replay. */
|
|
1762
|
+
const NO_CONVERSATION = new Set(['decision', 'host', 'embedding', 'live']);
|
|
1763
|
+
/** Downloads the records of the conversation on screen, as a trace file the studio opens. */
|
|
1764
|
+
function DownloadTraceButton({ feed, name }) {
|
|
1765
|
+
const records = useSyncExternalStore(feed.subscribe, feed.records, feed.records);
|
|
1766
|
+
return (_jsx(IconButton, { label: "Download trace", variant: "ghost", tooltip: records.length ? 'Download trace' : 'Nothing recorded yet', icon: _jsx(Icon, { icon: IconDownload, size: "sm" }), isDisabled: records.length === 0, onClick: () => {
|
|
1767
|
+
const stamp = new Date().toISOString().replaceAll(':', '-').slice(0, 19);
|
|
1768
|
+
download(`${name}-${stamp}.trace.json`, new Blob([JSON.stringify(records, null, 2)], { type: 'application/json' }));
|
|
1769
|
+
} }));
|
|
1770
|
+
}
|
|
1771
|
+
/** Open trace, for a type whose conversation a trace holds, and Download trace, outside an opened one. */
|
|
1772
|
+
function TraceFileButtons({ payload, tracing, feed, profileId, }) {
|
|
1773
|
+
if (payload == null)
|
|
1774
|
+
return null;
|
|
1775
|
+
const openLabel = tracing.open ? 'Close trace' : 'Open trace';
|
|
1776
|
+
return (_jsxs(_Fragment, { children: [!NO_CONVERSATION.has(payload.profile.type) && (_jsx(IconButton, { label: openLabel, variant: tracing.open ? 'secondary' : 'ghost', tooltip: openLabel, icon: _jsx(Icon, { icon: IconFileImport, size: "sm" }), "aria-pressed": tracing.open, onClick: tracing.toggle })), !tracing.open && _jsx(DownloadTraceButton, { feed: feed, name: profileId ?? 'agent' })] }));
|
|
1777
|
+
}
|
|
1735
1778
|
/** The preview's header: back to the editor, the agent to chat with, history, trace, and Export. */
|
|
1736
|
-
function PreviewHeader({ store, chatAgents, chatWith, compile, run, connection, trace, testing, copy, setSheet, }) {
|
|
1779
|
+
function PreviewHeader({ store, chatAgents, chatWith, compile, run, connection, trace, testing, tracing, feed, profileId, copy, setSheet, }) {
|
|
1737
1780
|
const testLabel = testing.open ? 'Close guardrail test' : 'Test guardrails';
|
|
1738
1781
|
const traceLabel = trace.open ? 'Hide trace' : 'View trace';
|
|
1739
1782
|
const { compiled } = compile;
|
|
1740
|
-
return (_jsx(Section, { variant: "transparent", paddingInline: 3, paddingBlock: 0, children: _jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(SheetButton, { label: "Back to the editor", icon: IconArrowLeft, sheet: null, setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: _jsx(ChatPicker, { agents: chatAgents, chatWith: chatWith, onChange: store.chatWith }) }), _jsx(StackItem, { size: "static", children: _jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(ActingAsButton, {}), compile.payload && run.isUsed && !testing.open && _jsx(ClearHistoryButton, { run: run }), compile.payload && (_jsx(IconButton, { label: testLabel, variant: "ghost", tooltip: testLabel, icon: _jsx(Icon, { icon: IconShieldSearch, size: "sm" }), "aria-pressed": testing.open, onClick: testing.toggle })), _jsx(ExportActions, { store: store, compiled: compiled.ok ? compiled : undefined, chatted: compile.chatted, chattedId: compile.chattedId, blocked: compile.blocked, copy: copy, connection: {
|
|
1783
|
+
return (_jsx(Section, { variant: "transparent", paddingInline: 3, paddingBlock: 0, children: _jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(SheetButton, { label: "Back to the editor", icon: IconArrowLeft, sheet: null, setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: _jsx(ChatPicker, { agents: chatAgents, chatWith: chatWith, onChange: store.chatWith }) }), _jsx(StackItem, { size: "static", children: _jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(ActingAsButton, {}), compile.payload && run.isUsed && !testing.open && !tracing.open && (_jsx(ClearHistoryButton, { run: run })), !testing.open && (_jsx(TraceFileButtons, { payload: compile.payload, tracing: tracing, feed: feed, profileId: profileId })), compile.payload && (_jsx(IconButton, { label: testLabel, variant: "ghost", tooltip: testLabel, icon: _jsx(Icon, { icon: IconShieldSearch, size: "sm" }), "aria-pressed": testing.open, onClick: testing.toggle })), _jsx(ExportActions, { store: store, compiled: compiled.ok ? compiled : undefined, chatted: compile.chatted, chattedId: compile.chattedId, blocked: compile.blocked, copy: copy, connection: {
|
|
1741
1784
|
connectionMode: connection.mode,
|
|
1742
1785
|
localBaseUrl: connection.local.baseUrl,
|
|
1743
|
-
} }), compile.traced && !testing.open ? (_jsx(IconButton, { label: traceLabel, variant: trace.open ? 'secondary' : 'ghost', tooltip: traceLabel, icon: _jsx(Icon, { icon: IconActivity, size: "sm" }), "aria-pressed": trace.open, onClick: trace.toggle })) : null] }) })] }) }));
|
|
1786
|
+
} }), (compile.traced || tracing.open) && !testing.open ? (_jsx(IconButton, { label: traceLabel, variant: trace.open ? 'secondary' : 'ghost', tooltip: traceLabel, icon: _jsx(Icon, { icon: IconActivity, size: "sm" }), "aria-pressed": trace.open, onClick: trace.toggle })) : null] }) })] }) }));
|
|
1744
1787
|
}
|
|
1745
1788
|
/**
|
|
1746
1789
|
* Why the agent to chat with does not run: the errors in its config, or the workspace's when its
|
|
@@ -1762,7 +1805,7 @@ function ConfigIssues({ compile, chatWith, onIssue, }) {
|
|
|
1762
1805
|
* The runner for the compiled agent, its conversation saved as it goes, or the guardrail tester
|
|
1763
1806
|
* over it; while none compiles, why.
|
|
1764
1807
|
*/
|
|
1765
|
-
function PreviewBody({ compile, run, connection, chatRef, chatWith, profileId, traceOpen, testing, onIssue, }) {
|
|
1808
|
+
function PreviewBody({ compile, run, connection, chatRef, chatWith, profileId, traceOpen, testing, tracing, feedOf, onIssue, }) {
|
|
1766
1809
|
const { payload, traced, tested } = compile;
|
|
1767
1810
|
/** Why the project's server does not run this profile: its files say, not the editor. */
|
|
1768
1811
|
const unserved = useProject()?.problems.find((problem) => problem.profile === profileId);
|
|
@@ -1777,6 +1820,7 @@ function PreviewBody({ compile, run, connection, chatRef, chatWith, profileId, t
|
|
|
1777
1820
|
payload,
|
|
1778
1821
|
onActivity: run.markUsed,
|
|
1779
1822
|
conversation: run.conversation,
|
|
1823
|
+
conversationView: run.conversationView,
|
|
1780
1824
|
initialText: run.initialText,
|
|
1781
1825
|
onConversationChange: (ref) => {
|
|
1782
1826
|
if (ref)
|
|
@@ -1792,9 +1836,9 @@ function PreviewBody({ compile, run, connection, chatRef, chatWith, profileId, t
|
|
|
1792
1836
|
: null;
|
|
1793
1837
|
const kept = useKeptRuns(run, shown);
|
|
1794
1838
|
const blocked = unserved ? (_jsx(PaneFailure, { title: `${unserved.profile} does not run here yet`, description: unserved.message })) : payload ? null : (_jsx(ConfigIssues, { compile: compile, chatWith: chatWith, onIssue: onIssue }));
|
|
1795
|
-
return (_jsx(InPlace, { view: testing && payload ? (_jsx(RaisedPane, { children: (isWide) => (_jsx(Suspense, { fallback: null, children: _jsx(GuardrailTester, { payload: payload, isWide: isWide }) })) })) :
|
|
1839
|
+
return (_jsx(InPlace, { view: testing && payload ? (_jsx(RaisedPane, { children: (isWide) => (_jsx(Suspense, { fallback: null, children: _jsx(GuardrailTester, { payload: payload, isWide: isWide }) })) })) : (tracing), children: _jsx(VStack, { height: "100%", children: _jsxs(StackItem, { size: "fill", children: [blocked, kept.map(([key, { props }]) => {
|
|
1796
1840
|
const isShown = shown !== null && key === run.runKey;
|
|
1797
|
-
return (_jsx("div", { style: { display: isShown ? 'contents' : 'none' }, children: _jsx(StudioRunner, { ...props, mode: connection.mode, runtime: connection.runtime, trace: traced ? traceOpen : undefined, chatRef: isShown ? chatRef : undefined }) }, key));
|
|
1841
|
+
return (_jsx("div", { style: { display: isShown ? 'contents' : 'none' }, children: _jsx(StudioRunner, { ...props, mode: connection.mode, runtime: connection.runtime, trace: traced ? traceOpen : undefined, chatRef: isShown ? chatRef : undefined, traces: feedOf(key) }) }, key));
|
|
1798
1842
|
})] }) }) }));
|
|
1799
1843
|
}
|
|
1800
1844
|
/**
|
|
@@ -1813,6 +1857,44 @@ function useKeptRuns(run, shown) {
|
|
|
1813
1857
|
kept.set(run.runKey, shown);
|
|
1814
1858
|
return [...kept];
|
|
1815
1859
|
}
|
|
1860
|
+
/**
|
|
1861
|
+
* The trace feed of each runner, by its run key: the header downloads the one on screen. Another
|
|
1862
|
+
* mode or a restart drops them all, as it drops the runners.
|
|
1863
|
+
*/
|
|
1864
|
+
function useRunFeeds(generation) {
|
|
1865
|
+
const [feeds] = useState(() => new Map());
|
|
1866
|
+
return useCallback((runKey) => {
|
|
1867
|
+
for (const key of feeds.keys())
|
|
1868
|
+
if (!key.startsWith(`${generation}:`))
|
|
1869
|
+
feeds.delete(key);
|
|
1870
|
+
let feed = feeds.get(runKey);
|
|
1871
|
+
if (!feed) {
|
|
1872
|
+
feed = createTraceFeed();
|
|
1873
|
+
feeds.set(runKey, feed);
|
|
1874
|
+
}
|
|
1875
|
+
return feed;
|
|
1876
|
+
}, [feeds, generation]);
|
|
1877
|
+
}
|
|
1878
|
+
/** Sends a message once the runner for the next run key is ready: a conversation goes on live. */
|
|
1879
|
+
function useSendAfterResume(chatRef, runKey) {
|
|
1880
|
+
const [next, setNext] = useState();
|
|
1881
|
+
useEffect(() => {
|
|
1882
|
+
if (!next?.text || next.after === runKey)
|
|
1883
|
+
return;
|
|
1884
|
+
const controller = new AbortController();
|
|
1885
|
+
// why: cleared once sent, not before: clearing re-renders, and the cleanup would abort the send.
|
|
1886
|
+
void sendWhenReady(chatRef, next.text, controller.signal).then(() => {
|
|
1887
|
+
if (!controller.signal.aborted)
|
|
1888
|
+
setNext(undefined);
|
|
1889
|
+
});
|
|
1890
|
+
return () => {
|
|
1891
|
+
controller.abort();
|
|
1892
|
+
};
|
|
1893
|
+
}, [next, runKey, chatRef]);
|
|
1894
|
+
return useCallback((text) => {
|
|
1895
|
+
setNext({ text, after: runKey });
|
|
1896
|
+
}, [runKey]);
|
|
1897
|
+
}
|
|
1816
1898
|
/**
|
|
1817
1899
|
* The compiled agent to chat with, under its header; while none compiles, why. It sits out renders
|
|
1818
1900
|
* that change nothing it shows: typing in the editor, opening a node, Keys.
|
|
@@ -1820,6 +1902,18 @@ function useKeptRuns(run, shown) {
|
|
|
1820
1902
|
const PreviewPane = memo(function PreviewPane({ store, chatAgents, chatWith, compile, run, connection, chatRef, copy, setSheet, onIssue, }) {
|
|
1821
1903
|
const [traceOpen, setTraceOpen] = useState(false);
|
|
1822
1904
|
const [testing, setTesting] = useState(false);
|
|
1905
|
+
const [tracing, setTracing] = useState(false);
|
|
1906
|
+
const feedOf = useRunFeeds(run.generation);
|
|
1907
|
+
const profileId = chatAgents.find((agent) => agent.value === chatWith)?.label;
|
|
1908
|
+
const sendNext = useSendAfterResume(chatRef, run.runKey);
|
|
1909
|
+
const { payload } = compile;
|
|
1910
|
+
const traceView = tracing && payload ? (_jsx(Suspense, { fallback: null, children: _jsx(TraceReplay, { payload: payload, runtime: connection.runtime, trace: traceOpen, onClose: () => {
|
|
1911
|
+
setTracing(false);
|
|
1912
|
+
}, onLive: ({ conversation, view, text }) => {
|
|
1913
|
+
run.resume(conversation, view);
|
|
1914
|
+
setTracing(false);
|
|
1915
|
+
sendNext(text);
|
|
1916
|
+
} }) })) : null;
|
|
1823
1917
|
return (_jsx(LayoutContent, { className: "studio-preview shell-fade", isScrollable: false, padding: 0, children: _jsxs(VStack, { height: "100%", children: [_jsx(PreviewHeader, { store: store, chatAgents: chatAgents, chatWith: chatWith, compile: compile, run: run, connection: connection, trace: {
|
|
1824
1918
|
open: traceOpen,
|
|
1825
1919
|
toggle: () => {
|
|
@@ -1829,8 +1923,14 @@ const PreviewPane = memo(function PreviewPane({ store, chatAgents, chatWith, com
|
|
|
1829
1923
|
open: testing,
|
|
1830
1924
|
toggle: () => {
|
|
1831
1925
|
setTesting((open) => !open);
|
|
1926
|
+
setTracing(false);
|
|
1927
|
+
},
|
|
1928
|
+
}, tracing: {
|
|
1929
|
+
open: tracing,
|
|
1930
|
+
toggle: () => {
|
|
1931
|
+
setTracing((open) => !open);
|
|
1832
1932
|
},
|
|
1833
|
-
}, copy: copy, setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: _jsx(PreviewBody, { compile: compile, run: run, connection: connection, chatRef: chatRef, chatWith: chatWith, profileId:
|
|
1933
|
+
}, feed: feedOf(run.runKey), profileId: profileId, copy: copy, setSheet: setSheet }), _jsx(StackItem, { size: "fill", children: _jsx(PreviewBody, { compile: compile, run: run, connection: connection, chatRef: chatRef, chatWith: chatWith, profileId: profileId, traceOpen: traceOpen, testing: testing, tracing: traceView, feedOf: feedOf, onIssue: onIssue }) })] }) }));
|
|
1834
1934
|
});
|
|
1835
1935
|
/** Tells the host what is on screen as it changes, and that it closed. */
|
|
1836
1936
|
function useScreenReport(onReport, { agent, type, issues, section }) {
|
|
@@ -2074,7 +2174,7 @@ function EditorColumnBody({ state, connection, view, compile, rowIssues, selecte
|
|
|
2074
2174
|
/** The studio, opened on `opened`, with what the page showing it provides. */
|
|
2075
2175
|
export function StudioScreen({ opened, host }) {
|
|
2076
2176
|
useEffect(() => {
|
|
2077
|
-
warmWhenIdle(testerPanel, mapPanel, codePane, exportKit);
|
|
2177
|
+
warmWhenIdle(testerPanel, traceReplay, mapPanel, codePane, exportKit);
|
|
2078
2178
|
}, []);
|
|
2079
2179
|
return (_jsx(StudioHostContext.Provider, { value: host, children: _jsx(Studio, { opened: opened }) }));
|
|
2080
2180
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TraceRecord } from '@theoremjs/agents/observability';
|
|
2
|
+
import { type ConversationView } from '@theoremjs/web/client';
|
|
3
|
+
import { type StudioBrowserRuntime } from '../browser.ts';
|
|
4
|
+
import { type StudioRunPayload } from '../mod.ts';
|
|
5
|
+
/** A trace file the builder opened. */
|
|
6
|
+
export interface OpenedTrace {
|
|
7
|
+
name: string;
|
|
8
|
+
records: TraceRecord[];
|
|
9
|
+
}
|
|
10
|
+
/** A live conversation started where a replay stopped: its ref, its view, and the message to send next. */
|
|
11
|
+
export interface LiveStart {
|
|
12
|
+
conversation: string;
|
|
13
|
+
view: ConversationView;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
/** The trace pane: a place to open one, then what it recorded and its replay. */
|
|
17
|
+
export declare function TraceReplay({ payload, trace, runtime, onClose, onLive, }: {
|
|
18
|
+
payload: StudioRunPayload;
|
|
19
|
+
trace: boolean | undefined;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
runtime: StudioBrowserRuntime | null;
|
|
22
|
+
onLive: (start: LiveStart) => void;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* An opened trace: the conversation it recorded, as the person saw it, and a replay of it through
|
|
4
|
+
* the profile as it is now. A replay reaches no model and, unless Live tools is on, no tool: the
|
|
5
|
+
* recording answers both. Where the profile asks for something the recording does not hold, the
|
|
6
|
+
* replay stops and says why; Continue live goes on from there with the real model.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { Banner } from '@astryxdesign/core/Banner';
|
|
11
|
+
import { Button } from '@astryxdesign/core/Button';
|
|
12
|
+
import { FileInput } from '@astryxdesign/core/FileInput';
|
|
13
|
+
import { HStack } from '@astryxdesign/core/HStack';
|
|
14
|
+
import { Icon } from '@astryxdesign/core/Icon';
|
|
15
|
+
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
16
|
+
import { Section } from '@astryxdesign/core/Section';
|
|
17
|
+
import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
|
|
18
|
+
import { StackItem } from '@astryxdesign/core/Stack';
|
|
19
|
+
import { Switch } from '@astryxdesign/core/Switch';
|
|
20
|
+
import { Text } from '@astryxdesign/core/Text';
|
|
21
|
+
import { VStack } from '@astryxdesign/core/VStack';
|
|
22
|
+
import { IconFileImport, IconPlayerPlay, IconX } from '@tabler/icons-react';
|
|
23
|
+
import { PaneState, TheoremChat } from '@theoremjs/react/ui';
|
|
24
|
+
import { createTraceFeed, } from '@theoremjs/web/client';
|
|
25
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
26
|
+
import { createBrowserStudioTransport, startBrowserStudioConversation, } from "../browser.js";
|
|
27
|
+
import { createReplay, parseTraceFile, recordedPayload, recordedTranscript, replayScript, startStudioConversation, studioInterface, studioPageTools, } from "../mod.js";
|
|
28
|
+
import { sendWhenReady } from "./lib/send-when-ready.js";
|
|
29
|
+
import { pageConversationKey } from "./lib/studio-conversation.js";
|
|
30
|
+
import { STUDIO_LABELS } from "./lib/studio-labels.js";
|
|
31
|
+
/**
|
|
32
|
+
* A live conversation on `blocks`, kept where the live chat keeps its own: sealed in the page
|
|
33
|
+
* when it runs there, else in this visitor's session on the server.
|
|
34
|
+
*/
|
|
35
|
+
function startLive(payload, runtime, blocks) {
|
|
36
|
+
return runtime
|
|
37
|
+
? startBrowserStudioConversation(payload, runtime, {
|
|
38
|
+
conversationKey: pageConversationKey(),
|
|
39
|
+
blocks,
|
|
40
|
+
})
|
|
41
|
+
: startStudioConversation(payload, blocks);
|
|
42
|
+
}
|
|
43
|
+
const ordinal = (turn) => `turn ${String(turn + 1)}`;
|
|
44
|
+
/** A chat that only shows: the conversation a trace recorded, and its records beside it. */
|
|
45
|
+
function recordedTransport(payload, transcript, traces) {
|
|
46
|
+
const refused = () => Promise.reject(new Error('A recorded conversation cannot change. Replay it, then continue live.'));
|
|
47
|
+
return {
|
|
48
|
+
describe: () => Promise.resolve(studioInterface(payload)),
|
|
49
|
+
turn: refused,
|
|
50
|
+
invoke: refused,
|
|
51
|
+
steer: refused,
|
|
52
|
+
rewind: refused,
|
|
53
|
+
conversation: () => Promise.resolve({ transcript }),
|
|
54
|
+
traces,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The pane to open a trace in: a place to drop it, and why the last one did not open. */
|
|
58
|
+
function OpenTrace({ onOpen }) {
|
|
59
|
+
const [problem, setProblem] = useState();
|
|
60
|
+
const [reading, setReading] = useState(false);
|
|
61
|
+
const changeAction = async (files) => {
|
|
62
|
+
const file = Array.isArray(files) ? files[0] : files;
|
|
63
|
+
if (!file)
|
|
64
|
+
return;
|
|
65
|
+
setReading(true);
|
|
66
|
+
try {
|
|
67
|
+
onOpen({ name: file.name, records: parseTraceFile(await file.text()) });
|
|
68
|
+
setProblem(undefined);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
setProblem(error instanceof Error ? error.message : String(error));
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
setReading(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return (_jsx(PaneState, { icon: _jsx(Icon, { icon: IconFileImport, size: "lg", color: "secondary" }), title: "Open a trace", description: "See how a conversation went, then replay it through this profile as it is now. A replay answers from the recording, so it needs no keys.", children: _jsxs(VStack, { gap: 2, children: [_jsx(FileInput, { label: "Trace file", isLabelHidden: true, mode: "dropzone", accept: "application/json,.json", isLoading: reading, placeholder: "Drop a trace (.json) or choose a file", value: null, changeAction: changeAction, onChange: () => undefined }), problem ? _jsx(Banner, { status: "error", title: problem }) : null] }) }));
|
|
78
|
+
}
|
|
79
|
+
/** What the replay is doing, or where and why it stopped, with the way on. */
|
|
80
|
+
function StatusBanner({ status, turns, onContinue, onLive, }) {
|
|
81
|
+
const [starting, setStarting] = useState(false);
|
|
82
|
+
const [problem, setProblem] = useState();
|
|
83
|
+
const live = (turn) => (_jsx(Button, { label: "Continue live", variant: "secondary", size: "sm", isLoading: starting, onClick: async () => {
|
|
84
|
+
setStarting(true);
|
|
85
|
+
setProblem(undefined);
|
|
86
|
+
try {
|
|
87
|
+
await onLive(turn);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
setProblem(error instanceof Error ? error.message : String(error));
|
|
91
|
+
setStarting(false);
|
|
92
|
+
}
|
|
93
|
+
} }));
|
|
94
|
+
const banner = (() => {
|
|
95
|
+
switch (status.kind) {
|
|
96
|
+
case 'running':
|
|
97
|
+
return (_jsx(Banner, { status: "info", title: `Replaying ${ordinal(status.turn)} of ${String(turns)}` }));
|
|
98
|
+
case 'waiting':
|
|
99
|
+
return (_jsx(Banner, { status: "info", title: `${ordinal(status.turn - 1)} waits on you`, description: "Answer it in the conversation, then continue the replay.", endContent: _jsx(Button, { label: "Continue replay", variant: "secondary", size: "sm", onClick: () => {
|
|
100
|
+
onContinue(status.turn);
|
|
101
|
+
} }) }));
|
|
102
|
+
case 'missing':
|
|
103
|
+
return (_jsx(Banner, { status: "warning", title: `Replay stopped before ${ordinal(status.turn)}`, description: "It was sent with files, and a trace never holds a file's bytes. Continue live to send it again with yours.", endContent: live(status.turn) }));
|
|
104
|
+
case 'diverged': {
|
|
105
|
+
const { turn, call, reason } = status.divergence;
|
|
106
|
+
return (_jsx(Banner, { status: "warning", title: `Replay stopped at ${ordinal(turn)}, model call ${String(call)}`, description: reason, endContent: live(turn) }));
|
|
107
|
+
}
|
|
108
|
+
case 'failed':
|
|
109
|
+
return (_jsx(Banner, { status: "error", title: `Replay stopped at ${ordinal(status.turn)}`, description: status.message, endContent: live(status.turn) }));
|
|
110
|
+
case 'done':
|
|
111
|
+
return (_jsx(Banner, { status: "success", title: `Replayed ${String(turns)} ${turns === 1 ? 'turn' : 'turns'}`, description: "Every model call this profile made was one the recording answered." }));
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
return (_jsxs(VStack, { gap: 1, children: [banner, problem ? _jsx(Banner, { status: "error", title: problem }) : null] }));
|
|
115
|
+
}
|
|
116
|
+
/** The replay of `script`: the chat it runs in, driven turn by turn, and where it stands. */
|
|
117
|
+
function ReplayRun({ payload, script, liveTools, trace, runtime, onLive, }) {
|
|
118
|
+
const chatRef = useRef(null);
|
|
119
|
+
const diverged = useRef(undefined);
|
|
120
|
+
const [status, setStatus] = useState({ kind: 'running', turn: 0 });
|
|
121
|
+
const [traces] = useState(createTraceFeed);
|
|
122
|
+
const replay = useMemo(() => createReplay(script, payload.profile.id, (divergence) => {
|
|
123
|
+
diverged.current ??= divergence;
|
|
124
|
+
}), [script, payload.profile.id]);
|
|
125
|
+
const ran = useMemo(() => (liveTools ? payload : recordedPayload(payload)), [liveTools, payload]);
|
|
126
|
+
const pageTools = useMemo(() => (liveTools ? studioPageTools(payload) : undefined), [liveTools, payload]);
|
|
127
|
+
const transport = useMemo(() => createBrowserStudioTransport(ran, {
|
|
128
|
+
mode: 'byok',
|
|
129
|
+
provider: replay.provider,
|
|
130
|
+
...(liveTools ? {} : { toolHandlers: replay.toolHandlers }),
|
|
131
|
+
}, { conversationKey: pageConversationKey(), traces }), [ran, replay, liveTools, traces]);
|
|
132
|
+
const [from, setFrom] = useState(0);
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const { signal } = controller;
|
|
136
|
+
void (async () => {
|
|
137
|
+
for (let turn = from; turn < script.turns.length; turn += 1) {
|
|
138
|
+
if (signal.aborted)
|
|
139
|
+
return;
|
|
140
|
+
const { text, attachments } = script.turns[turn];
|
|
141
|
+
if (attachments > 0)
|
|
142
|
+
return setStatus({ kind: 'missing', turn });
|
|
143
|
+
replay.startTurn(turn);
|
|
144
|
+
setStatus({ kind: 'running', turn });
|
|
145
|
+
try {
|
|
146
|
+
// why: The chat takes nothing until it has described its agent; later turns find it ready.
|
|
147
|
+
const sent = turn === 0
|
|
148
|
+
? await sendWhenReady(chatRef, text, signal)
|
|
149
|
+
: ((await chatRef.current?.send(text)) ?? null);
|
|
150
|
+
if (signal.aborted)
|
|
151
|
+
return;
|
|
152
|
+
if (diverged.current)
|
|
153
|
+
return setStatus({ kind: 'diverged', divergence: diverged.current });
|
|
154
|
+
if (!sent) {
|
|
155
|
+
// The turn before waits on a gate: its reply goes on with the calls it has left.
|
|
156
|
+
replay.startTurn(turn - 1);
|
|
157
|
+
return setStatus({ kind: 'waiting', turn });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
return setStatus({ kind: 'failed', turn, message });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
setStatus({ kind: 'done' });
|
|
166
|
+
})();
|
|
167
|
+
return () => {
|
|
168
|
+
controller.abort();
|
|
169
|
+
chatRef.current?.stop();
|
|
170
|
+
};
|
|
171
|
+
}, [from, replay, script]);
|
|
172
|
+
const live = async (turn) => {
|
|
173
|
+
const blocks = script.turns.slice(0, turn).flatMap((each) => each.blocks);
|
|
174
|
+
const started = await startLive(payload, runtime, blocks);
|
|
175
|
+
onLive({ ...started, text: script.turns[turn]?.text ?? '' });
|
|
176
|
+
};
|
|
177
|
+
return (_jsxs(VStack, { height: "100%", children: [_jsx(Section, { variant: "transparent", paddingInline: 3, paddingBlock: 2, children: _jsx(StatusBanner, { status: status, turns: script.turns.length, onContinue: setFrom, onLive: live }) }), _jsx(StackItem, { size: "fill", children: _jsx(TheoremChat, { detectCodeLanguage: true, labels: STUDIO_LABELS, transport: transport, trace: trace, composer: false, chatRef: chatRef, pageTools: pageTools }) })] }));
|
|
178
|
+
}
|
|
179
|
+
const recordedAt = (script) => script.recordedAt === undefined
|
|
180
|
+
? undefined
|
|
181
|
+
: new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(script.recordedAt);
|
|
182
|
+
/** An opened trace: its recorded conversation, or a replay of it, and the controls between them. */
|
|
183
|
+
function OpenedTraceView({ payload, opened, trace, runtime, onClose, onLive, }) {
|
|
184
|
+
const script = useMemo(() => replayScript(opened.records), [opened.records]);
|
|
185
|
+
const [view, setView] = useState('recorded');
|
|
186
|
+
const [attempt, setAttempt] = useState(0);
|
|
187
|
+
const [liveTools, setLiveTools] = useState(false);
|
|
188
|
+
const recorded = useMemo(() => {
|
|
189
|
+
const traces = createTraceFeed();
|
|
190
|
+
for (const record of opened.records)
|
|
191
|
+
traces.push(record);
|
|
192
|
+
return recordedTransport(payload, recordedTranscript(script), traces);
|
|
193
|
+
}, [payload, opened.records, script]);
|
|
194
|
+
const turns = script.turns.length;
|
|
195
|
+
const facts = [
|
|
196
|
+
script.agent || 'No agent',
|
|
197
|
+
`${String(turns)} ${turns === 1 ? 'turn' : 'turns'}`,
|
|
198
|
+
recordedAt(script),
|
|
199
|
+
].filter(Boolean);
|
|
200
|
+
const replayOnce = () => {
|
|
201
|
+
setAttempt((count) => count + 1);
|
|
202
|
+
setView('replayed');
|
|
203
|
+
};
|
|
204
|
+
return (_jsxs(VStack, { height: "100%", children: [_jsx(Section, { variant: "transparent", paddingInline: 3, paddingBlock: 2, children: _jsxs(VStack, { gap: 2, children: [_jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(StackItem, { size: "fill", children: _jsxs(VStack, { gap: 0, children: [_jsx(Text, { type: "label", maxLines: 1, children: opened.name }), _jsx(Text, { type: "supporting", color: "secondary", maxLines: 1, children: facts.join(' · ') })] }) }), attempt > 0 ? (_jsxs(SegmentedControl, { label: "Show", size: "sm", value: view, onChange: (next) => {
|
|
205
|
+
setView(next === 'replayed' ? 'replayed' : 'recorded');
|
|
206
|
+
}, children: [_jsx(SegmentedControlItem, { value: "recorded", label: "Recorded" }), _jsx(SegmentedControlItem, { value: "replayed", label: "Replayed" })] })) : null, _jsx(Switch, { label: "Live tools", size: "sm", value: liveTools, onChange: (next) => {
|
|
207
|
+
setLiveTools(next);
|
|
208
|
+
if (attempt > 0)
|
|
209
|
+
replayOnce();
|
|
210
|
+
} }), _jsx(Button, { label: attempt > 0 ? 'Replay again' : 'Replay', variant: "primary", size: "sm", icon: _jsx(Icon, { icon: IconPlayerPlay, size: "sm" }), isDisabled: turns === 0, onClick: replayOnce }), _jsx(IconButton, { label: "Close trace", variant: "ghost", tooltip: "Close trace", icon: _jsx(Icon, { icon: IconX, size: "sm" }), onClick: onClose })] }), script.agent && script.agent !== payload.profile.id ? (_jsx(Banner, { status: "info", title: `Recorded with ${script.agent}; a replay runs through ${payload.profile.id}.` })) : null, turns === 0 ? (_jsx(Banner, { status: "info", title: "This trace holds no conversation to replay." })) : null] }) }), _jsx(StackItem, { size: "fill", children: view === 'replayed' && attempt > 0 ? (_jsx(ReplayRun, { payload: payload, script: script, liveTools: liveTools, trace: trace, runtime: runtime, onLive: onLive }, attempt)) : (_jsx(TheoremChat, { detectCodeLanguage: true, labels: STUDIO_LABELS, transport: recorded, trace: trace, composer: false, conversation: "recorded", conversationView: { transcript: recordedTranscript(script) } }, opened.name)) })] }));
|
|
211
|
+
}
|
|
212
|
+
/** The trace pane: a place to open one, then what it recorded and its replay. */
|
|
213
|
+
export function TraceReplay({ payload, trace, runtime, onClose, onLive, }) {
|
|
214
|
+
const [opened, setOpened] = useState();
|
|
215
|
+
return opened ? (_jsx(OpenedTraceView, { payload: payload, opened: opened, trace: trace, runtime: runtime, onClose: onClose, onLive: onLive })) : (_jsx(OpenTrace, { onOpen: setOpened }));
|
|
216
|
+
}
|