chat-nest-sdk 1.0.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/README.md +103 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +195 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# chat-nest-sdk
|
|
2
|
+
|
|
3
|
+
> Frontend SDK for Chat Nest providing a simple React hook to consume streaming AI APIs safely.
|
|
4
|
+
|
|
5
|
+
This package handles:
|
|
6
|
+
- Streaming response handling
|
|
7
|
+
- Cancellation propagation
|
|
8
|
+
- Intelligent retry behavior
|
|
9
|
+
- Error normalization
|
|
10
|
+
- Message state management
|
|
11
|
+
|
|
12
|
+
Designed for production usage in React applications.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ✨ Features
|
|
17
|
+
|
|
18
|
+
- Streaming token handling
|
|
19
|
+
- Abort-safe cancellation
|
|
20
|
+
- Retry only on network / server failures
|
|
21
|
+
- No retries on client or policy errors
|
|
22
|
+
- Message state management
|
|
23
|
+
- Lightweight and framework-friendly
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 📦 Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install chat-nest-sdk
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🚀 Usage
|
|
36
|
+
|
|
37
|
+
### Example
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
import { useAiChat } from "chat-nest-sdk";
|
|
41
|
+
|
|
42
|
+
function App() {
|
|
43
|
+
const chat = useAiChat({
|
|
44
|
+
endpoint: "http://localhost:3001/api/chat",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<div>
|
|
50
|
+
{chat.messages.map((m) => (
|
|
51
|
+
<div key={m.id}>
|
|
52
|
+
<strong>{m.role}</strong>: {m.content}
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<button onClick={() => chat.sendMessage("Hello!")}>
|
|
58
|
+
Send
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<button onClick={chat.cancel}>
|
|
62
|
+
Cancel
|
|
63
|
+
</button>
|
|
64
|
+
</>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🧠 API
|
|
72
|
+
|
|
73
|
+
### useAiChat(options)
|
|
74
|
+
|
|
75
|
+
| Field | Type | Description |
|
|
76
|
+
| -------- | ------ | -------------------- |
|
|
77
|
+
| endpoint | string | Backend API endpoint |
|
|
78
|
+
|
|
79
|
+
| Field | Description |
|
|
80
|
+
| ----------------- | ------------------------ |
|
|
81
|
+
| messages | Chat message list |
|
|
82
|
+
| sendMessage(text) | Sends user message |
|
|
83
|
+
| cancel() | Cancels active request |
|
|
84
|
+
| isStreaming | Whether stream is active |
|
|
85
|
+
| error | Last error |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## ⚠️ Important Notes
|
|
90
|
+
|
|
91
|
+
Only one active request is allowed at a time.
|
|
92
|
+
|
|
93
|
+
Cancel immediately stops streaming and billing.
|
|
94
|
+
|
|
95
|
+
4xx errors are never retried.
|
|
96
|
+
|
|
97
|
+
Network failures retry automatically.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 📄 License
|
|
102
|
+
|
|
103
|
+
ISC
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { UseAIChatOptions, UseAIChatReturn, AIClient, AiClientConfig, Message, StreamCallbacks } from 'chat-nest-core';
|
|
2
|
+
|
|
3
|
+
declare function useAiChat(options: UseAIChatOptions): UseAIChatReturn;
|
|
4
|
+
|
|
5
|
+
declare class FetchAiClient implements AIClient {
|
|
6
|
+
private abortController?;
|
|
7
|
+
private config;
|
|
8
|
+
constructor(config: AiClientConfig);
|
|
9
|
+
streamChat(messages: Message[], callbacks: StreamCallbacks): Promise<void>;
|
|
10
|
+
cancel(): void;
|
|
11
|
+
private executeStream;
|
|
12
|
+
private normalizeError;
|
|
13
|
+
private backoff;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { FetchAiClient, useAiChat };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/react/useAiChat.ts
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
MessageRole
|
|
5
|
+
} from "chat-nest-core";
|
|
6
|
+
|
|
7
|
+
// src/core/aiClient.ts
|
|
8
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
9
|
+
var DEFAULT_RETRIES = 2;
|
|
10
|
+
var NonRetryableError = class extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "NonRetryableError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var FetchAiClient = class {
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = {
|
|
19
|
+
endpoint: config.endpoint,
|
|
20
|
+
headers: config.headers ?? {},
|
|
21
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT,
|
|
22
|
+
maxRetries: config.maxRetries ?? DEFAULT_RETRIES
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async streamChat(messages, callbacks) {
|
|
26
|
+
let attempt = 0;
|
|
27
|
+
while (attempt <= this.config.maxRetries) {
|
|
28
|
+
try {
|
|
29
|
+
await this.executeStream(messages, callbacks);
|
|
30
|
+
return;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error?.name === "AbortError") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (error instanceof Error && error.name === "NonRetryableError") {
|
|
36
|
+
callbacks.onError(this.normalizeError(error));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
attempt++;
|
|
40
|
+
if (attempt > this.config.maxRetries) {
|
|
41
|
+
callbacks.onError(this.normalizeError(error));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
await this.backoff(attempt);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
cancel() {
|
|
49
|
+
this.abortController?.abort();
|
|
50
|
+
}
|
|
51
|
+
async executeStream(messages, callbacks) {
|
|
52
|
+
this.abortController = new AbortController();
|
|
53
|
+
const timeoutId = setTimeout(
|
|
54
|
+
() => this.abortController?.abort(),
|
|
55
|
+
this.config.timeoutMs
|
|
56
|
+
);
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(this.config.endpoint, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
...this.config.headers
|
|
63
|
+
},
|
|
64
|
+
signal: this.abortController.signal,
|
|
65
|
+
body: JSON.stringify({ messages })
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
if (response.status >= 400 && response.status < 500) {
|
|
69
|
+
throw new NonRetryableError(`HTTP ${response.status}`);
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`HTTP ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
if (!response.body) {
|
|
74
|
+
throw new Error("Streaming not supported by the response");
|
|
75
|
+
}
|
|
76
|
+
const reader = response.body.getReader();
|
|
77
|
+
const decoder = new TextDecoder();
|
|
78
|
+
while (true) {
|
|
79
|
+
const { value, done } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
82
|
+
callbacks.onToken(chunk);
|
|
83
|
+
}
|
|
84
|
+
callbacks.onComplete();
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timeoutId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
normalizeError(error) {
|
|
90
|
+
if (error?.name === "AbortError") {
|
|
91
|
+
return new Error("Request cancelled by user");
|
|
92
|
+
}
|
|
93
|
+
if (error instanceof Error) {
|
|
94
|
+
return error;
|
|
95
|
+
}
|
|
96
|
+
return new Error("Unknown AI client error");
|
|
97
|
+
}
|
|
98
|
+
async backoff(attempt) {
|
|
99
|
+
const delay = Math.min(1e3 * attempt, 3e3);
|
|
100
|
+
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/core/utils/helpers.ts
|
|
105
|
+
function generateId() {
|
|
106
|
+
return crypto.randomUUID();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/react/useAiChat.ts
|
|
110
|
+
function useAiChat(options) {
|
|
111
|
+
const {
|
|
112
|
+
endpoint,
|
|
113
|
+
initialMessages = [],
|
|
114
|
+
maxMessages = 10
|
|
115
|
+
} = options;
|
|
116
|
+
const [messages, setMessages] = useState(
|
|
117
|
+
initialMessages
|
|
118
|
+
);
|
|
119
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
120
|
+
const [error, setError] = useState();
|
|
121
|
+
const clientRef = useRef(null);
|
|
122
|
+
if (!clientRef.current) {
|
|
123
|
+
clientRef.current = new FetchAiClient({
|
|
124
|
+
endpoint
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const sendMessage = useCallback(async (text) => {
|
|
128
|
+
if (!text.trim() || isStreaming) return;
|
|
129
|
+
setError(void 0);
|
|
130
|
+
const userMessage = {
|
|
131
|
+
id: generateId(),
|
|
132
|
+
role: MessageRole.User,
|
|
133
|
+
content: text
|
|
134
|
+
};
|
|
135
|
+
const assistantMessage = {
|
|
136
|
+
id: generateId(),
|
|
137
|
+
role: MessageRole.Assistant,
|
|
138
|
+
content: ""
|
|
139
|
+
};
|
|
140
|
+
setMessages((prev) => {
|
|
141
|
+
const next = [...prev, userMessage, assistantMessage];
|
|
142
|
+
return next.slice(-maxMessages);
|
|
143
|
+
});
|
|
144
|
+
setIsStreaming(true);
|
|
145
|
+
const history = [...messages, userMessage];
|
|
146
|
+
try {
|
|
147
|
+
await clientRef.current.streamChat(history, {
|
|
148
|
+
onToken(token) {
|
|
149
|
+
setMessages(
|
|
150
|
+
(prev) => prev.map(
|
|
151
|
+
(msg) => msg.id === assistantMessage.id ? {
|
|
152
|
+
...msg,
|
|
153
|
+
content: msg.content + token
|
|
154
|
+
} : msg
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
onComplete() {
|
|
159
|
+
setIsStreaming(false);
|
|
160
|
+
},
|
|
161
|
+
onError(err) {
|
|
162
|
+
setError(err.message);
|
|
163
|
+
setIsStreaming(false);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
} catch (err) {
|
|
167
|
+
setError(err.message);
|
|
168
|
+
setIsStreaming(false);
|
|
169
|
+
}
|
|
170
|
+
}, [endpoint, isStreaming, maxMessages, messages]);
|
|
171
|
+
const cancel = useCallback(() => {
|
|
172
|
+
clientRef.current?.cancel();
|
|
173
|
+
setIsStreaming(false);
|
|
174
|
+
}, []);
|
|
175
|
+
const reset = useCallback(() => {
|
|
176
|
+
cancel();
|
|
177
|
+
setMessages(initialMessages);
|
|
178
|
+
setError(void 0);
|
|
179
|
+
}, [cancel, initialMessages]);
|
|
180
|
+
return useMemo(
|
|
181
|
+
() => ({
|
|
182
|
+
messages,
|
|
183
|
+
sendMessage,
|
|
184
|
+
cancel,
|
|
185
|
+
reset,
|
|
186
|
+
isStreaming,
|
|
187
|
+
error
|
|
188
|
+
}),
|
|
189
|
+
[messages, sendMessage, cancel, reset, isStreaming, error]
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
FetchAiClient,
|
|
194
|
+
useAiChat
|
|
195
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chat-nest-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"homepage": "https://github.com/shivams10/chat-nest",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/shivams10/chat-nest/issues"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/shivams10/chat-nest"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format esm --dts --external react --external react-dom",
|
|
20
|
+
"dev": "tsup src/index.ts --format esm --watch --external react --external react-dom",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [],
|
|
25
|
+
"author": "Shivam Shukla",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"description": "",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^19.2.8",
|
|
31
|
+
"tsup": "^8.5.1",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=18",
|
|
36
|
+
"react-dom": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"import": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.ts"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"chat-nest-core": "*"
|
|
46
|
+
}
|
|
47
|
+
}
|