telygent-ui 0.1.2 → 0.1.4
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/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# telygent-ui
|
|
2
|
+
|
|
3
|
+
A shadcn-style component registry + CLI for Telygent UI.
|
|
4
|
+
|
|
5
|
+
## CLI workflow
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx telygent-ui setup
|
|
9
|
+
npx telygent-ui add chat-interface
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Adapter pattern
|
|
13
|
+
|
|
14
|
+
Wrap your app with `ChatProvider` and supply an adapter using your data library of choice.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import {ChatProvider, type SendMessageInput, type SendMessageResult, type ChatMessage} from "@/components/ai/ChatProvider";
|
|
18
|
+
import {ChatInterface} from "@/components/ai/ChatInterface";
|
|
19
|
+
|
|
20
|
+
const adapter = {
|
|
21
|
+
async sendMessage({message, conversationId}: SendMessageInput): Promise<SendMessageResult> {
|
|
22
|
+
// Use React Query, Axios, RTK Query, etc.
|
|
23
|
+
const response = await api.send({message, conversationId});
|
|
24
|
+
return {
|
|
25
|
+
conversationId: response.conversationId,
|
|
26
|
+
message: {
|
|
27
|
+
role: "assistant",
|
|
28
|
+
content: response.content,
|
|
29
|
+
createdAt: new Date(),
|
|
30
|
+
visualizations: response.visualizations,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async getHistory(conversationId: string): Promise<ChatMessage[]> {
|
|
35
|
+
const history = await api.history(conversationId);
|
|
36
|
+
return history.messages.map((item) => ({
|
|
37
|
+
role: item.role === "assistant" ? "assistant" : "user",
|
|
38
|
+
content: item.content,
|
|
39
|
+
createdAt: new Date(item.timestamp),
|
|
40
|
+
visualizations: item.visualizations ?? [],
|
|
41
|
+
summaryCards: item.summaryCards ?? [],
|
|
42
|
+
}));
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default function Page() {
|
|
47
|
+
const conversationId = "your-conversation-id";
|
|
48
|
+
return (
|
|
49
|
+
<ChatProvider adapter={adapter}>
|
|
50
|
+
<ChatInterface conversationId={conversationId} aiName="Telygent" />
|
|
51
|
+
</ChatProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## SSE adapter example
|
|
57
|
+
|
|
58
|
+
If your backend streams SSE updates, implement `sendMessageStream`:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import {ChatProvider, type SendMessageInput, type ChatStreamEvent} from "@/components/ai/ChatProvider";
|
|
62
|
+
import {ChatInterface} from "@/components/ai/ChatInterface";
|
|
63
|
+
|
|
64
|
+
async function* streamChat(input: SendMessageInput): AsyncIterable<ChatStreamEvent> {
|
|
65
|
+
const response = await fetch("/api/chat/stream", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {"Content-Type": "application/json"},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
question: input.message,
|
|
70
|
+
conversationId: input.conversationId,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!response.body) {
|
|
75
|
+
throw new Error("No stream body");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const reader = response.body.getReader();
|
|
79
|
+
const decoder = new TextDecoder();
|
|
80
|
+
let buffer = "";
|
|
81
|
+
|
|
82
|
+
const flush = async function* () {
|
|
83
|
+
const lines = buffer.split("\n");
|
|
84
|
+
buffer = lines.pop() ?? "";
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const trimmed = line.trim();
|
|
87
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
88
|
+
const payload = trimmed.replace(/^data:\s*/, "");
|
|
89
|
+
if (!payload) continue;
|
|
90
|
+
yield JSON.parse(payload) as ChatStreamEvent;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
while (true) {
|
|
95
|
+
const {value, done} = await reader.read();
|
|
96
|
+
if (done) break;
|
|
97
|
+
buffer += decoder.decode(value, {stream: true});
|
|
98
|
+
for await (const event of flush()) {
|
|
99
|
+
yield event;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const adapter = {
|
|
105
|
+
async sendMessage() {
|
|
106
|
+
throw new Error("sendMessage not used when streaming");
|
|
107
|
+
},
|
|
108
|
+
sendMessageStream: streamChat,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default function Page() {
|
|
112
|
+
return (
|
|
113
|
+
<ChatProvider adapter={adapter}>
|
|
114
|
+
<ChatInterface conversationId="conv_123" />
|
|
115
|
+
</ChatProvider>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## RTK Query SSE example
|
|
121
|
+
|
|
122
|
+
If you already have an RTK Query SSE endpoint, you can bridge it with the helper hook:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import {ChatProvider} from "@/components/ai/ChatProvider";
|
|
126
|
+
import {ChatInterface} from "@/components/ai/ChatInterface";
|
|
127
|
+
import {useRtkStreamAdapter} from "@/components/ai/hooks/use-rtk-stream-adapter";
|
|
128
|
+
import {useSendAiStreamMessageQuery} from "@/redux/api/generalApi";
|
|
129
|
+
|
|
130
|
+
export default function Page({conversationId}: {conversationId: string}) {
|
|
131
|
+
const {sendMessageStream} = useRtkStreamAdapter({useSendAiStreamMessageQuery});
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<ChatProvider adapter={{sendMessageStream} as any}>
|
|
135
|
+
<ChatInterface conversationId={conversationId} />
|
|
136
|
+
</ChatProvider>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Styles
|
|
142
|
+
|
|
143
|
+
Import the MDX styles once (for example in your global CSS):
|
|
144
|
+
|
|
145
|
+
```css
|
|
146
|
+
@import "@/components/ai/ai-mdx.css";
|
|
147
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "telygent-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Telygent UI CLI",
|
|
4
5
|
"type": "commonjs",
|
|
6
|
+
"main": "dist/index.js",
|
|
5
7
|
"bin": {
|
|
6
8
|
"telygent-ui": "dist/index.js"
|
|
7
9
|
},
|
|
@@ -9,5 +11,14 @@
|
|
|
9
11
|
"dist",
|
|
10
12
|
"registry"
|
|
11
13
|
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"telygent",
|
|
16
|
+
"ui",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"license": "UNLICENSED",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
12
23
|
"dependencies": {}
|
|
13
24
|
}
|
|
@@ -8,7 +8,7 @@ import * as echarts from "echarts";
|
|
|
8
8
|
import type {EChartsOption} from "echarts";
|
|
9
9
|
|
|
10
10
|
import {cn} from "@/lib/utils";
|
|
11
|
-
import {type ChatMessage, type ChatVisualization} from "./ChatProvider";
|
|
11
|
+
import {type ChatMessage, type ChatSummaryCard, type ChatVisualization} from "./ChatProvider";
|
|
12
12
|
import {useDatabaseChat} from "../hooks/use-database-chat";
|
|
13
13
|
|
|
14
14
|
const MemoizedAIWriter = React.memo(({children}: {children: React.ReactNode}) => {
|
|
@@ -55,25 +55,59 @@ const renderVisualization = (viz: ChatVisualization, key: string) => {
|
|
|
55
55
|
);
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
const
|
|
58
|
+
const renderSummaryCard = (card: ChatSummaryCard, key: string) => {
|
|
59
|
+
const trendTone =
|
|
60
|
+
card.trend === "up"
|
|
61
|
+
? "text-emerald-600"
|
|
62
|
+
: card.trend === "down"
|
|
63
|
+
? "text-rose-600"
|
|
64
|
+
: "text-slate-600";
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
key={key}
|
|
69
|
+
className="rounded-2xl border border-slate-200 bg-white/95 p-4 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
|
|
70
|
+
>
|
|
71
|
+
<p className="text-xs uppercase tracking-wide text-slate-400">{card.title}</p>
|
|
72
|
+
<div className="mt-2 flex items-end justify-between gap-3">
|
|
73
|
+
<p className="text-2xl font-semibold text-slate-900">{card.value}</p>
|
|
74
|
+
{card.delta ? (
|
|
75
|
+
<p className={cn("text-xs font-semibold", trendTone)}>{card.delta}</p>
|
|
76
|
+
) : null}
|
|
77
|
+
</div>
|
|
78
|
+
{card.subtitle ? (
|
|
79
|
+
<p className="mt-1 text-sm text-slate-500">{card.subtitle}</p>
|
|
80
|
+
) : null}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const renderMessageWithRichContent = (
|
|
59
86
|
message: string,
|
|
60
87
|
visualizations: ChatVisualization[],
|
|
88
|
+
summaryCards: ChatSummaryCard[],
|
|
61
89
|
isLatest: boolean
|
|
62
90
|
) => {
|
|
63
|
-
const placeholderRegex = /\[\[viz:([^\]]+)\]\]/g;
|
|
91
|
+
const placeholderRegex = /\[\[(viz|card):([^\]]+)\]\]/g;
|
|
64
92
|
const vizById = new Map(
|
|
65
93
|
visualizations
|
|
66
94
|
.filter((viz) => viz.id)
|
|
67
95
|
.map((viz) => [viz.id as string, viz])
|
|
68
96
|
);
|
|
97
|
+
const cardsById = new Map(
|
|
98
|
+
summaryCards
|
|
99
|
+
.filter((card) => card.id)
|
|
100
|
+
.map((card) => [card.id as string, card])
|
|
101
|
+
);
|
|
69
102
|
const usedVizIds = new Set<string>();
|
|
103
|
+
const usedCardIds = new Set<string>();
|
|
70
104
|
|
|
71
105
|
let match: RegExpExecArray | null;
|
|
72
106
|
let lastIndex = 0;
|
|
73
107
|
const parts: React.ReactNode[] = [];
|
|
74
108
|
|
|
75
109
|
while ((match = placeholderRegex.exec(message)) !== null) {
|
|
76
|
-
const [placeholder, id] = match;
|
|
110
|
+
const [placeholder, type, id] = match;
|
|
77
111
|
const start = match.index;
|
|
78
112
|
const end = start + placeholder.length;
|
|
79
113
|
|
|
@@ -92,10 +126,20 @@ const renderMessageWithVisualizations = (
|
|
|
92
126
|
}
|
|
93
127
|
}
|
|
94
128
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
if (type === "viz") {
|
|
130
|
+
const viz = vizById.get(id);
|
|
131
|
+
if (viz) {
|
|
132
|
+
usedVizIds.add(id);
|
|
133
|
+
parts.push(renderVisualization(viz, `viz-${id}-${start}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (type === "card") {
|
|
138
|
+
const card = cardsById.get(id);
|
|
139
|
+
if (card) {
|
|
140
|
+
usedCardIds.add(id);
|
|
141
|
+
parts.push(renderSummaryCard(card, `card-${id}-${start}`));
|
|
142
|
+
}
|
|
99
143
|
}
|
|
100
144
|
|
|
101
145
|
lastIndex = end;
|
|
@@ -115,12 +159,16 @@ const renderMessageWithVisualizations = (
|
|
|
115
159
|
}
|
|
116
160
|
|
|
117
161
|
const unused = visualizations.filter((viz) => !viz.id || !usedVizIds.has(viz.id));
|
|
118
|
-
|
|
162
|
+
const unusedCards = summaryCards.filter((card) => !card.id || !usedCardIds.has(card.id));
|
|
163
|
+
if (unused.length || unusedCards.length) {
|
|
119
164
|
parts.push(
|
|
120
165
|
<div key="viz-unused" className="mt-4 space-y-4">
|
|
121
166
|
{unused.map((viz, vizIndex) =>
|
|
122
167
|
renderVisualization(viz, `viz-unused-${viz.id ?? vizIndex}`)
|
|
123
168
|
)}
|
|
169
|
+
{unusedCards.map((card, cardIndex) =>
|
|
170
|
+
renderSummaryCard(card, `card-unused-${card.id ?? cardIndex}`)
|
|
171
|
+
)}
|
|
124
172
|
</div>
|
|
125
173
|
);
|
|
126
174
|
}
|
|
@@ -198,7 +246,7 @@ export function ChatInterface({
|
|
|
198
246
|
className={cn(
|
|
199
247
|
"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
248
|
item.status === "thinking"
|
|
201
|
-
? "min-h-[76px]
|
|
249
|
+
? "min-h-[76px] h-auto overflow-y-auto"
|
|
202
250
|
: ""
|
|
203
251
|
)}
|
|
204
252
|
>
|
|
@@ -209,10 +257,12 @@ export function ChatInterface({
|
|
|
209
257
|
<span>Thinking…</span>
|
|
210
258
|
</p>
|
|
211
259
|
) : null}
|
|
212
|
-
{item.visualizations && item.visualizations.length > 0
|
|
213
|
-
|
|
260
|
+
{(item.visualizations && item.visualizations.length > 0) ||
|
|
261
|
+
(item.summaryCards && item.summaryCards.length > 0)
|
|
262
|
+
? renderMessageWithRichContent(
|
|
214
263
|
item.content,
|
|
215
|
-
item.visualizations,
|
|
264
|
+
item.visualizations ?? [],
|
|
265
|
+
item.summaryCards ?? [],
|
|
216
266
|
timeline.length === index + 1
|
|
217
267
|
)
|
|
218
268
|
: timeline.length === index + 1
|
|
@@ -11,12 +11,22 @@ export type ChatVisualization = {
|
|
|
11
11
|
options?: unknown;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
export type ChatSummaryCard = {
|
|
15
|
+
id?: string;
|
|
16
|
+
title: string;
|
|
17
|
+
value: string;
|
|
18
|
+
subtitle?: string;
|
|
19
|
+
trend?: "up" | "down" | string;
|
|
20
|
+
delta?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
14
23
|
export type ChatMessage = {
|
|
15
24
|
id?: string;
|
|
16
25
|
role: ChatMessageRole;
|
|
17
26
|
content: string;
|
|
18
27
|
createdAt?: Date;
|
|
19
28
|
visualizations?: ChatVisualization[];
|
|
29
|
+
summaryCards?: ChatSummaryCard[];
|
|
20
30
|
phase?: "thinking" | "final" | "error";
|
|
21
31
|
status?: string;
|
|
22
32
|
meta?: Record<string, unknown>;
|
|
@@ -42,6 +52,7 @@ export type ChatStreamEvent = {
|
|
|
42
52
|
content?: string;
|
|
43
53
|
conversationId?: string;
|
|
44
54
|
visualizations?: ChatVisualization[];
|
|
55
|
+
summaryCards?: ChatSummaryCard[];
|
|
45
56
|
meta?: Record<string, unknown>;
|
|
46
57
|
tool_calls?: unknown[];
|
|
47
58
|
softCap?: number;
|
|
@@ -54,7 +54,12 @@ export function useDatabaseChat(options: UseDatabaseChatOptions) {
|
|
|
54
54
|
setLoadingHistory(true);
|
|
55
55
|
const history = await adapter.getHistory(conversationId);
|
|
56
56
|
if (history && history.length > 0) {
|
|
57
|
-
|
|
57
|
+
const normalized = history.map((item) => ({
|
|
58
|
+
...item,
|
|
59
|
+
visualizations: item.visualizations ?? [],
|
|
60
|
+
summaryCards: (item as any).summaryCards ?? (item as any).summary_cards ?? [],
|
|
61
|
+
}));
|
|
62
|
+
setTimeline(normalized);
|
|
58
63
|
}
|
|
59
64
|
} finally {
|
|
60
65
|
setLoadingHistory(false);
|
|
@@ -110,6 +115,7 @@ export function useDatabaseChat(options: UseDatabaseChatOptions) {
|
|
|
110
115
|
phase: event.phase,
|
|
111
116
|
status: event.status,
|
|
112
117
|
visualizations: event.visualizations ?? [],
|
|
118
|
+
summaryCards: event.summaryCards ?? [],
|
|
113
119
|
meta: event.meta,
|
|
114
120
|
toolCalls: event.tool_calls ?? [],
|
|
115
121
|
softCap: event.softCap,
|
|
@@ -30,7 +30,7 @@ export function useRtkStreamAdapter({useSendAiStreamMessageQuery}: UseRtkStreamA
|
|
|
30
30
|
queueRef.current = resolve;
|
|
31
31
|
});
|
|
32
32
|
yield next;
|
|
33
|
-
if (next.
|
|
33
|
+
if (next.status === "final" || next.status === "error") {
|
|
34
34
|
break;
|
|
35
35
|
}
|
|
36
36
|
}
|