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.2",
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 renderMessageWithVisualizations = (
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
- const viz = vizById.get(id);
96
- if (viz) {
97
- usedVizIds.add(id);
98
- parts.push(renderVisualization(viz, `viz-${id}-${start}`));
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
- if (unused.length) {
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] max-h-[76px] overflow-y-auto"
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
- ? renderMessageWithVisualizations(
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
- setTimeline(history);
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.phase === "final" || next.phase === "error") {
33
+ if (next.status === "final" || next.status === "error") {
34
34
  break;
35
35
  }
36
36
  }