@btst/stack 2.0.0 → 2.0.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/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +32 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +32 -0
- package/dist/plugins/cms/api/index.d.cts +1 -1
- package/dist/plugins/cms/api/index.d.mts +1 -1
- package/dist/plugins/cms/api/index.d.ts +1 -1
- package/dist/plugins/kanban/api/index.d.cts +9 -9
- package/dist/plugins/kanban/api/index.d.mts +9 -9
- package/dist/plugins/kanban/api/index.d.ts +9 -9
- package/package.json +1 -1
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +64 -1
|
@@ -118,6 +118,9 @@ function ChatInterface({
|
|
|
118
118
|
transport,
|
|
119
119
|
onError: (err) => {
|
|
120
120
|
console.error("useChat onError:", err);
|
|
121
|
+
if (!id && !hasNavigatedRef.current) {
|
|
122
|
+
isFirstMessageSentRef.current = false;
|
|
123
|
+
}
|
|
121
124
|
},
|
|
122
125
|
onFinish: async () => {
|
|
123
126
|
if (isPublicMode) return;
|
|
@@ -183,7 +186,29 @@ function ChatInterface({
|
|
|
183
186
|
const [input, setInput] = React.useState("");
|
|
184
187
|
const [attachedFiles, setAttachedFiles] = React.useState([]);
|
|
185
188
|
const scrollRef = React.useRef(null);
|
|
189
|
+
const userHasScrolledRef = React.useRef(false);
|
|
190
|
+
const prevStatusRef = React.useRef(status);
|
|
191
|
+
React.useEffect(() => {
|
|
192
|
+
if (status !== prevStatusRef.current && (status === "streaming" || status === "submitted")) {
|
|
193
|
+
userHasScrolledRef.current = false;
|
|
194
|
+
}
|
|
195
|
+
prevStatusRef.current = status;
|
|
196
|
+
}, [status]);
|
|
186
197
|
React.useEffect(() => {
|
|
198
|
+
const viewport = scrollRef.current?.querySelector(
|
|
199
|
+
"[data-radix-scroll-area-viewport]"
|
|
200
|
+
);
|
|
201
|
+
if (!viewport) return;
|
|
202
|
+
const handleScroll = () => {
|
|
203
|
+
const { scrollTop, scrollHeight, clientHeight } = viewport;
|
|
204
|
+
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
205
|
+
userHasScrolledRef.current = !isNearBottom;
|
|
206
|
+
};
|
|
207
|
+
viewport.addEventListener("scroll", handleScroll);
|
|
208
|
+
return () => viewport.removeEventListener("scroll", handleScroll);
|
|
209
|
+
}, []);
|
|
210
|
+
React.useEffect(() => {
|
|
211
|
+
if (userHasScrolledRef.current) return;
|
|
187
212
|
if (scrollRef.current) {
|
|
188
213
|
const scrollElement = scrollRef.current.querySelector(
|
|
189
214
|
"[data-radix-scroll-area-viewport]"
|
|
@@ -208,8 +233,10 @@ function ChatInterface({
|
|
|
208
233
|
if (!isPublicMode && !id && messages.length === 0) {
|
|
209
234
|
isFirstMessageSentRef.current = true;
|
|
210
235
|
}
|
|
236
|
+
userHasScrolledRef.current = false;
|
|
211
237
|
const savedInput = input;
|
|
212
238
|
const savedFiles = files ? [...files] : [];
|
|
239
|
+
const messageCountBeforeSend = messages.length;
|
|
213
240
|
setInput("");
|
|
214
241
|
setAttachedFiles([]);
|
|
215
242
|
try {
|
|
@@ -231,6 +258,10 @@ function ChatInterface({
|
|
|
231
258
|
} catch (error2) {
|
|
232
259
|
setInput(savedInput);
|
|
233
260
|
setAttachedFiles(savedFiles);
|
|
261
|
+
if (isFirstMessageSentRef.current && !hasNavigatedRef.current) {
|
|
262
|
+
isFirstMessageSentRef.current = false;
|
|
263
|
+
}
|
|
264
|
+
setMessages((prev) => prev.slice(0, messageCountBeforeSend));
|
|
234
265
|
console.error("Error sending message:", error2);
|
|
235
266
|
}
|
|
236
267
|
};
|
|
@@ -272,6 +303,7 @@ function ChatInterface({
|
|
|
272
303
|
className
|
|
273
304
|
),
|
|
274
305
|
"data-testid": "chat-interface",
|
|
306
|
+
"data-chat-status": status,
|
|
275
307
|
children: /* @__PURE__ */ jsxRuntime.jsx(scrollArea.ScrollArea, { ref: scrollRef, className: "flex-1 h-full", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
276
308
|
"div",
|
|
277
309
|
{
|
|
@@ -116,6 +116,9 @@ function ChatInterface({
|
|
|
116
116
|
transport,
|
|
117
117
|
onError: (err) => {
|
|
118
118
|
console.error("useChat onError:", err);
|
|
119
|
+
if (!id && !hasNavigatedRef.current) {
|
|
120
|
+
isFirstMessageSentRef.current = false;
|
|
121
|
+
}
|
|
119
122
|
},
|
|
120
123
|
onFinish: async () => {
|
|
121
124
|
if (isPublicMode) return;
|
|
@@ -181,7 +184,29 @@ function ChatInterface({
|
|
|
181
184
|
const [input, setInput] = useState("");
|
|
182
185
|
const [attachedFiles, setAttachedFiles] = useState([]);
|
|
183
186
|
const scrollRef = useRef(null);
|
|
187
|
+
const userHasScrolledRef = useRef(false);
|
|
188
|
+
const prevStatusRef = useRef(status);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (status !== prevStatusRef.current && (status === "streaming" || status === "submitted")) {
|
|
191
|
+
userHasScrolledRef.current = false;
|
|
192
|
+
}
|
|
193
|
+
prevStatusRef.current = status;
|
|
194
|
+
}, [status]);
|
|
184
195
|
useEffect(() => {
|
|
196
|
+
const viewport = scrollRef.current?.querySelector(
|
|
197
|
+
"[data-radix-scroll-area-viewport]"
|
|
198
|
+
);
|
|
199
|
+
if (!viewport) return;
|
|
200
|
+
const handleScroll = () => {
|
|
201
|
+
const { scrollTop, scrollHeight, clientHeight } = viewport;
|
|
202
|
+
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
203
|
+
userHasScrolledRef.current = !isNearBottom;
|
|
204
|
+
};
|
|
205
|
+
viewport.addEventListener("scroll", handleScroll);
|
|
206
|
+
return () => viewport.removeEventListener("scroll", handleScroll);
|
|
207
|
+
}, []);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (userHasScrolledRef.current) return;
|
|
185
210
|
if (scrollRef.current) {
|
|
186
211
|
const scrollElement = scrollRef.current.querySelector(
|
|
187
212
|
"[data-radix-scroll-area-viewport]"
|
|
@@ -206,8 +231,10 @@ function ChatInterface({
|
|
|
206
231
|
if (!isPublicMode && !id && messages.length === 0) {
|
|
207
232
|
isFirstMessageSentRef.current = true;
|
|
208
233
|
}
|
|
234
|
+
userHasScrolledRef.current = false;
|
|
209
235
|
const savedInput = input;
|
|
210
236
|
const savedFiles = files ? [...files] : [];
|
|
237
|
+
const messageCountBeforeSend = messages.length;
|
|
211
238
|
setInput("");
|
|
212
239
|
setAttachedFiles([]);
|
|
213
240
|
try {
|
|
@@ -229,6 +256,10 @@ function ChatInterface({
|
|
|
229
256
|
} catch (error2) {
|
|
230
257
|
setInput(savedInput);
|
|
231
258
|
setAttachedFiles(savedFiles);
|
|
259
|
+
if (isFirstMessageSentRef.current && !hasNavigatedRef.current) {
|
|
260
|
+
isFirstMessageSentRef.current = false;
|
|
261
|
+
}
|
|
262
|
+
setMessages((prev) => prev.slice(0, messageCountBeforeSend));
|
|
232
263
|
console.error("Error sending message:", error2);
|
|
233
264
|
}
|
|
234
265
|
};
|
|
@@ -270,6 +301,7 @@ function ChatInterface({
|
|
|
270
301
|
className
|
|
271
302
|
),
|
|
272
303
|
"data-testid": "chat-interface",
|
|
304
|
+
"data-chat-status": status,
|
|
273
305
|
children: /* @__PURE__ */ jsx(ScrollArea, { ref: scrollRef, className: "flex-1 h-full", children: /* @__PURE__ */ jsxs(
|
|
274
306
|
"div",
|
|
275
307
|
{
|
|
@@ -16,8 +16,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
|
|
|
16
16
|
itemCount: number;
|
|
17
17
|
createdAt: string;
|
|
18
18
|
updatedAt: string;
|
|
19
|
-
id: string;
|
|
20
19
|
name: string;
|
|
20
|
+
id: string;
|
|
21
21
|
slug: string;
|
|
22
22
|
description?: string | undefined;
|
|
23
23
|
jsonSchema: string;
|
|
@@ -16,8 +16,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
|
|
|
16
16
|
itemCount: number;
|
|
17
17
|
createdAt: string;
|
|
18
18
|
updatedAt: string;
|
|
19
|
-
id: string;
|
|
20
19
|
name: string;
|
|
20
|
+
id: string;
|
|
21
21
|
slug: string;
|
|
22
22
|
description?: string | undefined;
|
|
23
23
|
jsonSchema: string;
|
|
@@ -16,8 +16,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
|
|
|
16
16
|
itemCount: number;
|
|
17
17
|
createdAt: string;
|
|
18
18
|
updatedAt: string;
|
|
19
|
-
id: string;
|
|
20
19
|
name: string;
|
|
20
|
+
id: string;
|
|
21
21
|
slug: string;
|
|
22
22
|
description?: string | undefined;
|
|
23
23
|
jsonSchema: string;
|
|
@@ -5,12 +5,12 @@ import { B as Board, C as Column, T as Task, d as ColumnWithTasks } from '../../
|
|
|
5
5
|
|
|
6
6
|
declare const createBoardSchema: z.ZodObject<{
|
|
7
7
|
description: z.ZodOptional<z.ZodString>;
|
|
8
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
9
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
10
8
|
name: z.ZodString;
|
|
11
9
|
slug: z.ZodOptional<z.ZodString>;
|
|
12
10
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
13
11
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
12
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
13
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
14
14
|
}, z.core.$strip>;
|
|
15
15
|
declare const updateBoardSchema: z.ZodObject<{
|
|
16
16
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
@@ -24,9 +24,9 @@ declare const updateBoardSchema: z.ZodObject<{
|
|
|
24
24
|
}, z.core.$strip>;
|
|
25
25
|
declare const createColumnSchema: z.ZodObject<{
|
|
26
26
|
title: z.ZodString;
|
|
27
|
+
boardId: z.ZodString;
|
|
27
28
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
28
29
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
29
|
-
boardId: z.ZodString;
|
|
30
30
|
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
31
31
|
}, z.core.$strip>;
|
|
32
32
|
declare const updateColumnSchema: z.ZodObject<{
|
|
@@ -265,12 +265,12 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
265
265
|
method: "POST";
|
|
266
266
|
body: z.ZodObject<{
|
|
267
267
|
description: z.ZodOptional<z.ZodString>;
|
|
268
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
269
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
270
268
|
name: z.ZodString;
|
|
271
269
|
slug: z.ZodOptional<z.ZodString>;
|
|
272
270
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
273
271
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
272
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
273
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
274
274
|
}, z.core.$strip>;
|
|
275
275
|
}, {
|
|
276
276
|
columns: ColumnWithTasks[];
|
|
@@ -287,12 +287,12 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
287
287
|
method: "PUT";
|
|
288
288
|
body: z.ZodObject<{
|
|
289
289
|
description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
290
|
-
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
291
|
-
updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
292
290
|
name: z.ZodOptional<z.ZodString>;
|
|
293
291
|
slug: z.ZodOptional<z.ZodString>;
|
|
294
292
|
ownerId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
295
293
|
organizationId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
294
|
+
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
295
|
+
updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
296
296
|
}, z.core.$strip>;
|
|
297
297
|
}, Board>;
|
|
298
298
|
readonly deleteBoard: better_call.StrictEndpoint<"/boards/:id", {
|
|
@@ -304,9 +304,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
304
304
|
method: "POST";
|
|
305
305
|
body: z.ZodObject<{
|
|
306
306
|
title: z.ZodString;
|
|
307
|
+
boardId: z.ZodString;
|
|
307
308
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
308
309
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
309
|
-
boardId: z.ZodString;
|
|
310
310
|
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
311
311
|
}, z.core.$strip>;
|
|
312
312
|
}, Column>;
|
|
@@ -314,9 +314,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
314
314
|
method: "PUT";
|
|
315
315
|
body: z.ZodObject<{
|
|
316
316
|
title: z.ZodOptional<z.ZodString>;
|
|
317
|
+
boardId: z.ZodOptional<z.ZodString>;
|
|
317
318
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
318
319
|
updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
319
|
-
boardId: z.ZodOptional<z.ZodString>;
|
|
320
320
|
order: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodNumber>>>;
|
|
321
321
|
}, z.core.$strip>;
|
|
322
322
|
}, Column>;
|
|
@@ -5,12 +5,12 @@ import { B as Board, C as Column, T as Task, d as ColumnWithTasks } from '../../
|
|
|
5
5
|
|
|
6
6
|
declare const createBoardSchema: z.ZodObject<{
|
|
7
7
|
description: z.ZodOptional<z.ZodString>;
|
|
8
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
9
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
10
8
|
name: z.ZodString;
|
|
11
9
|
slug: z.ZodOptional<z.ZodString>;
|
|
12
10
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
13
11
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
12
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
13
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
14
14
|
}, z.core.$strip>;
|
|
15
15
|
declare const updateBoardSchema: z.ZodObject<{
|
|
16
16
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
@@ -24,9 +24,9 @@ declare const updateBoardSchema: z.ZodObject<{
|
|
|
24
24
|
}, z.core.$strip>;
|
|
25
25
|
declare const createColumnSchema: z.ZodObject<{
|
|
26
26
|
title: z.ZodString;
|
|
27
|
+
boardId: z.ZodString;
|
|
27
28
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
28
29
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
29
|
-
boardId: z.ZodString;
|
|
30
30
|
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
31
31
|
}, z.core.$strip>;
|
|
32
32
|
declare const updateColumnSchema: z.ZodObject<{
|
|
@@ -265,12 +265,12 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
265
265
|
method: "POST";
|
|
266
266
|
body: z.ZodObject<{
|
|
267
267
|
description: z.ZodOptional<z.ZodString>;
|
|
268
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
269
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
270
268
|
name: z.ZodString;
|
|
271
269
|
slug: z.ZodOptional<z.ZodString>;
|
|
272
270
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
273
271
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
272
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
273
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
274
274
|
}, z.core.$strip>;
|
|
275
275
|
}, {
|
|
276
276
|
columns: ColumnWithTasks[];
|
|
@@ -287,12 +287,12 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
287
287
|
method: "PUT";
|
|
288
288
|
body: z.ZodObject<{
|
|
289
289
|
description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
290
|
-
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
291
|
-
updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
292
290
|
name: z.ZodOptional<z.ZodString>;
|
|
293
291
|
slug: z.ZodOptional<z.ZodString>;
|
|
294
292
|
ownerId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
295
293
|
organizationId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
294
|
+
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
295
|
+
updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
296
296
|
}, z.core.$strip>;
|
|
297
297
|
}, Board>;
|
|
298
298
|
readonly deleteBoard: better_call.StrictEndpoint<"/boards/:id", {
|
|
@@ -304,9 +304,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
304
304
|
method: "POST";
|
|
305
305
|
body: z.ZodObject<{
|
|
306
306
|
title: z.ZodString;
|
|
307
|
+
boardId: z.ZodString;
|
|
307
308
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
308
309
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
309
|
-
boardId: z.ZodString;
|
|
310
310
|
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
311
311
|
}, z.core.$strip>;
|
|
312
312
|
}, Column>;
|
|
@@ -314,9 +314,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
314
314
|
method: "PUT";
|
|
315
315
|
body: z.ZodObject<{
|
|
316
316
|
title: z.ZodOptional<z.ZodString>;
|
|
317
|
+
boardId: z.ZodOptional<z.ZodString>;
|
|
317
318
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
318
319
|
updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
319
|
-
boardId: z.ZodOptional<z.ZodString>;
|
|
320
320
|
order: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodNumber>>>;
|
|
321
321
|
}, z.core.$strip>;
|
|
322
322
|
}, Column>;
|
|
@@ -5,12 +5,12 @@ import { B as Board, C as Column, T as Task, d as ColumnWithTasks } from '../../
|
|
|
5
5
|
|
|
6
6
|
declare const createBoardSchema: z.ZodObject<{
|
|
7
7
|
description: z.ZodOptional<z.ZodString>;
|
|
8
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
9
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
10
8
|
name: z.ZodString;
|
|
11
9
|
slug: z.ZodOptional<z.ZodString>;
|
|
12
10
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
13
11
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
12
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
13
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
14
14
|
}, z.core.$strip>;
|
|
15
15
|
declare const updateBoardSchema: z.ZodObject<{
|
|
16
16
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
@@ -24,9 +24,9 @@ declare const updateBoardSchema: z.ZodObject<{
|
|
|
24
24
|
}, z.core.$strip>;
|
|
25
25
|
declare const createColumnSchema: z.ZodObject<{
|
|
26
26
|
title: z.ZodString;
|
|
27
|
+
boardId: z.ZodString;
|
|
27
28
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
28
29
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
29
|
-
boardId: z.ZodString;
|
|
30
30
|
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
31
31
|
}, z.core.$strip>;
|
|
32
32
|
declare const updateColumnSchema: z.ZodObject<{
|
|
@@ -265,12 +265,12 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
265
265
|
method: "POST";
|
|
266
266
|
body: z.ZodObject<{
|
|
267
267
|
description: z.ZodOptional<z.ZodString>;
|
|
268
|
-
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
269
|
-
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
270
268
|
name: z.ZodString;
|
|
271
269
|
slug: z.ZodOptional<z.ZodString>;
|
|
272
270
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
273
271
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
272
|
+
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
273
|
+
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
274
274
|
}, z.core.$strip>;
|
|
275
275
|
}, {
|
|
276
276
|
columns: ColumnWithTasks[];
|
|
@@ -287,12 +287,12 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
287
287
|
method: "PUT";
|
|
288
288
|
body: z.ZodObject<{
|
|
289
289
|
description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
290
|
-
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
291
|
-
updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
292
290
|
name: z.ZodOptional<z.ZodString>;
|
|
293
291
|
slug: z.ZodOptional<z.ZodString>;
|
|
294
292
|
ownerId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
295
293
|
organizationId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
294
|
+
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
295
|
+
updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
296
296
|
}, z.core.$strip>;
|
|
297
297
|
}, Board>;
|
|
298
298
|
readonly deleteBoard: better_call.StrictEndpoint<"/boards/:id", {
|
|
@@ -304,9 +304,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
304
304
|
method: "POST";
|
|
305
305
|
body: z.ZodObject<{
|
|
306
306
|
title: z.ZodString;
|
|
307
|
+
boardId: z.ZodString;
|
|
307
308
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
308
309
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
309
|
-
boardId: z.ZodString;
|
|
310
310
|
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
311
311
|
}, z.core.$strip>;
|
|
312
312
|
}, Column>;
|
|
@@ -314,9 +314,9 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
314
314
|
method: "PUT";
|
|
315
315
|
body: z.ZodObject<{
|
|
316
316
|
title: z.ZodOptional<z.ZodString>;
|
|
317
|
+
boardId: z.ZodOptional<z.ZodString>;
|
|
317
318
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
318
319
|
updatedAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
319
|
-
boardId: z.ZodOptional<z.ZodString>;
|
|
320
320
|
order: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodNumber>>>;
|
|
321
321
|
}, z.core.$strip>;
|
|
322
322
|
}, Column>;
|
package/package.json
CHANGED
|
@@ -170,6 +170,12 @@ export function ChatInterface({
|
|
|
170
170
|
transport,
|
|
171
171
|
onError: (err) => {
|
|
172
172
|
console.error("useChat onError:", err);
|
|
173
|
+
// Reset first-message tracking if the send failed before a conversation was created.
|
|
174
|
+
// Without this, isFirstMessageSentRef stays true and the next successful send
|
|
175
|
+
// skips the "first message" navigation logic, corrupting the conversation flow.
|
|
176
|
+
if (!id && !hasNavigatedRef.current) {
|
|
177
|
+
isFirstMessageSentRef.current = false;
|
|
178
|
+
}
|
|
173
179
|
},
|
|
174
180
|
onFinish: async () => {
|
|
175
181
|
// In public mode, skip all persistence-related operations
|
|
@@ -271,8 +277,43 @@ export function ChatInterface({
|
|
|
271
277
|
const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]);
|
|
272
278
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
273
279
|
|
|
274
|
-
//
|
|
280
|
+
// Track whether the user has manually scrolled away from the bottom.
|
|
281
|
+
// When true, auto-scroll is paused so the user can read earlier context.
|
|
282
|
+
const userHasScrolledRef = useRef(false);
|
|
283
|
+
const prevStatusRef = useRef(status);
|
|
284
|
+
|
|
285
|
+
// Reset the scroll lock when a new generation starts so auto-scroll
|
|
286
|
+
// resumes for the next assistant response.
|
|
275
287
|
useEffect(() => {
|
|
288
|
+
if (
|
|
289
|
+
status !== prevStatusRef.current &&
|
|
290
|
+
(status === "streaming" || status === "submitted")
|
|
291
|
+
) {
|
|
292
|
+
userHasScrolledRef.current = false;
|
|
293
|
+
}
|
|
294
|
+
prevStatusRef.current = status;
|
|
295
|
+
}, [status]);
|
|
296
|
+
|
|
297
|
+
// Attach a scroll listener to detect when the user scrolls away from the bottom.
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
const viewport = scrollRef.current?.querySelector(
|
|
300
|
+
"[data-radix-scroll-area-viewport]",
|
|
301
|
+
);
|
|
302
|
+
if (!viewport) return;
|
|
303
|
+
|
|
304
|
+
const handleScroll = () => {
|
|
305
|
+
const { scrollTop, scrollHeight, clientHeight } = viewport;
|
|
306
|
+
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
307
|
+
userHasScrolledRef.current = !isNearBottom;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
viewport.addEventListener("scroll", handleScroll);
|
|
311
|
+
return () => viewport.removeEventListener("scroll", handleScroll);
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
// Auto-scroll to bottom when messages change, unless the user has scrolled away
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (userHasScrolledRef.current) return;
|
|
276
317
|
if (scrollRef.current) {
|
|
277
318
|
const scrollElement = scrollRef.current.querySelector(
|
|
278
319
|
"[data-radix-scroll-area-viewport]",
|
|
@@ -309,10 +350,24 @@ export function ChatInterface({
|
|
|
309
350
|
isFirstMessageSentRef.current = true;
|
|
310
351
|
}
|
|
311
352
|
|
|
353
|
+
// Re-enable auto-scroll so the user's own message (and any subsequent
|
|
354
|
+
// error indicator or assistant reply) is scrolled into view. Without
|
|
355
|
+
// this, if the user had scrolled up earlier, userHasScrolledRef stays
|
|
356
|
+
// true and none of the new content would be auto-scrolled to — and if
|
|
357
|
+
// the request fails before reaching "streaming" status the ref would
|
|
358
|
+
// remain stuck permanently.
|
|
359
|
+
userHasScrolledRef.current = false;
|
|
360
|
+
|
|
312
361
|
// Save current values before clearing - we'll restore them if send fails
|
|
313
362
|
const savedInput = input;
|
|
314
363
|
const savedFiles = files ? [...files] : [];
|
|
315
364
|
|
|
365
|
+
// Capture the message count before sending so we can restore to this
|
|
366
|
+
// exact point on failure. The SDK may append both a user message and a
|
|
367
|
+
// partial assistant message during streaming — using a fixed snapshot
|
|
368
|
+
// length removes all of them instead of just the last one.
|
|
369
|
+
const messageCountBeforeSend = messages.length;
|
|
370
|
+
|
|
316
371
|
// Clear input immediately (optimistically) - the AI SDK renders messages optimistically,
|
|
317
372
|
// so we need to clear the input before the message appears to avoid duplicate text
|
|
318
373
|
setInput("");
|
|
@@ -341,6 +396,13 @@ export function ChatInterface({
|
|
|
341
396
|
// Restore input on failure so user can retry
|
|
342
397
|
setInput(savedInput);
|
|
343
398
|
setAttachedFiles(savedFiles);
|
|
399
|
+
// Reset first-message tracking so the next attempt still triggers navigation
|
|
400
|
+
if (isFirstMessageSentRef.current && !hasNavigatedRef.current) {
|
|
401
|
+
isFirstMessageSentRef.current = false;
|
|
402
|
+
}
|
|
403
|
+
// Remove all messages the SDK added after our send attempt (optimistic
|
|
404
|
+
// user message AND any partial assistant message from a mid-stream failure).
|
|
405
|
+
setMessages((prev) => prev.slice(0, messageCountBeforeSend));
|
|
344
406
|
console.error("Error sending message:", error);
|
|
345
407
|
}
|
|
346
408
|
};
|
|
@@ -410,6 +472,7 @@ export function ChatInterface({
|
|
|
410
472
|
className,
|
|
411
473
|
)}
|
|
412
474
|
data-testid="chat-interface"
|
|
475
|
+
data-chat-status={status}
|
|
413
476
|
>
|
|
414
477
|
{/* Messages Area */}
|
|
415
478
|
<ScrollArea ref={scrollRef} className="flex-1 h-full">
|