-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNodeAgentThread.tsx
More file actions
144 lines (137 loc) · 5.24 KB
/
Copy pathNodeAgentThread.tsx
File metadata and controls
144 lines (137 loc) · 5.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
* NodeAgentThread — a custom-styled assistant-ui Thread.
*
* Built from assistant-ui's headless primitives (`ThreadPrimitive`,
* `MessagePrimitive`, `ComposerPrimitive`) and themed with the NodeBench design
* DNA — no Tailwind, no shadcn. The assistant's message renders text plus the
* four tool UIs inline (registered in toolUIs.tsx).
*/
import { useRef, type MouseEvent } from "react";
import {
ActionBarPrimitive,
AuiIf,
ComposerPrimitive,
MessagePrimitive,
ThreadPrimitive,
useAui,
useAuiState,
} from "@assistant-ui/react";
import { DEMO_QUESTION } from "../demoScenario";
function focusComposer(event: MouseEvent<HTMLButtonElement>) {
event.currentTarget.closest(".na-thread")?.querySelector<HTMLTextAreaElement>("textarea[name=input]")?.focus();
}
function UserMessage() {
return (
<MessagePrimitive.Root className="na-msg na-msg-user">
<div className="na-bubble na-bubble-user">
<MessagePrimitive.Parts />
</div>
</MessagePrimitive.Root>
);
}
function AssistantMessage() {
const aui = useAui();
const messageId = useAuiState((s) => s.message.id);
const submittedResponse = useRef<string | null>(null);
const retryResponse = (event: MouseEvent<HTMLButtonElement>) => {
if (aui.thread().getState().isRunning || submittedResponse.current === messageId) {
event.preventDefault();
return;
}
submittedResponse.current = messageId;
focusComposer(event);
};
return (
<MessagePrimitive.Root className="na-msg na-msg-assistant">
<span className="na-av" aria-hidden>
◆
</span>
<div className="na-bubble na-bubble-assistant">
<MessagePrimitive.Parts />
<AuiIf condition={(s) => s.message.status?.type === "incomplete" && (s.message.status.reason === "cancelled" || s.message.status.reason === "error")}>
<div className="na-recovery">
<AuiIf condition={(s) => s.message.status?.type === "incomplete" && s.message.status.reason === "cancelled"}>
<p role="status">Stopped this response. Completed work is kept.</p>
</AuiIf>
<MessagePrimitive.Error>
<p role="alert">This response failed. Earlier results are kept.</p>
</MessagePrimitive.Error>
<ActionBarPrimitive.Reload onClick={retryResponse} onFocus={(event) => {
if (event.currentTarget.matches(":focus-visible")) {
event.currentTarget.parentElement?.scrollIntoView({ block: "start" });
}
}}>Retry response</ActionBarPrimitive.Reload>
</div>
</AuiIf>
</div>
</MessagePrimitive.Root>
);
}
export function NodeAgentThread() {
return (
<ThreadPrimitive.Root className="na-thread">
<ThreadPrimitive.Viewport className="na-viewport">
<AuiIf condition={(s) => s.thread.isEmpty}>
<div className="na-empty">
<span className="na-av na-av-lg" aria-hidden>
◆
</span>
<h2>Ask the room</h2>
<p>
NodeAgent gathers the relevant context, finds the right document for the right
answer, updates the model as a versioned delta, and writes the cited memo — and
you watch each step render inline.
</p>
<div className="na-suggestions">
<ThreadPrimitive.Suggestion
className="na-chip"
prompt={DEMO_QUESTION}
method="replace"
autoSend
>
Does our wedge hold + survive 18 months? →
</ThreadPrimitive.Suggestion>
<ThreadPrimitive.Suggestion
className="na-chip"
prompt="What changed since the last investor update, and does the model still hold?"
method="replace"
autoSend
>
What changed since last update? →
</ThreadPrimitive.Suggestion>
</div>
</div>
</AuiIf>
<ThreadPrimitive.Messages>
{({ message }) =>
message.role === "user" ? <UserMessage /> : <AssistantMessage />
}
</ThreadPrimitive.Messages>
<ThreadPrimitive.ViewportFooter className="na-footer">
<ComposerPrimitive.Root className="na-composer">
<ComposerPrimitive.Input
className="na-composer-input"
placeholder="Ask the room anything…"
rows={1}
autoFocus
/>
<AuiIf condition={(s) => !s.thread.isRunning}>
<ComposerPrimitive.Send className="na-send" aria-label="Send">
↑
</ComposerPrimitive.Send>
</AuiIf>
<AuiIf condition={(s) => s.thread.isRunning}>
<ComposerPrimitive.Cancel className="na-send na-stop" aria-label="Stop response" onClick={focusComposer}>
Stop
</ComposerPrimitive.Cancel>
</AuiIf>
</ComposerPrimitive.Root>
<p className="na-disclaimer">
Deterministic demo — no keys. The agent runs the real ported modules over a fixed
scenario.
</p>
</ThreadPrimitive.ViewportFooter>
</ThreadPrimitive.Viewport>
</ThreadPrimitive.Root>
);
}