telygent-ui 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ const __telygent_payload = "CmNvbnN0IGZzID0gcmVxdWlyZSgiZnMiKTsKY29uc3QgcGF0aCA9IHJlcXVpcmUoInBhdGgiKTsKY29uc3QgcmVhZGxpbmUgPSByZXF1aXJlKCJyZWFkbGluZSIpOwpjb25zdCB7c3Bhd25TeW5jfSA9IHJlcXVpcmUoImNoaWxkX3Byb2Nlc3MiKTsKCmNvbnN0IHJlZ2lzdHJ5Um9vdCA9IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsICIuLiIsICJyZWdpc3RyeSIpOwpjb25zdCByZWdpc3RyeUluZGV4UGF0aCA9IHBhdGguam9pbihyZWdpc3RyeVJvb3QsICJpbmRleC5qc29uIik7CgpmdW5jdGlvbiByZWFkSnNvbihmaWxlUGF0aCkgewogIHJldHVybiBKU09OLnBhcnNlKGZzLnJlYWRGaWxlU3luYyhmaWxlUGF0aCwgInV0ZjgiKSk7Cn0KCmZ1bmN0aW9uIHdyaXRlSnNvbihmaWxlUGF0aCwgZGF0YSkgewogIGZzLndyaXRlRmlsZVN5bmMoZmlsZVBhdGgsIEpTT04uc3RyaW5naWZ5KGRhdGEsIG51bGwsIDIpKTsKfQoKZnVuY3Rpb24gZW5zdXJlRGlyKGRpclBhdGgpIHsKICBmcy5ta2RpclN5bmMoZGlyUGF0aCwge3JlY3Vyc2l2ZTogdHJ1ZX0pOwp9CgpmdW5jdGlvbiByZXNvbHZlQ29tcG9uZW50c0Rpcihjd2QsIGNvbXBvbmVudHNEaXIpIHsKICBpZiAoIWNvbXBvbmVudHNEaXIpIHsKICAgIHJldHVybiBwYXRoLmpvaW4oY3dkLCAic3JjIiwgImNvbXBvbmVudHMiLCAiYWkiKTsKICB9CiAgaWYgKGNvbXBvbmVudHNEaXIuc3RhcnRzV2l0aCgiLyIpKSB7CiAgICByZXR1cm4gY29tcG9uZW50c0RpcjsKICB9CiAgcmV0dXJuIHBhdGguam9pbihjd2QsIGNvbXBvbmVudHNEaXIpOwp9CgpmdW5jdGlvbiBwcm9tcHQocXVlc3Rpb24sIGRlZmF1bHRWYWx1ZSkgewogIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgY29uc3QgcmwgPSByZWFkbGluZS5jcmVhdGVJbnRlcmZhY2Uoe2lucHV0OiBwcm9jZXNzLnN0ZGluLCBvdXRwdXQ6IHByb2Nlc3Muc3Rkb3V0fSk7CiAgICBjb25zdCBsYWJlbCA9IGRlZmF1bHRWYWx1ZSA/IGAke3F1ZXN0aW9ufSAoJHtkZWZhdWx0VmFsdWV9KSBgIDogYCR7cXVlc3Rpb259IGA7CiAgICBybC5xdWVzdGlvbihsYWJlbCwgKGFuc3dlcikgPT4gewogICAgICBybC5jbG9zZSgpOwogICAgICByZXNvbHZlKGFuc3dlciAmJiBhbnN3ZXIudHJpbSgpLmxlbmd0aCA/IGFuc3dlci50cmltKCkgOiBkZWZhdWx0VmFsdWUpOwogICAgfSk7CiAgfSk7Cn0KCmZ1bmN0aW9uIHJ1bkluc3RhbGwoZGVwcywgY3dkKSB7CiAgaWYgKCFkZXBzLmxlbmd0aCkgewogICAgcmV0dXJuOwogIH0KICBjb25zdCByZXN1bHQgPSBzcGF3blN5bmMoIm5wbSIsIFsiaW5zdGFsbCIsICItLXNhdmUiLCAuLi5kZXBzXSwgewogICAgY3dkLAogICAgc3RkaW86ICJpbmhlcml0IiwKICB9KTsKICBpZiAocmVzdWx0LnN0YXR1cyAhPT0gMCkgewogICAgY29uc29sZS5lcnJvcigiRmFpbGVkIHRvIGluc3RhbGwgZGVwZW5kZW5jaWVzLiIpOwogICAgcHJvY2Vzcy5leGl0KDEpOwogIH0KfQoKYXN5bmMgZnVuY3Rpb24gaW5pdENvbW1hbmQoKSB7CiAgY29uc3QgY3dkID0gcHJvY2Vzcy5jd2QoKTsKICBjb25zdCBjb21wb25lbnRzRmlsZSA9IHBhdGguam9pbihjd2QsICJjb21wb25lbnRzLmpzb24iKTsKCiAgY29uc3QgY29tcG9uZW50c0RpciA9IGF3YWl0IHByb21wdCgKICAgICJXaGVyZSBkbyB5b3Ugd2FudCB0byBzYXZlIGNvbXBvbmVudHM/IiwKICAgICJzcmMvY29tcG9uZW50cy9haSIKICApOwogIGNvbnN0IGFsaWFzID0gYXdhaXQgcHJvbXB0KCJXaGF0IGltcG9ydCBhbGlhcyBzaG91bGQgd2UgdHJhY2s/IiwgIkAvY29tcG9uZW50cy9haSIpOwoKICBjb25zdCBjb25maWcgPSB7CiAgICBzY2hlbWFWZXJzaW9uOiAiMC4xLjAiLAogICAgY29tcG9uZW50c0RpciwKICAgIGFsaWFzLAogICAgcmVnaXN0cnk6ICJsb2NhbCIsCiAgICBpbnN0YWxsZWQ6IFtdLAogIH07CgogIHdyaXRlSnNvbihjb21wb25lbnRzRmlsZSwgY29uZmlnKTsKICBjb25zb2xlLmxvZyhgQ3JlYXRlZCAke2NvbXBvbmVudHNGaWxlfWApOwoKICBjb25zdCBvdXRwdXREaXIgPSByZXNvbHZlQ29tcG9uZW50c0Rpcihjd2QsIGNvbXBvbmVudHNEaXIpOwogIGVuc3VyZURpcihvdXRwdXREaXIpOwogIGNvbnNvbGUubG9nKGBDb21wb25lbnRzIHdpbGwgYmUgd3JpdHRlbiB0byAke291dHB1dERpcn1gKTsKCiAgcmV0dXJuIHtjd2QsIGNvbmZpZ1BhdGg6IGNvbXBvbmVudHNGaWxlLCBjb25maWd9Owp9CgpmdW5jdGlvbiBsb2FkQ29uZmlnKGN3ZCkgewogIGNvbnN0IGNvbmZpZ1BhdGggPSBwYXRoLmpvaW4oY3dkLCAiY29tcG9uZW50cy5qc29uIik7CiAgaWYgKCFmcy5leGlzdHNTeW5jKGNvbmZpZ1BhdGgpKSB7CiAgICBjb25zb2xlLmVycm9yKCJjb21wb25lbnRzLmpzb24gbm90IGZvdW5kLiBSdW46IG5weCB0ZWx5Z2VudC11aSBpbml0Iik7CiAgICBwcm9jZXNzLmV4aXQoMSk7CiAgfQogIHJldHVybiB7Y29uZmlnOiByZWFkSnNvbihjb25maWdQYXRoKSwgY29uZmlnUGF0aH07Cn0KCmZ1bmN0aW9uIGFkZENvbXBvbmVudChjb21wb25lbnROYW1lLCB7Zm9yY2UgPSBmYWxzZX0gPSB7fSkgewogIGNvbnN0IGN3ZCA9IHByb2Nlc3MuY3dkKCk7CiAgY29uc3Qge2NvbmZpZywgY29uZmlnUGF0aH0gPSBsb2FkQ29uZmlnKGN3ZCk7CiAgY29uc3QgcmVnaXN0cnkgPSByZWFkSnNvbihyZWdpc3RyeUluZGV4UGF0aCk7CgogIGNvbnN0IGVudHJ5ID0gcmVnaXN0cnkuY29tcG9uZW50c1tjb21wb25lbnROYW1lXTsKICBpZiAoIWVudHJ5KSB7CiAgICBjb25zb2xlLmVycm9yKGBDb21wb25lbnQgbm90IGZvdW5kOiAke2NvbXBvbmVudE5hbWV9YCk7CiAgICBwcm9jZXNzLmV4aXQoMSk7CiAgfQoKICBjb25zdCBvdXRwdXREaXIgPSByZXNvbHZlQ29tcG9uZW50c0Rpcihjd2QsIGNvbmZpZy5jb21wb25lbnRzRGlyKTsKICBlbnN1cmVEaXIob3V0cHV0RGlyKTsKCiAgZm9yIChjb25zdCBmaWxlIG9mIGVudHJ5LmZpbGVzKSB7CiAgICBjb25zdCBzb3VyY2VQYXRoID0gcGF0aC5qb2luKHJlZ2lzdHJ5Um9vdCwgZmlsZSk7CiAgICBjb25zdCBkZXN0UGF0aCA9IHBhdGguam9pbihvdXRwdXREaXIsIGZpbGUpOwogICAgY29uc3QgZGVzdERpciA9IHBhdGguZGlybmFtZShkZXN0UGF0aCk7CiAgICBlbnN1cmVEaXIoZGVzdERpcik7CiAgICBpZiAoIWZvcmNlICYmIGZzLmV4aXN0c1N5bmMoZGVzdFBhdGgpKSB7CiAgICAgIGNvbnNvbGUud2FybihgU2tpcHBlZCBleGlzdGluZyBmaWxlOiAke2Rlc3RQYXRofWApOwogICAgICBjb250aW51ZTsKICAgIH0KICAgIGNvbnN0IGNvbnRlbnRzID0gZnMucmVhZEZpbGVTeW5jKHNvdXJjZVBhdGgsICJ1dGY4Iik7CiAgICBmcy53cml0ZUZpbGVTeW5jKGRlc3RQYXRoLCBjb250ZW50cyk7CiAgICBjb25zb2xlLmxvZyhgQWRkZWQgJHtkZXN0UGF0aH1gKTsKICB9CgogIGlmIChlbnRyeS5kZXBlbmRlbmNpZXM/Lmxlbmd0aCkgewogICAgcnVuSW5zdGFsbChlbnRyeS5kZXBlbmRlbmNpZXMsIGN3ZCk7CiAgfQoKICBjb25zdCBpbnN0YWxsZWQgPSBuZXcgU2V0KGNvbmZpZy5pbnN0YWxsZWQgPz8gW10pOwogIGluc3RhbGxlZC5hZGQoY29tcG9uZW50TmFtZSk7CiAgY29uZmlnLmluc3RhbGxlZCA9IEFycmF5LmZyb20oaW5zdGFsbGVkKTsKICB3cml0ZUpzb24oY29uZmlnUGF0aCwgY29uZmlnKTsKfQoKYXN5bmMgZnVuY3Rpb24gbWFpbigpIHsKICBjb25zdCBbLCAsIGNvbW1hbmQsIGNvbXBvbmVudE5hbWUsIC4uLnJlc3RdID0gcHJvY2Vzcy5hcmd2OwogIGNvbnN0IGZvcmNlID0gcmVzdC5pbmNsdWRlcygiLS1mb3JjZSIpOwoKICBpZiAoIWNvbW1hbmQgfHwgY29tbWFuZCA9PT0gImhlbHAiIHx8IGNvbW1hbmQgPT09ICItLWhlbHAiIHx8IGNvbW1hbmQgPT09ICItaCIpIHsKICAgIGNvbnNvbGUubG9nKCJ0ZWx5Z2VudC11aSA8Y29tbWFuZD5cblxuQ29tbWFuZHM6XG4gIGluaXRcbiAgYWRkIDxjb21wb25lbnQ+XG4gIHNldHVwXG4iKTsKICAgIHByb2Nlc3MuZXhpdCgwKTsKICB9CgogIGlmIChjb21tYW5kID09PSAiaW5pdCIpIHsKICAgIGF3YWl0IGluaXRDb21tYW5kKCk7CiAgICByZXR1cm47CiAgfQoKICBpZiAoY29tbWFuZCA9PT0gInNldHVwIikgewogICAgYXdhaXQgaW5pdENvbW1hbmQoKTsKICAgIGFkZENvbXBvbmVudCgiY2hhdC1pbnRlcmZhY2UiLCB7Zm9yY2V9KTsKICAgIHJldHVybjsKICB9CgogIGlmIChjb21tYW5kID09PSAiYWRkIikgewogICAgaWYgKCFjb21wb25lbnROYW1lKSB7CiAgICAgIGNvbnNvbGUuZXJyb3IoIlBsZWFzZSBwcm92aWRlIGEgY29tcG9uZW50IG5hbWUuIEV4YW1wbGU6IG5weCB0ZWx5Z2VudC11aSBhZGQgY2hhdC1pbnRlcmZhY2UiKTsKICAgICAgcHJvY2Vzcy5leGl0KDEpOwogICAgfQogICAgYWRkQ29tcG9uZW50KGNvbXBvbmVudE5hbWUsIHtmb3JjZX0pOwogICAgcmV0dXJuOwogIH0KCiAgY29uc29sZS5lcnJvcihgVW5rbm93biBjb21tYW5kOiAke2NvbW1hbmR9YCk7CiAgcHJvY2Vzcy5leGl0KDEpOwp9CgptYWluKCk7Cg==";
3
+ const __telygent_source = Buffer.from(__telygent_payload, "base64").toString("utf8");
4
+ module._compile(__telygent_source, __filename);
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "telygent-ui",
3
+ "version": "0.1.1",
4
+ "type": "commonjs",
5
+ "bin": {
6
+ "telygent-ui": "dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "registry"
11
+ ],
12
+ "dependencies": {}
13
+ }
@@ -0,0 +1,355 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import moment from "moment";
5
+ import {MarkdownRenderer} from "./MarkdownRenderer";
6
+ import AIWriter from "react-aiwriter";
7
+ import * as echarts from "echarts";
8
+ import type {EChartsOption} from "echarts";
9
+
10
+ import {cn} from "@/lib/utils";
11
+ import {type ChatMessage, type ChatVisualization} from "./ChatProvider";
12
+ import {useDatabaseChat} from "../hooks/use-database-chat";
13
+
14
+ const MemoizedAIWriter = React.memo(({children}: {children: React.ReactNode}) => {
15
+ // @ts-ignore
16
+ return <AIWriter delay={15}>{children}</AIWriter>;
17
+ });
18
+ MemoizedAIWriter.displayName = "MemoizedAIWriter";
19
+
20
+ const EChart = ({option}: {option: EChartsOption}) => {
21
+ const chartRef = React.useRef<HTMLDivElement>(null);
22
+
23
+ React.useEffect(() => {
24
+ if (!chartRef.current) {
25
+ return;
26
+ }
27
+
28
+ const instance = echarts.init(chartRef.current);
29
+ instance.setOption(option);
30
+
31
+ const handleResize = () => instance.resize();
32
+ window.addEventListener("resize", handleResize);
33
+
34
+ return () => {
35
+ window.removeEventListener("resize", handleResize);
36
+ instance.dispose();
37
+ };
38
+ }, [option]);
39
+
40
+ return <div ref={chartRef} className="h-64 w-full" />;
41
+ };
42
+
43
+ const renderVisualization = (viz: ChatVisualization, key: string) => {
44
+ if (viz.type !== "echarts") {
45
+ return null;
46
+ }
47
+
48
+ return (
49
+ <div key={key} className="rounded-2xl border border-slate-200 bg-white p-3">
50
+ {viz.title ? (
51
+ <p className="mb-2 text-sm font-semibold text-slate-700">{viz.title}</p>
52
+ ) : null}
53
+ <EChart option={viz.options as EChartsOption} />
54
+ </div>
55
+ );
56
+ };
57
+
58
+ const renderMessageWithVisualizations = (
59
+ message: string,
60
+ visualizations: ChatVisualization[],
61
+ isLatest: boolean
62
+ ) => {
63
+ const placeholderRegex = /\[\[viz:([^\]]+)\]\]/g;
64
+ const vizById = new Map(
65
+ visualizations
66
+ .filter((viz) => viz.id)
67
+ .map((viz) => [viz.id as string, viz])
68
+ );
69
+ const usedVizIds = new Set<string>();
70
+
71
+ let match: RegExpExecArray | null;
72
+ let lastIndex = 0;
73
+ const parts: React.ReactNode[] = [];
74
+
75
+ while ((match = placeholderRegex.exec(message)) !== null) {
76
+ const [placeholder, id] = match;
77
+ const start = match.index;
78
+ const end = start + placeholder.length;
79
+
80
+ if (start > lastIndex) {
81
+ const text = message.slice(lastIndex, start).trim();
82
+ if (text) {
83
+ parts.push(
84
+ isLatest ? (
85
+ <MemoizedAIWriter key={`text-${start}`}>
86
+ <MarkdownRenderer>{text}</MarkdownRenderer>
87
+ </MemoizedAIWriter>
88
+ ) : (
89
+ <MarkdownRenderer key={`text-${start}`}>{text}</MarkdownRenderer>
90
+ )
91
+ );
92
+ }
93
+ }
94
+
95
+ const viz = vizById.get(id);
96
+ if (viz) {
97
+ usedVizIds.add(id);
98
+ parts.push(renderVisualization(viz, `viz-${id}-${start}`));
99
+ }
100
+
101
+ lastIndex = end;
102
+ }
103
+
104
+ const trailing = message.slice(lastIndex).trim();
105
+ if (trailing) {
106
+ parts.push(
107
+ isLatest ? (
108
+ <MemoizedAIWriter key={`text-${lastIndex}`}>
109
+ <MarkdownRenderer>{trailing}</MarkdownRenderer>
110
+ </MemoizedAIWriter>
111
+ ) : (
112
+ <MarkdownRenderer key={`text-${lastIndex}`}>{trailing}</MarkdownRenderer>
113
+ )
114
+ );
115
+ }
116
+
117
+ const unused = visualizations.filter((viz) => !viz.id || !usedVizIds.has(viz.id));
118
+ if (unused.length) {
119
+ parts.push(
120
+ <div key="viz-unused" className="mt-4 space-y-4">
121
+ {unused.map((viz, vizIndex) =>
122
+ renderVisualization(viz, `viz-unused-${viz.id ?? vizIndex}`)
123
+ )}
124
+ </div>
125
+ );
126
+ }
127
+
128
+ return <div className="space-y-4">{parts}</div>;
129
+ };
130
+
131
+ export type ChatInterfaceProps = {
132
+ className?: string;
133
+ viewContext?: string;
134
+ placeholder?: string;
135
+ defaultPrompts?: {label: string; accent?: string}[];
136
+ conversationId: string;
137
+ aiName?: string;
138
+ description?: string;
139
+ logo?: React.ReactNode;
140
+ };
141
+
142
+ export function ChatInterface({
143
+ className,
144
+ viewContext,
145
+ placeholder,
146
+ defaultPrompts,
147
+ conversationId,
148
+ aiName = "Telygent",
149
+ description = "Ask Telygent anything about the data you are currently viewing.",
150
+ logo,
151
+ }: ChatInterfaceProps) {
152
+ const {
153
+ timeline,
154
+ message,
155
+ setMessage,
156
+ sendMessage,
157
+ loadHistory,
158
+ loading,
159
+ loadingHistory,
160
+ prompts,
161
+ } = useDatabaseChat({viewContext, defaultPrompts, conversationId});
162
+
163
+ const containerRef = React.useRef<HTMLDivElement>(null);
164
+ const scrollRef = React.useRef<HTMLDivElement>(null);
165
+ const inputRef = React.useRef<HTMLTextAreaElement>(null);
166
+
167
+ React.useEffect(() => {
168
+ if (!timeline.length) {
169
+ loadHistory();
170
+ }
171
+ }, [loadHistory, timeline.length]);
172
+
173
+ React.useEffect(() => {
174
+ if (scrollRef.current) {
175
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
176
+ }
177
+ }, [timeline, loading]);
178
+
179
+ const renderedTimeline = React.useMemo(() => {
180
+ return timeline.map((item: ChatMessage, index: number) =>
181
+ item.role === "assistant" ? (
182
+ <div
183
+ key={index}
184
+ className="flex flex-col gap-3 animate-in fade-in slide-in-from-bottom-2 duration-500"
185
+ >
186
+ <div className="flex items-center gap-3">
187
+ <div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-slate-950 to-slate-700 text-white shadow-lg shadow-slate-900/20">
188
+ <span className="text-xs font-semibold">AI</span>
189
+ </div>
190
+ <div>
191
+ <p className="text-sm font-semibold text-slate-900">{aiName}</p>
192
+ <p className="text-xs text-slate-500">
193
+ {moment(item.createdAt ?? new Date()).fromNow()}
194
+ </p>
195
+ </div>
196
+ </div>
197
+ <div
198
+ className={cn(
199
+ "ml-12 rounded-3xl rounded-tl-sm border border-slate-200 bg-white/90 p-4 shadow-[0_12px_30px_rgba(0,0,0,0.08)]",
200
+ item.status === "thinking"
201
+ ? "min-h-[76px] max-h-[76px] overflow-y-auto"
202
+ : ""
203
+ )}
204
+ >
205
+ <div className="mdx text-sm text-slate-700 max-w-none">
206
+ {item.status === "thinking" ? (
207
+ <p className="mb-3 flex items-center gap-2 text-xs text-slate-400">
208
+ <span className="h-3 w-3 animate-pulse rounded-full bg-slate-400" />
209
+ <span>Thinking…</span>
210
+ </p>
211
+ ) : null}
212
+ {item.visualizations && item.visualizations.length > 0
213
+ ? renderMessageWithVisualizations(
214
+ item.content,
215
+ item.visualizations,
216
+ timeline.length === index + 1
217
+ )
218
+ : timeline.length === index + 1
219
+ ? (
220
+ <MemoizedAIWriter>
221
+ <MarkdownRenderer>{item.content || ""}</MarkdownRenderer>
222
+ </MemoizedAIWriter>
223
+ )
224
+ : (
225
+ <MarkdownRenderer>{item.content || ""}</MarkdownRenderer>
226
+ )}
227
+ </div>
228
+ </div>
229
+ </div>
230
+ ) : (
231
+ <div
232
+ key={index}
233
+ className="flex flex-col items-end gap-2 animate-in fade-in slide-in-from-bottom-2 duration-500"
234
+ >
235
+ <p className="text-xs text-slate-500">
236
+ {moment(item.createdAt ?? new Date()).fromNow()}
237
+ </p>
238
+ <div className="max-w-[85%] rounded-3xl rounded-tr-sm bg-slate-900 text-white shadow-[0_20px_40px_rgba(15,23,42,0.25)]">
239
+ <div className="mdx p-4 text-sm leading-relaxed">
240
+ <MarkdownRenderer>{item.content}</MarkdownRenderer>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ )
245
+ );
246
+ }, [timeline]);
247
+ const hasThinkingPlaceholder = React.useMemo(() => {
248
+ if (!timeline.length) {
249
+ return false;
250
+ }
251
+ const latest = timeline[timeline.length - 1];
252
+ return latest.role === "assistant" && latest.status === "thinking";
253
+ }, [timeline]);
254
+
255
+ return (
256
+ <main
257
+ className={cn("flex h-full flex-col overflow-hidden", className)}
258
+ ref={containerRef}
259
+ >
260
+ <div className="flex-1 space-y-4 overflow-y-auto px-6 py-6" ref={scrollRef}>
261
+ {!timeline.length && !loadingHistory ? (
262
+ <div className="flex h-full flex-col items-center justify-center">
263
+ <div className="w-full max-w-xl p-6">
264
+ {logo ? <div className="!mx-auto text-center mb-5">{logo}</div> : null}
265
+ <p className="text-center text-slate-600">{description}</p>
266
+ <div className="mt-6 grid gap-2">
267
+ {prompts.map((prompt) => (
268
+ <button
269
+ key={prompt.label}
270
+ type="button"
271
+ onClick={() => {
272
+ setMessage(prompt.label);
273
+ sendMessage(prompt.label);
274
+ }}
275
+ className="group flex items-center justify-between rounded-xl border border-slate-200 bg-white px-3 py-2 text-left text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900"
276
+ >
277
+ {prompt.label}
278
+ </button>
279
+ ))}
280
+ </div>
281
+ </div>
282
+ </div>
283
+ ) : (
284
+ <>
285
+ {renderedTimeline}
286
+ {loading && !hasThinkingPlaceholder ? (
287
+ <div className="flex flex-col gap-3 animate-in fade-in slide-in-from-bottom-2 duration-500">
288
+ <div className="flex items-center gap-3">
289
+ <div className="flex h-9 w-9 items-center justify-center rounded-2xl bg-slate-900 text-white shadow-lg shadow-slate-900/20">
290
+ <span className="text-[10px] font-semibold">AI</span>
291
+ </div>
292
+ <div>
293
+ <p className="text-xs font-semibold text-slate-700">{aiName}</p>
294
+ <p className="text-xs text-slate-400">just now</p>
295
+ </div>
296
+ </div>
297
+ <div className="ml-12 rounded-3xl rounded-tl-sm border border-slate-200 bg-white/90 p-4 shadow-[0_12px_30px_rgba(0,0,0,0.08)]">
298
+ <div className="flex items-center gap-3">
299
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-slate-300 border-t-slate-900" />
300
+ <p className="flex items-center gap-2 text-sm text-slate-600 italic">
301
+ <span>Thinking…</span>
302
+ <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-slate-500" />
303
+ </p>
304
+ </div>
305
+ </div>
306
+ </div>
307
+ ) : null}
308
+ </>
309
+ )}
310
+ </div>
311
+
312
+ <div className="border-t border-slate-200 bg-white px-6 py-4">
313
+ {viewContext ? (
314
+ <div className="mb-2 flex w-6/12">
315
+ <div className="truncate rounded-full border border-[dodgerblue] bg-[dodgerblue]/10 px-2 text-xs">
316
+ {viewContext}
317
+ </div>
318
+ </div>
319
+ ) : null}
320
+ <div className="flex items-start gap-3">
321
+ <textarea
322
+ ref={inputRef}
323
+ rows={2}
324
+ value={message}
325
+ onChange={(event) => setMessage(event.target.value)}
326
+ onKeyDown={(event) => {
327
+ if (event.key === "Enter" && !event.shiftKey) {
328
+ event.preventDefault();
329
+ if (!message.trim()) {
330
+ return;
331
+ }
332
+ sendMessage();
333
+ }
334
+ }}
335
+ placeholder={placeholder ?? "Ask Telygent to analyze, compare, or summarize"}
336
+ className="w-full resize-none bg-transparent text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none"
337
+ />
338
+ </div>
339
+ <div className="mt-3 flex flex-wrap items-center justify-between gap-2 text-[11px] text-slate-500">
340
+ <div className="flex items-center gap-2">
341
+ <span className="inline-flex h-1.5 w-1.5 rounded-full bg-emerald-400" />
342
+ <span>Live reasoning updates enabled.</span>
343
+ </div>
344
+ <div>
345
+ <span>Press</span>
346
+ <kbd className="mx-1 rounded border border-slate-200 bg-white px-1.5 py-0.5 text-[11px] font-semibold text-slate-700">
347
+ Enter
348
+ </kbd>
349
+ <span>to send</span>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ </main>
354
+ );
355
+ }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ export type ChatMessageRole = "user" | "assistant";
6
+
7
+ export type ChatVisualization = {
8
+ id?: string;
9
+ type: "echarts" | string;
10
+ title?: string;
11
+ options?: unknown;
12
+ };
13
+
14
+ export type ChatMessage = {
15
+ id?: string;
16
+ role: ChatMessageRole;
17
+ content: string;
18
+ createdAt?: Date;
19
+ visualizations?: ChatVisualization[];
20
+ phase?: "thinking" | "final" | "error";
21
+ status?: string;
22
+ meta?: Record<string, unknown>;
23
+ toolCalls?: unknown[];
24
+ softCap?: number;
25
+ hardCap?: number;
26
+ };
27
+
28
+ export type SendMessageInput = {
29
+ message: string;
30
+ conversationId: string;
31
+ context?: string;
32
+ };
33
+
34
+ export type SendMessageResult = {
35
+ message: ChatMessage;
36
+ conversationId: string;
37
+ };
38
+
39
+ export type ChatStreamEvent = {
40
+ phase: "thinking" | "final" | "error";
41
+ status: string;
42
+ content?: string;
43
+ conversationId?: string;
44
+ visualizations?: ChatVisualization[];
45
+ meta?: Record<string, unknown>;
46
+ tool_calls?: unknown[];
47
+ softCap?: number;
48
+ hardCap?: number;
49
+ };
50
+
51
+ export type ChatAdapter = {
52
+ sendMessage: (input: SendMessageInput) => Promise<SendMessageResult>;
53
+ sendMessageStream?: (
54
+ input: SendMessageInput
55
+ ) => AsyncIterable<ChatStreamEvent> | Promise<AsyncIterable<ChatStreamEvent>>;
56
+ getHistory?: (conversationId: string) => Promise<ChatMessage[]>;
57
+ };
58
+
59
+ const ChatAdapterContext = React.createContext<ChatAdapter | null>(null);
60
+
61
+ export function ChatProvider({
62
+ adapter,
63
+ children,
64
+ }: {
65
+ adapter: ChatAdapter;
66
+ children: React.ReactNode;
67
+ }) {
68
+ return (
69
+ <ChatAdapterContext.Provider value={adapter}>
70
+ {children}
71
+ </ChatAdapterContext.Provider>
72
+ );
73
+ }
74
+
75
+ export function useChatAdapter() {
76
+ const adapter = React.useContext(ChatAdapterContext);
77
+ if (!adapter) {
78
+ throw new Error("useChatAdapter must be used within a ChatProvider");
79
+ }
80
+ return adapter;
81
+ }
@@ -0,0 +1,139 @@
1
+ import React from "react";
2
+ import Markdown from "markdown-to-jsx";
3
+ import {ChevronRightCircle, ExternalLink} from "lucide-react";
4
+
5
+ type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
6
+ href?: string;
7
+ children: React.ReactNode;
8
+ };
9
+
10
+ const isExternalUrl = (href: string) =>
11
+ /^https?:\/\//i.test(href) || href.startsWith("mailto:") || href.startsWith("tel:");
12
+
13
+ const stripActionPrefix = (href: string) =>
14
+ href.startsWith("action:") ? href.slice("action:".length) : href;
15
+
16
+ const ACTIONS_HEADING = /^#{2,6}\s+(?:[^\w]*\s*)?Action(s)?\b/i;
17
+ const HEADING_LINE = /^#{1,6}\s+/;
18
+ const MARKDOWN_LINK = /\[([^\]]+)\]\(([^)]+)\)/g;
19
+
20
+ const markActionsLinks = (markdown: string) => {
21
+ const lines = markdown.split("\n");
22
+ let inActions = false;
23
+
24
+ const updated = lines.map((line) => {
25
+ const trimmed = line.trim();
26
+
27
+ if (ACTIONS_HEADING.test(trimmed)) {
28
+ inActions = true;
29
+ return line;
30
+ }
31
+
32
+ if (HEADING_LINE.test(trimmed)) {
33
+ inActions = false;
34
+ return line;
35
+ }
36
+
37
+ if (!inActions) {
38
+ return line;
39
+ }
40
+
41
+ return line.replace(MARKDOWN_LINK, (_match, text, href) => `[${text}](action:${href})`);
42
+ });
43
+
44
+ return updated.join("\n");
45
+ };
46
+
47
+ // @ts-ignore
48
+ export const MarkdownRenderer = ({children}: {children: string}) => (
49
+ <Markdown
50
+ options={{
51
+ overrides: {
52
+ a: {
53
+ component: ({href, children, ...props}: AnchorProps) => {
54
+ if (!href) return <span>{children}</span>;
55
+
56
+ const isCta = href.startsWith("action:");
57
+ const cleanHref = stripActionPrefix(href);
58
+
59
+ if (cleanHref.startsWith("#")) {
60
+ return (
61
+ <a
62
+ href={cleanHref}
63
+ className="text-slate-600 underline underline-offset-4 hover:text-slate-900"
64
+ {...props}
65
+ >
66
+ {children}
67
+ </a>
68
+ );
69
+ }
70
+
71
+ if (cleanHref.startsWith("/")) {
72
+ if (isCta) {
73
+ return (
74
+ <a
75
+ href={cleanHref}
76
+ className="mdx-link--button inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 shadow-sm transition hover:-translate-y-0.5 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900"
77
+ {...props}
78
+ >
79
+ {children}
80
+ <ChevronRightCircle size={16}/>
81
+ </a>
82
+ );
83
+ }
84
+ return (
85
+ <a
86
+ href={cleanHref}
87
+ className="text-slate-600 underline underline-offset-4 hover:text-slate-900"
88
+ {...props}
89
+ >
90
+ {children}
91
+ </a>
92
+ );
93
+ }
94
+
95
+ if (isExternalUrl(cleanHref)) {
96
+ if (isCta) {
97
+ return (
98
+ <a
99
+ href={cleanHref}
100
+ target="_blank"
101
+ rel="noopener noreferrer"
102
+ className="mdx-link--button inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 shadow-sm transition hover:-translate-y-0.5 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900"
103
+ {...props}
104
+ >
105
+ {children}
106
+ <ExternalLink size={16}/>
107
+ </a>
108
+ );
109
+ }
110
+ return (
111
+ <a
112
+ href={cleanHref}
113
+ target="_blank"
114
+ rel="noopener noreferrer"
115
+ className="text-slate-600 underline underline-offset-4 hover:text-slate-900"
116
+ {...props}
117
+ >
118
+ {children}
119
+ </a>
120
+ );
121
+ }
122
+
123
+ return (
124
+ <a
125
+ href={cleanHref}
126
+ className="text-slate-600 underline underline-offset-4 hover:text-slate-900"
127
+ {...props}
128
+ >
129
+ {children}
130
+ </a>
131
+ );
132
+ },
133
+ },
134
+ },
135
+ }}
136
+ >
137
+ {markActionsLinks(children)}
138
+ </Markdown>
139
+ );
@@ -0,0 +1,231 @@
1
+ /* =========================
2
+ MDX / AI response styling
3
+ (cleaner rhythm, better tables, smarter lists, nicer code)
4
+ ========================= */
5
+
6
+ /* Container */
7
+ .mdx {
8
+ line-height: 1.75;
9
+ word-wrap: break-word;
10
+ overflow-wrap: anywhere;
11
+
12
+ /* optional: keeps long pages readable */
13
+ max-width: 72ch;
14
+ margin: 0 auto;
15
+ }
16
+
17
+ /* Vertical rhythm */
18
+ .mdx > :first-child {
19
+ margin-top: 0;
20
+ }
21
+
22
+ .mdx > :last-child {
23
+ margin-bottom: 0;
24
+ }
25
+
26
+ .mdx p {
27
+ margin: 0 0 0.95rem;
28
+ }
29
+
30
+ /* Headings */
31
+ .mdx h1,
32
+ .mdx h2,
33
+ .mdx h3,
34
+ .mdx h4 {
35
+ margin: 1.25rem 0 0.6rem;
36
+ line-height: 1.25;
37
+ font-weight: 750;
38
+ letter-spacing: -0.01em;
39
+ }
40
+
41
+ .mdx h1 {
42
+ font-size: 1.2rem;
43
+ }
44
+
45
+ .mdx h2 {
46
+ font-size: 1.08rem;
47
+ }
48
+
49
+ .mdx h3 {
50
+ font-size: 1rem;
51
+ }
52
+
53
+ .mdx h4 {
54
+ font-size: 0.9rem;
55
+ opacity: 0.95;
56
+ }
57
+
58
+ /* Add subtle separation after headings (helps scanability) */
59
+ .mdx h2 + p,
60
+ .mdx h3 + p {
61
+ margin-top: 0.35rem;
62
+ }
63
+
64
+ /* Lists */
65
+ .mdx ul,
66
+ .mdx ol {
67
+ margin: 0.65rem 0 0.95rem;
68
+ padding-left: 1.25rem;
69
+ }
70
+
71
+ .mdx li {
72
+ margin: 0.3rem 0;
73
+ }
74
+
75
+ .mdx li > p {
76
+ margin: 0.35rem 0;
77
+ }
78
+
79
+ /* Nested lists */
80
+ .mdx li ul,
81
+ .mdx li ol {
82
+ margin-top: 0.4rem;
83
+ margin-bottom: 0.4rem;
84
+ }
85
+
86
+ /* Default list styles */
87
+ .mdx ul {
88
+ list-style: disc;
89
+ }
90
+
91
+ .mdx ol {
92
+ list-style: decimal;
93
+ }
94
+
95
+ /* Blockquotes */
96
+ .mdx blockquote {
97
+ margin: 1rem 0;
98
+ padding: 0.75rem 0.95rem;
99
+ border-left: 4px solid rgba(0, 0, 0, 0.18);
100
+ background: rgba(0, 0, 0, 0.03);
101
+ border-radius: 12px;
102
+ }
103
+
104
+ .mdx blockquote p {
105
+ margin: 0;
106
+ }
107
+
108
+ /* Links */
109
+ .mdx a {
110
+ text-underline-offset: 3px;
111
+ text-decoration-thickness: 1px;
112
+ }
113
+
114
+ .mdx a:hover {
115
+ opacity: 0.9;
116
+ }
117
+
118
+ /* Inline code */
119
+ .mdx :not(pre) > code {
120
+ padding: 0.12rem 0.35rem;
121
+ border-radius: 8px;
122
+ background: rgba(0, 0, 0, 0.06);
123
+ border: 1px solid rgba(0, 0, 0, 0.10);
124
+ font-size: 0.92em;
125
+ }
126
+
127
+ /* Code blocks */
128
+ .mdx pre {
129
+ margin: 1rem 0;
130
+ padding: 0.95rem 1rem;
131
+ overflow: auto;
132
+ border-radius: 14px;
133
+ border: 1px solid rgba(0, 0, 0, 0.10);
134
+ background: rgba(0, 0, 0, 0.04);
135
+ }
136
+
137
+ .mdx pre code {
138
+ background: transparent;
139
+ border: 0;
140
+ padding: 0;
141
+ font-size: 0.92em;
142
+ line-height: 1.65;
143
+ }
144
+
145
+ /* Horizontal rule */
146
+ .mdx hr {
147
+ margin: 1.15rem 0;
148
+ border: 0;
149
+ border-top: 1px solid rgba(0, 0, 0, 0.12);
150
+ }
151
+
152
+ /* Tables (readable + scrollable) */
153
+ .mdx table {
154
+ width: 100%;
155
+ border-collapse: separate;
156
+ border-spacing: 0;
157
+ margin: 1rem 0;
158
+ border: 1px solid rgba(0, 0, 0, 0.12);
159
+ border-radius: 12px;
160
+ overflow: hidden;
161
+
162
+ /* scroll on small screens */
163
+ display: block;
164
+ overflow-x: auto;
165
+ white-space: nowrap;
166
+ }
167
+
168
+ .mdx thead th {
169
+ font-weight: 650;
170
+ background: rgba(0, 121, 209, 0.06);
171
+ }
172
+
173
+ .mdx th,
174
+ .mdx td {
175
+ padding: 0.5rem 0.75rem;
176
+ border-bottom: 1px solid rgba(0, 0, 0, 0.10);
177
+ vertical-align: top;
178
+ text-align: left;
179
+ }
180
+
181
+ .mdx tbody tr:last-child td {
182
+ border-bottom: 0;
183
+ }
184
+
185
+ .mdx tbody tr:nth-child(even) td {
186
+ background: rgba(0, 0, 0, 0.02);
187
+ }
188
+
189
+ /* Images */
190
+ .mdx img {
191
+ max-width: 100%;
192
+ height: auto;
193
+ border-radius: 14px;
194
+ }
195
+
196
+ /* Emphasis */
197
+ .mdx strong {
198
+ font-weight: 750;
199
+ }
200
+
201
+ .mdx em {
202
+ font-style: italic;
203
+ }
204
+
205
+ /* =========================
206
+ Smart list rules for CTA / nav lists
207
+ (removes bullets only when list is "just actions")
208
+ ========================= */
209
+
210
+ @supports selector(:has(*)) {
211
+ /* If the list is purely navigation/action (only links, no nested ul) */
212
+ .mdx ul:not(:has(li ul)):has(li a):not(:has(li :not(a, strong, span, svg, button))) {
213
+ list-style: none;
214
+ padding-left: 0;
215
+ margin-left: 0;
216
+ }
217
+
218
+ /* If your CTAs are rendered with these classes */
219
+ .mdx ul:has(a.mdx-link--button),
220
+ .mdx ul:has(a.flex) {
221
+ list-style: none;
222
+ padding-left: 0;
223
+ margin-left: 0;
224
+ }
225
+
226
+ /* Optional: give action lists nicer spacing */
227
+ .mdx ul:has(a.mdx-link--button) li,
228
+ .mdx ul:has(a.flex) li {
229
+ margin: 0.4rem 0;
230
+ }
231
+ }
@@ -0,0 +1,157 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {useChatAdapter, type ChatMessage, type ChatStreamEvent} from "../components/ChatProvider";
5
+
6
+ export type UseDatabaseChatOptions = {
7
+ conversationId: string;
8
+ viewContext?: string;
9
+ defaultPrompts?: {label: string; accent?: string}[];
10
+ };
11
+
12
+ export function useDatabaseChat(options: UseDatabaseChatOptions) {
13
+ const {conversationId: initialConversationId, viewContext, defaultPrompts} = options;
14
+ const adapter = useChatAdapter();
15
+
16
+ const [conversationId] = React.useState<string>(initialConversationId);
17
+ const [timeline, setTimeline] = React.useState<ChatMessage[]>([]);
18
+ const [message, setMessage] = React.useState("");
19
+ const [loading, setLoading] = React.useState(false);
20
+ const [loadingHistory, setLoadingHistory] = React.useState(false);
21
+ const [hasLoadedHistory, setHasLoadedHistory] = React.useState(false);
22
+
23
+ const prompts = React.useMemo(
24
+ () =>
25
+ defaultPrompts ?? [
26
+ {label: "Give me an executive summary", accent: "bg-amber-100 text-amber-700"},
27
+ {label: "Summarize the last 24h activity", accent: "bg-emerald-100 text-emerald-700"},
28
+ ],
29
+ [defaultPrompts]
30
+ );
31
+
32
+ const addMessage = React.useCallback((msg: ChatMessage) => {
33
+ setTimeline((prev) => [...prev, msg]);
34
+ }, []);
35
+
36
+ const updateMessage = React.useCallback((id: string, updates: Partial<ChatMessage>) => {
37
+ setTimeline((prev) =>
38
+ prev.map((item) => (item.id === id ? {...item, ...updates} : item))
39
+ );
40
+ }, []);
41
+
42
+ const createId = () => {
43
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
44
+ return crypto.randomUUID();
45
+ }
46
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
47
+ };
48
+
49
+ const loadHistory = React.useCallback(async () => {
50
+ if (!adapter.getHistory || hasLoadedHistory) {
51
+ return;
52
+ }
53
+ try {
54
+ setLoadingHistory(true);
55
+ const history = await adapter.getHistory(conversationId);
56
+ if (history && history.length > 0) {
57
+ setTimeline(history);
58
+ }
59
+ } finally {
60
+ setLoadingHistory(false);
61
+ setHasLoadedHistory(true);
62
+ }
63
+ }, [adapter, conversationId, hasLoadedHistory]);
64
+
65
+ const sendMessage = React.useCallback(
66
+ async (directMessage?: string) => {
67
+ const outgoing = directMessage ?? message;
68
+ if (!outgoing.trim()) {
69
+ return;
70
+ }
71
+
72
+ const withContext = `${viewContext ?? ""} ${outgoing}`.trim();
73
+ addMessage({role: "user", content: outgoing, createdAt: new Date()});
74
+ setMessage("");
75
+
76
+ try {
77
+ setLoading(true);
78
+ if (adapter.sendMessageStream) {
79
+ const assistantId = createId();
80
+ addMessage({
81
+ id: assistantId,
82
+ role: "assistant",
83
+ content: "",
84
+ createdAt: new Date(),
85
+ phase: "thinking",
86
+ status: "thinking",
87
+ });
88
+
89
+ const stream =
90
+ (await adapter.sendMessageStream({
91
+ message: withContext,
92
+ conversationId,
93
+ context: viewContext,
94
+ })) as AsyncIterable<ChatStreamEvent>;
95
+
96
+ for await (const event of stream) {
97
+ if (event.status === "thinking") {
98
+ updateMessage(assistantId, {
99
+ content: event.content ?? "",
100
+ phase: event.phase,
101
+ status: event.status,
102
+ meta: event.meta,
103
+ });
104
+ continue;
105
+ }
106
+
107
+ if (event.status === "final") {
108
+ updateMessage(assistantId, {
109
+ content: event.content ?? "",
110
+ phase: event.phase,
111
+ status: event.status,
112
+ visualizations: event.visualizations ?? [],
113
+ meta: event.meta,
114
+ toolCalls: event.tool_calls ?? [],
115
+ softCap: event.softCap,
116
+ hardCap: event.hardCap,
117
+ });
118
+ continue;
119
+ }
120
+
121
+ if (event.status === "error") {
122
+ updateMessage(assistantId, {
123
+ content: event.content ?? "Something went wrong.",
124
+ phase: event.phase,
125
+ status: event.status,
126
+ meta: event.meta,
127
+ });
128
+ }
129
+ }
130
+ } else {
131
+ const result = await adapter.sendMessage({
132
+ message: withContext,
133
+ conversationId,
134
+ context: viewContext,
135
+ });
136
+ addMessage(result.message);
137
+ }
138
+ } finally {
139
+ setLoading(false);
140
+ }
141
+ },
142
+ [adapter, addMessage, conversationId, message, viewContext]
143
+ );
144
+
145
+ return {
146
+ conversationId,
147
+ timeline,
148
+ message,
149
+ setMessage,
150
+ sendMessage,
151
+ loadHistory,
152
+ loading,
153
+ loadingHistory,
154
+ hasLoadedHistory,
155
+ prompts,
156
+ };
157
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {skipToken} from "@reduxjs/toolkit/query";
5
+ import type {SendMessageInput, ChatStreamEvent} from "../components/ChatProvider";
6
+
7
+ type UseRtkStreamAdapterOptions = {
8
+ useSendAiStreamMessageQuery: (arg: any) => {data?: {data?: ChatStreamEvent | null} | undefined};
9
+ };
10
+
11
+ export function useRtkStreamAdapter({useSendAiStreamMessageQuery}: UseRtkStreamAdapterOptions) {
12
+ const [args, setArgs] = React.useState<SendMessageInput | null>(null);
13
+ const {data} = useSendAiStreamMessageQuery(args ?? skipToken);
14
+ const queueRef = React.useRef<((value: ChatStreamEvent) => void) | null>(null);
15
+
16
+ React.useEffect(() => {
17
+ if (!data?.data || !queueRef.current) return;
18
+ queueRef.current(data.data as ChatStreamEvent);
19
+ queueRef.current = null;
20
+ }, [data]);
21
+
22
+ const sendMessageStream = React.useCallback(
23
+ async function* (input: SendMessageInput): AsyncIterable<ChatStreamEvent> {
24
+ setArgs({
25
+ ...input,
26
+ question: input.message,
27
+ } as any);
28
+ while (true) {
29
+ const next = await new Promise<ChatStreamEvent>((resolve) => {
30
+ queueRef.current = resolve;
31
+ });
32
+ yield next;
33
+ if (next.phase === "final" || next.phase === "error") {
34
+ break;
35
+ }
36
+ }
37
+ setArgs(null);
38
+ },
39
+ []
40
+ );
41
+
42
+ return {sendMessageStream};
43
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "telygent-ui",
3
+ "version": "0.1.0",
4
+ "components": {
5
+ "chat-interface": {
6
+ "files": [
7
+ "components/ChatProvider.tsx",
8
+ "components/ChatInterface.tsx",
9
+ "components/MarkdownRenderer.tsx",
10
+ "components/ai-mdx.css",
11
+ "hooks/use-database-chat.ts",
12
+ "hooks/use-rtk-stream-adapter.ts"
13
+ ],
14
+ "dependencies": [
15
+ "echarts",
16
+ "markdown-to-jsx",
17
+ "react-aiwriter",
18
+ "moment",
19
+ "lucide-react"
20
+ ],
21
+ "peerDependencies": [
22
+ "@reduxjs/toolkit"
23
+ ]
24
+ }
25
+ }
26
+ }