@flowget/ai-chat 0.1.0
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/CHANGELOG.md +52 -0
- package/LICENSE +105 -0
- package/README.md +228 -0
- package/dist/react.cjs +567 -0
- package/dist/react.d.cts +250 -0
- package/dist/react.d.ts +250 -0
- package/dist/react.js +561 -0
- package/dist/server.cjs +118 -0
- package/dist/server.d.cts +91 -0
- package/dist/server.d.ts +91 -0
- package/dist/server.js +114 -0
- package/dist/styles.css +473 -0
- package/package.json +91 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useCallback, useContext, useMemo, useRef } from 'react';
|
|
3
|
+
import { makeAssistantToolUI, generateId, useLocalRuntime, AssistantRuntimeProvider, AssistantModalPrimitive, ThreadListPrimitive, ThreadPrimitive, MessagePrimitive, ErrorPrimitive, ComposerPrimitive } from '@assistant-ui/react';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/react/WorkflowChat.tsx
|
|
7
|
+
|
|
8
|
+
// src/react/layout.ts
|
|
9
|
+
var ORIGIN_X = 40;
|
|
10
|
+
var ORIGIN_Y = 120;
|
|
11
|
+
var COL_GAP = 240;
|
|
12
|
+
var ROW_GAP = 140;
|
|
13
|
+
function computeDepths(graph) {
|
|
14
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
15
|
+
const children = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const node of graph.nodes) {
|
|
17
|
+
incoming.set(node.id, 0);
|
|
18
|
+
children.set(node.id, []);
|
|
19
|
+
}
|
|
20
|
+
for (const edge of graph.edges) {
|
|
21
|
+
if (!incoming.has(edge.target) || !children.has(edge.source)) continue;
|
|
22
|
+
incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1);
|
|
23
|
+
children.get(edge.source)?.push(edge.target);
|
|
24
|
+
}
|
|
25
|
+
const depth = /* @__PURE__ */ new Map();
|
|
26
|
+
const queue = graph.nodes.filter((node) => (incoming.get(node.id) ?? 0) === 0).map((node) => node.id);
|
|
27
|
+
const first = graph.nodes[0];
|
|
28
|
+
if (queue.length === 0 && first) queue.push(first.id);
|
|
29
|
+
for (const id of queue) depth.set(id, 0);
|
|
30
|
+
let guard = graph.nodes.length * graph.nodes.length + 1;
|
|
31
|
+
while (queue.length > 0 && guard-- > 0) {
|
|
32
|
+
const id = queue.shift();
|
|
33
|
+
if (id === void 0) break;
|
|
34
|
+
const current = depth.get(id) ?? 0;
|
|
35
|
+
for (const child of children.get(id) ?? []) {
|
|
36
|
+
const next = current + 1;
|
|
37
|
+
if (next > (depth.get(child) ?? -1)) {
|
|
38
|
+
depth.set(child, next);
|
|
39
|
+
queue.push(child);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const node of graph.nodes) {
|
|
44
|
+
if (!depth.has(node.id)) depth.set(node.id, 0);
|
|
45
|
+
}
|
|
46
|
+
return depth;
|
|
47
|
+
}
|
|
48
|
+
function layeredPositions(graph) {
|
|
49
|
+
const depth = computeDepths(graph);
|
|
50
|
+
const rowByColumn = /* @__PURE__ */ new Map();
|
|
51
|
+
const positions = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const node of graph.nodes) {
|
|
53
|
+
const col = depth.get(node.id) ?? 0;
|
|
54
|
+
const row = rowByColumn.get(col) ?? 0;
|
|
55
|
+
rowByColumn.set(col, row + 1);
|
|
56
|
+
positions.set(node.id, {
|
|
57
|
+
x: ORIGIN_X + col * COL_GAP,
|
|
58
|
+
y: ORIGIN_Y + row * ROW_GAP
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return positions;
|
|
62
|
+
}
|
|
63
|
+
function toCanvasEdge(edge) {
|
|
64
|
+
return {
|
|
65
|
+
id: edge.id,
|
|
66
|
+
source: edge.source,
|
|
67
|
+
target: edge.target,
|
|
68
|
+
// Drop `null` handles / type; the strict canvas edge wants them absent.
|
|
69
|
+
...typeof edge.sourceHandle === "string" ? { sourceHandle: edge.sourceHandle } : {},
|
|
70
|
+
...typeof edge.targetHandle === "string" ? { targetHandle: edge.targetHandle } : {},
|
|
71
|
+
...typeof edge.type === "string" ? { type: edge.type } : {}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function layoutProposal(proposed, current) {
|
|
75
|
+
const currentPositions = new Map(
|
|
76
|
+
(current?.nodes ?? []).map((node) => [node.id, node.position])
|
|
77
|
+
);
|
|
78
|
+
const layered = layeredPositions(proposed);
|
|
79
|
+
const nodes = proposed.nodes.map((node) => ({
|
|
80
|
+
id: node.id,
|
|
81
|
+
type: node.type,
|
|
82
|
+
data: node.data,
|
|
83
|
+
position: currentPositions.get(node.id) ?? node.position ?? layered.get(node.id) ?? { x: ORIGIN_X, y: ORIGIN_Y }
|
|
84
|
+
}));
|
|
85
|
+
return { nodes, edges: proposed.edges.map(toCanvasEdge) };
|
|
86
|
+
}
|
|
87
|
+
var BridgeContext = createContext(null);
|
|
88
|
+
function BridgeProvider({
|
|
89
|
+
bridge,
|
|
90
|
+
children
|
|
91
|
+
}) {
|
|
92
|
+
return /* @__PURE__ */ jsx(BridgeContext.Provider, { value: bridge, children });
|
|
93
|
+
}
|
|
94
|
+
function useBridge() {
|
|
95
|
+
const bridge = useContext(BridgeContext);
|
|
96
|
+
if (bridge === null) {
|
|
97
|
+
throw new Error("useApplyProposal must be used within <WorkflowChat>");
|
|
98
|
+
}
|
|
99
|
+
return bridge;
|
|
100
|
+
}
|
|
101
|
+
function applyProposal(bridge, proposed) {
|
|
102
|
+
const layout = bridge.layout ?? layoutProposal;
|
|
103
|
+
bridge.applyGraph(layout(proposed, bridge.getCurrentGraph()));
|
|
104
|
+
}
|
|
105
|
+
function useApplyProposal() {
|
|
106
|
+
const bridge = useBridge();
|
|
107
|
+
return useCallback(
|
|
108
|
+
(proposed) => applyProposal(bridge, proposed),
|
|
109
|
+
[bridge]
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/react/contract.ts
|
|
114
|
+
var PROPOSE_TOOL_NAME = "propose_workflow";
|
|
115
|
+
|
|
116
|
+
// src/react/adapter.ts
|
|
117
|
+
function latestUserText(messages) {
|
|
118
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
119
|
+
const message = messages[i];
|
|
120
|
+
if (message?.role !== "user") continue;
|
|
121
|
+
return message.content.filter(
|
|
122
|
+
(part) => part.type === "text"
|
|
123
|
+
).map((part) => part.text).join("\n").trim();
|
|
124
|
+
}
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
function createWorkflowChatAdapter({
|
|
128
|
+
transport,
|
|
129
|
+
getCurrentGraph,
|
|
130
|
+
getRequestMeta
|
|
131
|
+
}) {
|
|
132
|
+
return {
|
|
133
|
+
async *run({ messages, abortSignal, unstable_getMessage }) {
|
|
134
|
+
const inProgress = unstable_getMessage?.();
|
|
135
|
+
const decided = inProgress?.content.find(
|
|
136
|
+
(part) => part.type === "tool-call" && part.toolName === PROPOSE_TOOL_NAME && part.approval?.approved !== void 0
|
|
137
|
+
);
|
|
138
|
+
if (decided?.type === "tool-call") {
|
|
139
|
+
yield { content: [], status: { type: "complete", reason: "stop" } };
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const command = latestUserText(messages);
|
|
143
|
+
const meta = getRequestMeta?.() ?? {};
|
|
144
|
+
let text = "";
|
|
145
|
+
let terminal;
|
|
146
|
+
for await (const event of transport(
|
|
147
|
+
{
|
|
148
|
+
command,
|
|
149
|
+
currentGraph: getCurrentGraph(),
|
|
150
|
+
...meta.actor !== void 0 ? { actor: meta.actor } : {},
|
|
151
|
+
...meta.context !== void 0 ? { context: meta.context } : {}
|
|
152
|
+
},
|
|
153
|
+
abortSignal
|
|
154
|
+
)) {
|
|
155
|
+
if (event.kind === "text-delta") {
|
|
156
|
+
text += event.delta;
|
|
157
|
+
yield { content: [{ type: "text", text }], status: { type: "running" } };
|
|
158
|
+
} else if (event.kind === "message") {
|
|
159
|
+
terminal = {
|
|
160
|
+
content: [{ type: "text", text: event.text || text }],
|
|
161
|
+
status: { type: "complete", reason: "stop" }
|
|
162
|
+
};
|
|
163
|
+
} else if (event.kind === "proposal") {
|
|
164
|
+
const id = generateId();
|
|
165
|
+
const args = { graph: event.graph, summary: event.summary };
|
|
166
|
+
terminal = {
|
|
167
|
+
content: [
|
|
168
|
+
...text ? [{ type: "text", text }] : [],
|
|
169
|
+
{
|
|
170
|
+
type: "tool-call",
|
|
171
|
+
toolCallId: id,
|
|
172
|
+
toolName: PROPOSE_TOOL_NAME,
|
|
173
|
+
args,
|
|
174
|
+
argsText: JSON.stringify(args),
|
|
175
|
+
approval: { id }
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
status: { type: "requires-action", reason: "tool-calls" }
|
|
179
|
+
};
|
|
180
|
+
} else if (event.kind === "error") {
|
|
181
|
+
throw new Error(event.error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
yield terminal ?? {
|
|
185
|
+
content: [{ type: "text", text }],
|
|
186
|
+
status: { type: "complete", reason: "stop" }
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/react/transport.ts
|
|
193
|
+
function parseFrame(frame) {
|
|
194
|
+
const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
195
|
+
if (data === "") return null;
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(data);
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function httpChatStreamTransport(endpoint = "/api/chat") {
|
|
203
|
+
return async function* (request, signal) {
|
|
204
|
+
const res = await fetch(endpoint, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
"Content-Type": "application/json",
|
|
208
|
+
Accept: "text/event-stream"
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify(request),
|
|
211
|
+
...signal ? { signal } : {}
|
|
212
|
+
});
|
|
213
|
+
if (!res.ok || res.body === null) {
|
|
214
|
+
throw new Error(`Chat request failed (${res.status}): ${res.statusText}`);
|
|
215
|
+
}
|
|
216
|
+
const reader = res.body.getReader();
|
|
217
|
+
const decoder = new TextDecoder();
|
|
218
|
+
let buffer = "";
|
|
219
|
+
for (; ; ) {
|
|
220
|
+
const { done, value } = await reader.read();
|
|
221
|
+
if (done) break;
|
|
222
|
+
buffer += decoder.decode(value, { stream: true });
|
|
223
|
+
let sep = buffer.indexOf("\n\n");
|
|
224
|
+
while (sep !== -1) {
|
|
225
|
+
const frame = buffer.slice(0, sep);
|
|
226
|
+
buffer = buffer.slice(sep + 2);
|
|
227
|
+
const event = parseFrame(frame);
|
|
228
|
+
if (event !== null) yield event;
|
|
229
|
+
sep = buffer.indexOf("\n\n");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const tail = parseFrame(buffer);
|
|
233
|
+
if (tail !== null) yield tail;
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function ChatRuntimeProvider({
|
|
237
|
+
children,
|
|
238
|
+
getCurrentGraph,
|
|
239
|
+
transport,
|
|
240
|
+
getRequestMeta
|
|
241
|
+
}) {
|
|
242
|
+
const adapter = useMemo(
|
|
243
|
+
() => createWorkflowChatAdapter({
|
|
244
|
+
transport: transport ?? httpChatStreamTransport(),
|
|
245
|
+
getCurrentGraph,
|
|
246
|
+
...getRequestMeta ? { getRequestMeta } : {}
|
|
247
|
+
}),
|
|
248
|
+
[transport, getCurrentGraph, getRequestMeta]
|
|
249
|
+
);
|
|
250
|
+
const runtime = useLocalRuntime(adapter);
|
|
251
|
+
return /* @__PURE__ */ jsx(AssistantRuntimeProvider, { runtime, children });
|
|
252
|
+
}
|
|
253
|
+
function SparkleIcon({ size = 16, className }) {
|
|
254
|
+
return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className, "aria-hidden": "true", children: [
|
|
255
|
+
/* @__PURE__ */ jsx(
|
|
256
|
+
"path",
|
|
257
|
+
{
|
|
258
|
+
d: "M12 3l1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3z",
|
|
259
|
+
fill: "currentColor"
|
|
260
|
+
}
|
|
261
|
+
),
|
|
262
|
+
/* @__PURE__ */ jsx("path", { d: "M19 14l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8.8-2z", fill: "currentColor", opacity: "0.7" })
|
|
263
|
+
] });
|
|
264
|
+
}
|
|
265
|
+
function CloseIcon({ size = 16, className }) {
|
|
266
|
+
return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className, "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6L6 18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) });
|
|
267
|
+
}
|
|
268
|
+
function SendIcon({ size = 16, className }) {
|
|
269
|
+
return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className, "aria-hidden": "true", children: /* @__PURE__ */ jsx(
|
|
270
|
+
"path",
|
|
271
|
+
{
|
|
272
|
+
d: "M12 20V5M12 5l-6 6M12 5l6 6",
|
|
273
|
+
stroke: "currentColor",
|
|
274
|
+
strokeWidth: "2",
|
|
275
|
+
strokeLinecap: "round",
|
|
276
|
+
strokeLinejoin: "round"
|
|
277
|
+
}
|
|
278
|
+
) });
|
|
279
|
+
}
|
|
280
|
+
function StopIcon({ size = 14, className }) {
|
|
281
|
+
return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className, "aria-hidden": "true", children: /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2", fill: "currentColor" }) });
|
|
282
|
+
}
|
|
283
|
+
function ComposeIcon({ size = 15, className }) {
|
|
284
|
+
return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className, "aria-hidden": "true", children: [
|
|
285
|
+
/* @__PURE__ */ jsx("path", { d: "M12 20h9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
|
|
286
|
+
/* @__PURE__ */ jsx(
|
|
287
|
+
"path",
|
|
288
|
+
{
|
|
289
|
+
d: "M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z",
|
|
290
|
+
stroke: "currentColor",
|
|
291
|
+
strokeWidth: "2",
|
|
292
|
+
strokeLinecap: "round",
|
|
293
|
+
strokeLinejoin: "round"
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
] });
|
|
297
|
+
}
|
|
298
|
+
function CheckIcon({ size = 16, className }) {
|
|
299
|
+
return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", className, "aria-hidden": "true", children: /* @__PURE__ */ jsx(
|
|
300
|
+
"path",
|
|
301
|
+
{
|
|
302
|
+
d: "M20 6L9 17l-5-5",
|
|
303
|
+
stroke: "currentColor",
|
|
304
|
+
strokeWidth: "2.2",
|
|
305
|
+
strokeLinecap: "round",
|
|
306
|
+
strokeLinejoin: "round"
|
|
307
|
+
}
|
|
308
|
+
) });
|
|
309
|
+
}
|
|
310
|
+
var DEFAULT_EXAMPLES = [
|
|
311
|
+
"Explain the workflow in a single sentence",
|
|
312
|
+
"Email ops when a new order arrives",
|
|
313
|
+
"On a schedule, fetch a URL and log the response",
|
|
314
|
+
"Add a 5-second delay before the final step"
|
|
315
|
+
];
|
|
316
|
+
function EmptyState({ examples }) {
|
|
317
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-aichat-empty", children: [
|
|
318
|
+
/* @__PURE__ */ jsx("span", { className: "fg-aichat-empty__mark", children: /* @__PURE__ */ jsx(SparkleIcon, { size: 22 }) }),
|
|
319
|
+
/* @__PURE__ */ jsx("p", { className: "fg-aichat-empty__title", children: "Describe a workflow" }),
|
|
320
|
+
/* @__PURE__ */ jsx("p", { className: "fg-aichat-empty__hint", children: "Tell me what it should do \u2014 I\u2019ll draft it, and you apply it to the canvas." }),
|
|
321
|
+
examples.length > 0 && /* @__PURE__ */ jsx("div", { className: "fg-aichat-empty__examples", children: examples.map((text) => /* @__PURE__ */ jsx(
|
|
322
|
+
ThreadPrimitive.Suggestion,
|
|
323
|
+
{
|
|
324
|
+
className: "fg-aichat-chip",
|
|
325
|
+
prompt: text,
|
|
326
|
+
send: true,
|
|
327
|
+
children: text
|
|
328
|
+
},
|
|
329
|
+
text
|
|
330
|
+
)) })
|
|
331
|
+
] });
|
|
332
|
+
}
|
|
333
|
+
function UserMessage() {
|
|
334
|
+
return /* @__PURE__ */ jsx(MessagePrimitive.Root, { className: "fg-aichat-msg fg-aichat-msg--user", children: /* @__PURE__ */ jsx("div", { className: "fg-aichat-msg__bubble", children: /* @__PURE__ */ jsx(MessagePrimitive.Parts, {}) }) });
|
|
335
|
+
}
|
|
336
|
+
var ThinkingEmpty = ({ status }) => {
|
|
337
|
+
if (status.type !== "running") return null;
|
|
338
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-aichat-thinking", role: "status", children: [
|
|
339
|
+
/* @__PURE__ */ jsx("span", { className: "fg-aichat-thinking__text", children: "Thinking" }),
|
|
340
|
+
/* @__PURE__ */ jsxs("span", { className: "fg-aichat-thinking__dots", "aria-hidden": "true", children: [
|
|
341
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
342
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
343
|
+
/* @__PURE__ */ jsx("span", {})
|
|
344
|
+
] })
|
|
345
|
+
] });
|
|
346
|
+
};
|
|
347
|
+
var ASSISTANT_PARTS = { Empty: ThinkingEmpty };
|
|
348
|
+
function AssistantMessage() {
|
|
349
|
+
return /* @__PURE__ */ jsx(MessagePrimitive.Root, { className: "fg-aichat-msg fg-aichat-msg--assistant", children: /* @__PURE__ */ jsxs("div", { className: "fg-aichat-msg__body", children: [
|
|
350
|
+
/* @__PURE__ */ jsx(MessagePrimitive.Parts, { components: ASSISTANT_PARTS }),
|
|
351
|
+
/* @__PURE__ */ jsx(MessagePrimitive.Error, { children: /* @__PURE__ */ jsx(ErrorPrimitive.Root, { className: "fg-aichat-error", children: /* @__PURE__ */ jsx(ErrorPrimitive.Message, { className: "fg-aichat-error__msg" }) }) })
|
|
352
|
+
] }) });
|
|
353
|
+
}
|
|
354
|
+
function Composer() {
|
|
355
|
+
return /* @__PURE__ */ jsx("div", { className: "fg-aichat-composer", children: /* @__PURE__ */ jsxs(ComposerPrimitive.Root, { className: "fg-aichat-composer__field", children: [
|
|
356
|
+
/* @__PURE__ */ jsx(
|
|
357
|
+
ComposerPrimitive.Input,
|
|
358
|
+
{
|
|
359
|
+
className: "fg-aichat-composer__input",
|
|
360
|
+
placeholder: "Describe or edit a workflow\u2026",
|
|
361
|
+
rows: 1
|
|
362
|
+
}
|
|
363
|
+
),
|
|
364
|
+
/* @__PURE__ */ jsx(ThreadPrimitive.If, { running: false, children: /* @__PURE__ */ jsx(ComposerPrimitive.Send, { className: "fg-aichat-send", "aria-label": "Send", children: /* @__PURE__ */ jsx(SendIcon, { size: 16 }) }) }),
|
|
365
|
+
/* @__PURE__ */ jsx(ThreadPrimitive.If, { running: true, children: /* @__PURE__ */ jsx(
|
|
366
|
+
ComposerPrimitive.Cancel,
|
|
367
|
+
{
|
|
368
|
+
className: "fg-aichat-send fg-aichat-send--stop",
|
|
369
|
+
"aria-label": "Stop",
|
|
370
|
+
children: /* @__PURE__ */ jsx(StopIcon, { size: 13 })
|
|
371
|
+
}
|
|
372
|
+
) })
|
|
373
|
+
] }) });
|
|
374
|
+
}
|
|
375
|
+
function Thread({ examples = DEFAULT_EXAMPLES } = {}) {
|
|
376
|
+
return /* @__PURE__ */ jsxs(ThreadPrimitive.Root, { className: "fg-aichat-thread", children: [
|
|
377
|
+
/* @__PURE__ */ jsxs(ThreadPrimitive.Viewport, { className: "fg-aichat-thread__viewport", children: [
|
|
378
|
+
/* @__PURE__ */ jsx(ThreadPrimitive.Empty, { children: /* @__PURE__ */ jsx(EmptyState, { examples }) }),
|
|
379
|
+
/* @__PURE__ */ jsx(
|
|
380
|
+
ThreadPrimitive.Messages,
|
|
381
|
+
{
|
|
382
|
+
components: { UserMessage, AssistantMessage }
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
] }),
|
|
386
|
+
/* @__PURE__ */ jsx(Composer, {})
|
|
387
|
+
] });
|
|
388
|
+
}
|
|
389
|
+
var ProposalToolUI = makeAssistantToolUI({
|
|
390
|
+
toolName: PROPOSE_TOOL_NAME,
|
|
391
|
+
display: "standalone",
|
|
392
|
+
render: function ProposalCard({ args, status, approval, respondToApproval }) {
|
|
393
|
+
const applyProposal2 = useApplyProposal();
|
|
394
|
+
if (args?.graph === void 0) {
|
|
395
|
+
return /* @__PURE__ */ jsx("div", { className: "fg-aichat-card fg-aichat-card--drafting", children: "Drafting\u2026" });
|
|
396
|
+
}
|
|
397
|
+
const { graph, summary } = args;
|
|
398
|
+
const decided = approval?.approved;
|
|
399
|
+
if (decided === true) {
|
|
400
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-aichat-applied", children: [
|
|
401
|
+
/* @__PURE__ */ jsx("span", { className: "fg-aichat-applied__badge", children: /* @__PURE__ */ jsx(CheckIcon, { size: 13 }) }),
|
|
402
|
+
/* @__PURE__ */ jsx("p", { className: "fg-aichat-applied__summary", children: summary })
|
|
403
|
+
] });
|
|
404
|
+
}
|
|
405
|
+
if (decided === false) {
|
|
406
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-aichat-dismissed", children: [
|
|
407
|
+
/* @__PURE__ */ jsx("p", { className: "fg-aichat-dismissed__summary", children: summary }),
|
|
408
|
+
/* @__PURE__ */ jsx("span", { className: "fg-aichat-dismissed__tag", children: "Dismissed" })
|
|
409
|
+
] });
|
|
410
|
+
}
|
|
411
|
+
const apply = () => {
|
|
412
|
+
applyProposal2(graph);
|
|
413
|
+
respondToApproval({ approved: true });
|
|
414
|
+
};
|
|
415
|
+
const dismiss = () => {
|
|
416
|
+
respondToApproval({ approved: false, reason: "Dismissed by user" });
|
|
417
|
+
};
|
|
418
|
+
const busy = status.type === "running";
|
|
419
|
+
const stepCount = graph.nodes.length;
|
|
420
|
+
return /* @__PURE__ */ jsxs("div", { className: "fg-aichat-card fg-aichat-card--proposal", children: [
|
|
421
|
+
/* @__PURE__ */ jsx("div", { className: "fg-aichat-card__label", children: "Proposed workflow" }),
|
|
422
|
+
/* @__PURE__ */ jsx("p", { className: "fg-aichat-card__summary", children: summary }),
|
|
423
|
+
/* @__PURE__ */ jsxs("div", { className: "fg-aichat-card__meta", children: [
|
|
424
|
+
stepCount,
|
|
425
|
+
" step",
|
|
426
|
+
stepCount === 1 ? "" : "s"
|
|
427
|
+
] }),
|
|
428
|
+
/* @__PURE__ */ jsxs("div", { className: "fg-aichat-card__actions", children: [
|
|
429
|
+
/* @__PURE__ */ jsx(
|
|
430
|
+
"button",
|
|
431
|
+
{
|
|
432
|
+
type: "button",
|
|
433
|
+
className: "fg-aichat-btn fg-aichat-btn--ghost",
|
|
434
|
+
onClick: dismiss,
|
|
435
|
+
disabled: busy,
|
|
436
|
+
children: "Dismiss"
|
|
437
|
+
}
|
|
438
|
+
),
|
|
439
|
+
/* @__PURE__ */ jsx(
|
|
440
|
+
"button",
|
|
441
|
+
{
|
|
442
|
+
type: "button",
|
|
443
|
+
className: "fg-aichat-btn fg-aichat-btn--primary",
|
|
444
|
+
onClick: apply,
|
|
445
|
+
disabled: busy,
|
|
446
|
+
children: "Apply to canvas"
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
] })
|
|
450
|
+
] });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
var DEFAULT_HEADING = "Workflow AI";
|
|
454
|
+
var DEFAULT_SUBTITLE = "Draft & edit with a prompt";
|
|
455
|
+
function ChatHeader({
|
|
456
|
+
heading,
|
|
457
|
+
subtitle
|
|
458
|
+
}) {
|
|
459
|
+
return /* @__PURE__ */ jsxs("header", { className: "fg-aichat-dock__head", children: [
|
|
460
|
+
/* @__PURE__ */ jsxs("div", { className: "fg-aichat-dock__brand", children: [
|
|
461
|
+
/* @__PURE__ */ jsx("span", { className: "fg-aichat-dock__mark", children: /* @__PURE__ */ jsx(SparkleIcon, { size: 15 }) }),
|
|
462
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
463
|
+
/* @__PURE__ */ jsx("div", { className: "fg-aichat-dock__title", children: heading }),
|
|
464
|
+
/* @__PURE__ */ jsx("div", { className: "fg-aichat-dock__subtitle", children: subtitle })
|
|
465
|
+
] })
|
|
466
|
+
] }),
|
|
467
|
+
/* @__PURE__ */ jsxs(ThreadListPrimitive.New, { className: "fg-aichat-newchat", children: [
|
|
468
|
+
/* @__PURE__ */ jsx(ComposeIcon, { size: 14 }),
|
|
469
|
+
/* @__PURE__ */ jsx("span", { children: "New chat" })
|
|
470
|
+
] })
|
|
471
|
+
] });
|
|
472
|
+
}
|
|
473
|
+
function WorkflowChat({
|
|
474
|
+
currentGraph,
|
|
475
|
+
applyGraph,
|
|
476
|
+
transport,
|
|
477
|
+
endpoint,
|
|
478
|
+
heading = DEFAULT_HEADING,
|
|
479
|
+
subtitle = DEFAULT_SUBTITLE,
|
|
480
|
+
examples,
|
|
481
|
+
layout,
|
|
482
|
+
actor,
|
|
483
|
+
context
|
|
484
|
+
}) {
|
|
485
|
+
const graphRef = useRef(currentGraph);
|
|
486
|
+
graphRef.current = currentGraph;
|
|
487
|
+
const applyRef = useRef(applyGraph);
|
|
488
|
+
applyRef.current = applyGraph;
|
|
489
|
+
const layoutRef = useRef(layout);
|
|
490
|
+
layoutRef.current = layout;
|
|
491
|
+
const metaRef = useRef({});
|
|
492
|
+
metaRef.current = {
|
|
493
|
+
...actor !== void 0 ? { actor } : {},
|
|
494
|
+
...context !== void 0 ? { context } : {}
|
|
495
|
+
};
|
|
496
|
+
const bridge = useMemo(
|
|
497
|
+
() => ({
|
|
498
|
+
getCurrentGraph: () => graphRef.current,
|
|
499
|
+
applyGraph: (graph) => applyRef.current(graph),
|
|
500
|
+
layout: (proposed, current) => (layoutRef.current ?? layoutProposal)(proposed, current)
|
|
501
|
+
}),
|
|
502
|
+
[]
|
|
503
|
+
);
|
|
504
|
+
const resolvedTransport = useMemo(
|
|
505
|
+
() => transport ?? httpChatStreamTransport(endpoint),
|
|
506
|
+
[transport, endpoint]
|
|
507
|
+
);
|
|
508
|
+
const getCurrentGraph = useCallback(() => graphRef.current, []);
|
|
509
|
+
const getRequestMeta = useCallback(() => metaRef.current, []);
|
|
510
|
+
return /* @__PURE__ */ jsx(BridgeProvider, { bridge, children: /* @__PURE__ */ jsxs(
|
|
511
|
+
ChatRuntimeProvider,
|
|
512
|
+
{
|
|
513
|
+
getCurrentGraph,
|
|
514
|
+
transport: resolvedTransport,
|
|
515
|
+
getRequestMeta,
|
|
516
|
+
children: [
|
|
517
|
+
/* @__PURE__ */ jsxs(AssistantModalPrimitive.Root, { children: [
|
|
518
|
+
/* @__PURE__ */ jsx(AssistantModalPrimitive.Anchor, { className: "fg-aichat-anchor", children: /* @__PURE__ */ jsxs(
|
|
519
|
+
AssistantModalPrimitive.Trigger,
|
|
520
|
+
{
|
|
521
|
+
className: "fg-aichat-launcher",
|
|
522
|
+
"aria-label": "Toggle workflow AI",
|
|
523
|
+
children: [
|
|
524
|
+
/* @__PURE__ */ jsx(
|
|
525
|
+
SparkleIcon,
|
|
526
|
+
{
|
|
527
|
+
size: 20,
|
|
528
|
+
className: "fg-aichat-launcher__icon fg-aichat-launcher__icon--open"
|
|
529
|
+
}
|
|
530
|
+
),
|
|
531
|
+
/* @__PURE__ */ jsx(
|
|
532
|
+
CloseIcon,
|
|
533
|
+
{
|
|
534
|
+
size: 20,
|
|
535
|
+
className: "fg-aichat-launcher__icon fg-aichat-launcher__icon--close"
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
]
|
|
539
|
+
}
|
|
540
|
+
) }),
|
|
541
|
+
/* @__PURE__ */ jsxs(
|
|
542
|
+
AssistantModalPrimitive.Content,
|
|
543
|
+
{
|
|
544
|
+
className: "fg-aichat-panel",
|
|
545
|
+
side: "top",
|
|
546
|
+
align: "end",
|
|
547
|
+
sideOffset: 12,
|
|
548
|
+
children: [
|
|
549
|
+
/* @__PURE__ */ jsx(ChatHeader, { heading, subtitle }),
|
|
550
|
+
/* @__PURE__ */ jsx(Thread, { ...examples !== void 0 ? { examples } : {} })
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
] }),
|
|
555
|
+
/* @__PURE__ */ jsx(ProposalToolUI, {})
|
|
556
|
+
]
|
|
557
|
+
}
|
|
558
|
+
) });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export { ChatRuntimeProvider, WorkflowChat, createWorkflowChatAdapter, httpChatStreamTransport, layoutProposal };
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ai = require('@flowget/ai');
|
|
4
|
+
|
|
5
|
+
// src/server/index.ts
|
|
6
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
7
|
+
var DEFAULT_MAX_COMMAND_LENGTH = 8e3;
|
|
8
|
+
async function parseChatRequest(req, limits = {}) {
|
|
9
|
+
const maxBodyBytes = limits.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
10
|
+
const maxCommandLength = limits.maxCommandLength ?? DEFAULT_MAX_COMMAND_LENGTH;
|
|
11
|
+
const declared = Number(req.headers.get("content-length"));
|
|
12
|
+
if (Number.isFinite(declared) && declared > maxBodyBytes) return null;
|
|
13
|
+
let text;
|
|
14
|
+
try {
|
|
15
|
+
text = await req.text();
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (text.length > maxBodyBytes) return null;
|
|
20
|
+
let body;
|
|
21
|
+
try {
|
|
22
|
+
body = JSON.parse(text);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const record = body;
|
|
30
|
+
const command = typeof record.command === "string" ? record.command : "";
|
|
31
|
+
if (command.trim() === "" || command.length > maxCommandLength) return null;
|
|
32
|
+
const currentGraph = record.currentGraph !== null && typeof record.currentGraph === "object" && !Array.isArray(record.currentGraph) ? record.currentGraph : void 0;
|
|
33
|
+
const actor = record.actor !== null && typeof record.actor === "object" && !Array.isArray(record.actor) ? record.actor : void 0;
|
|
34
|
+
return {
|
|
35
|
+
command,
|
|
36
|
+
...currentGraph !== void 0 ? { currentGraph } : {},
|
|
37
|
+
...actor !== void 0 ? { actor } : {},
|
|
38
|
+
...record.context !== void 0 ? { context: record.context } : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function sseFrame(event) {
|
|
42
|
+
return `data: ${JSON.stringify(event)}
|
|
43
|
+
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
var SSE_HEADERS = {
|
|
47
|
+
"content-type": "text/event-stream",
|
|
48
|
+
"cache-control": "no-cache, no-transform",
|
|
49
|
+
// Disable proxy buffering (nginx) so events flush as they're produced.
|
|
50
|
+
"x-accel-buffering": "no"
|
|
51
|
+
};
|
|
52
|
+
function streamAuthorSSE(request, config, options = {}) {
|
|
53
|
+
const { signal, onError } = options;
|
|
54
|
+
const stream = new ReadableStream({
|
|
55
|
+
async start(controller) {
|
|
56
|
+
const encoder = new TextEncoder();
|
|
57
|
+
let closed = false;
|
|
58
|
+
const send = (event) => {
|
|
59
|
+
if (closed) return;
|
|
60
|
+
try {
|
|
61
|
+
controller.enqueue(encoder.encode(sseFrame(event)));
|
|
62
|
+
} catch {
|
|
63
|
+
closed = true;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
const resolved = typeof config === "function" ? await config(request) : config;
|
|
68
|
+
for await (const event of ai.authorStream(resolved, request, {
|
|
69
|
+
...signal ? { signal } : {}
|
|
70
|
+
})) {
|
|
71
|
+
send(event);
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error("[@flowget/ai-chat] authoring stream failed:", err);
|
|
75
|
+
send({
|
|
76
|
+
kind: "error",
|
|
77
|
+
error: onError ? onError(err) : "Authoring failed"
|
|
78
|
+
});
|
|
79
|
+
} finally {
|
|
80
|
+
closed = true;
|
|
81
|
+
try {
|
|
82
|
+
controller.close();
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return new Response(stream, { status: 200, headers: { ...SSE_HEADERS } });
|
|
89
|
+
}
|
|
90
|
+
async function createChatStreamResponse(req, config, options = {}) {
|
|
91
|
+
const { buildRequest, onError, ...limits } = options;
|
|
92
|
+
const parsed = await parseChatRequest(req, limits);
|
|
93
|
+
if (parsed === null) {
|
|
94
|
+
const body = sseFrame({ kind: "error", error: "Invalid request" });
|
|
95
|
+
return new Response(body, { status: 200, headers: { ...SSE_HEADERS } });
|
|
96
|
+
}
|
|
97
|
+
let request = parsed;
|
|
98
|
+
if (buildRequest) {
|
|
99
|
+
try {
|
|
100
|
+
request = await buildRequest(parsed, req);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error("[@flowget/ai-chat] buildRequest failed:", err);
|
|
103
|
+
const body = sseFrame({
|
|
104
|
+
kind: "error",
|
|
105
|
+
error: onError ? onError(err) : "Authoring failed"
|
|
106
|
+
});
|
|
107
|
+
return new Response(body, { status: 200, headers: { ...SSE_HEADERS } });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return streamAuthorSSE(request, config, {
|
|
111
|
+
...req.signal ? { signal: req.signal } : {},
|
|
112
|
+
...onError ? { onError } : {}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
exports.createChatStreamResponse = createChatStreamResponse;
|
|
117
|
+
exports.parseChatRequest = parseChatRequest;
|
|
118
|
+
exports.streamAuthorSSE = streamAuthorSSE;
|