@chat-js/cli 0.3.0 → 0.6.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/dist/index.js +1173 -964
- package/package.json +1 -1
- package/templates/chat-app/app/(auth)/device-login/page.tsx +37 -0
- package/templates/chat-app/app/(auth)/login/page.tsx +26 -2
- package/templates/chat-app/app/(auth)/register/page.tsx +0 -12
- package/templates/chat-app/app/(chat)/api/chat/filter-reasoning-parts.ts +1 -1
- package/templates/chat-app/app/(chat)/api/chat/prepare/route.ts +94 -0
- package/templates/chat-app/app/(chat)/api/chat/route.ts +107 -16
- package/templates/chat-app/app/(chat)/layout.tsx +4 -1
- package/templates/chat-app/app/api/trpc/[trpc]/route.ts +1 -0
- package/templates/chat-app/app/globals.css +9 -9
- package/templates/chat-app/app/layout.tsx +4 -2
- package/templates/chat-app/biome.jsonc +3 -3
- package/templates/chat-app/chat.config.ts +32 -12
- package/templates/chat-app/components/ai-elements/prompt-input.tsx +1 -1
- package/templates/chat-app/components/anonymous-session-init.tsx +10 -6
- package/templates/chat-app/components/artifact-actions.tsx +81 -18
- package/templates/chat-app/components/artifact-panel.tsx +142 -41
- package/templates/chat-app/components/attachment-list.tsx +1 -1
- package/templates/chat-app/components/{social-auth-providers.tsx → auth-providers.tsx} +49 -4
- package/templates/chat-app/components/chat/chat-welcome.tsx +3 -3
- package/templates/chat-app/components/chat-menu-items.tsx +1 -1
- package/templates/chat-app/components/chat-sync.tsx +9 -11
- package/templates/chat-app/components/console.tsx +9 -9
- package/templates/chat-app/components/context-usage.tsx +2 -2
- package/templates/chat-app/components/create-artifact.tsx +15 -5
- package/templates/chat-app/components/data-stream-handler.tsx +57 -16
- package/templates/chat-app/components/device-login-page.tsx +191 -0
- package/templates/chat-app/components/diffview.tsx +8 -2
- package/templates/chat-app/components/electron-auth-handler.tsx +184 -0
- package/templates/chat-app/components/electron-auth-ui.tsx +121 -0
- package/templates/chat-app/components/favicon-group.tsx +1 -1
- package/templates/chat-app/components/feedback-actions.tsx +7 -3
- package/templates/chat-app/components/greeting.tsx +1 -1
- package/templates/chat-app/components/interactive-chart-impl.tsx +3 -4
- package/templates/chat-app/components/interactive-charts.tsx +1 -1
- package/templates/chat-app/components/login-form.tsx +52 -10
- package/templates/chat-app/components/message-editor.tsx +7 -3
- package/templates/chat-app/components/message-siblings.tsx +14 -1
- package/templates/chat-app/components/model-selector.tsx +295 -27
- package/templates/chat-app/components/multimodal-input.tsx +259 -22
- package/templates/chat-app/components/parallel-response-cards.tsx +175 -0
- package/templates/chat-app/components/part/code-execution.tsx +8 -2
- package/templates/chat-app/components/part/document-common.tsx +1 -1
- package/templates/chat-app/components/part/document-preview.tsx +5 -5
- package/templates/chat-app/components/part/retrieve-url.tsx +12 -12
- package/templates/chat-app/components/part/text-message-part.tsx +9 -1
- package/templates/chat-app/components/project-chat-item.tsx +1 -1
- package/templates/chat-app/components/project-menu-items.tsx +1 -1
- package/templates/chat-app/components/research-task.tsx +1 -1
- package/templates/chat-app/components/research-tasks.tsx +1 -1
- package/templates/chat-app/components/retry-button.tsx +25 -8
- package/templates/chat-app/components/sandbox.tsx +1 -1
- package/templates/chat-app/components/sheet-editor.tsx +7 -7
- package/templates/chat-app/components/sidebar-chats-list.tsx +1 -1
- package/templates/chat-app/components/sidebar-toggle.tsx +15 -2
- package/templates/chat-app/components/sidebar-top-row.tsx +27 -12
- package/templates/chat-app/components/sidebar-user-nav.tsx +10 -1
- package/templates/chat-app/components/signup-form.tsx +49 -10
- package/templates/chat-app/components/sources.tsx +4 -4
- package/templates/chat-app/components/text-editor.tsx +5 -2
- package/templates/chat-app/components/toolbar.tsx +3 -3
- package/templates/chat-app/components/ui/sidebar.tsx +0 -1
- package/templates/chat-app/components/upgrade-cta/limit-display.tsx +1 -1
- package/templates/chat-app/components/user-message.tsx +14 -2
- package/templates/chat-app/electron.d.ts +41 -0
- package/templates/chat-app/evals/my-eval.eval.ts +3 -1
- package/templates/chat-app/hooks/chat-sync-hooks.ts +11 -0
- package/templates/chat-app/hooks/use-artifact.tsx +13 -13
- package/templates/chat-app/hooks/use-navigate-to-message.ts +39 -0
- package/templates/chat-app/lib/ai/gateways/provider-types.ts +19 -10
- package/templates/chat-app/lib/ai/stream-errors.test.ts +72 -0
- package/templates/chat-app/lib/ai/stream-errors.ts +94 -0
- package/templates/chat-app/lib/ai/tools/code-execution.javascript.ts +171 -0
- package/templates/chat-app/lib/ai/tools/code-execution.python.ts +336 -0
- package/templates/chat-app/lib/ai/tools/code-execution.shared.test.ts +71 -0
- package/templates/chat-app/lib/ai/tools/code-execution.shared.ts +59 -0
- package/templates/chat-app/lib/ai/tools/code-execution.ts +62 -391
- package/templates/chat-app/lib/ai/tools/code-execution.types.ts +24 -0
- package/templates/chat-app/lib/ai/tools/steps/multi-query-web-search.ts +3 -2
- package/templates/chat-app/lib/ai/types.ts +74 -3
- package/templates/chat-app/lib/anonymous-session-client.ts +0 -3
- package/templates/chat-app/lib/artifacts/code/client.tsx +35 -5
- package/templates/chat-app/lib/artifacts/sheet/client.tsx +11 -3
- package/templates/chat-app/lib/auth-client.ts +23 -1
- package/templates/chat-app/lib/auth.ts +18 -1
- package/templates/chat-app/lib/blob.ts +1 -1
- package/templates/chat-app/lib/clone-messages.ts +1 -1
- package/templates/chat-app/lib/config-schema.ts +18 -1
- package/templates/chat-app/lib/constants.ts +3 -4
- package/templates/chat-app/lib/db/migrations/0044_gray_red_shift.sql +5 -0
- package/templates/chat-app/lib/db/migrations/meta/0044_snapshot.json +1480 -0
- package/templates/chat-app/lib/db/migrations/meta/_journal.json +7 -0
- package/templates/chat-app/lib/db/queries.ts +84 -4
- package/templates/chat-app/lib/db/schema.ts +4 -1
- package/templates/chat-app/lib/editor/config.ts +4 -4
- package/templates/chat-app/lib/electron-auth.ts +96 -0
- package/templates/chat-app/lib/env-schema.ts +33 -4
- package/templates/chat-app/lib/message-conversion.ts +14 -2
- package/templates/chat-app/lib/playwright-test-environment.ts +18 -0
- package/templates/chat-app/lib/social-auth.ts +5 -0
- package/templates/chat-app/lib/stores/hooks-threads.ts +38 -1
- package/templates/chat-app/lib/stores/with-threads.test.ts +137 -0
- package/templates/chat-app/lib/stores/with-threads.ts +159 -7
- package/templates/chat-app/lib/stores/with-tracing.ts +1 -1
- package/templates/chat-app/lib/thread-utils.ts +22 -3
- package/templates/chat-app/lib/utils/download-assets.ts +6 -7
- package/templates/chat-app/lib/utils/rate-limit.ts +9 -3
- package/templates/chat-app/package.json +20 -18
- package/templates/chat-app/playwright.config.ts +0 -19
- package/templates/chat-app/providers/chat-input-provider.tsx +40 -2
- package/templates/chat-app/proxy.ts +28 -3
- package/templates/chat-app/scripts/check-env.ts +10 -0
- package/templates/chat-app/scripts/db-branch-delete.sh +7 -1
- package/templates/chat-app/scripts/db-branch-use.sh +7 -1
- package/templates/chat-app/scripts/with-db.sh +7 -1
- package/templates/chat-app/trpc/server.tsx +7 -2
- package/templates/chat-app/tsconfig.json +2 -1
- package/templates/chat-app/vercel.json +0 -10
- package/templates/chat-app/vitest.config.ts +2 -0
- package/templates/electron/CHANGELOG.md +7 -0
- package/templates/electron/README.md +54 -0
- package/templates/electron/entitlements.mac.plist +10 -0
- package/templates/electron/forge.config.ts +157 -0
- package/templates/electron/icon.png +0 -0
- package/templates/electron/package.json +53 -0
- package/templates/electron/scripts/generate-icons.test.js +37 -0
- package/templates/electron/scripts/generate-icons.ts +29 -0
- package/templates/electron/scripts/run-forge.cjs +28 -0
- package/templates/electron/scripts/write-branding.ts +18 -0
- package/templates/electron/src/config.ts +16 -0
- package/templates/electron/src/lib/auth-client.ts +64 -0
- package/templates/electron/src/main.ts +670 -0
- package/templates/electron/src/preload.d.ts +27 -0
- package/templates/electron/src/preload.ts +25 -0
- package/templates/electron/tsconfig.json +18 -0
|
@@ -19,6 +19,12 @@ export interface MessageSiblingInfo<UM> {
|
|
|
19
19
|
siblings: UM[];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface ParallelGroupInfo<UM> {
|
|
23
|
+
messages: UM[];
|
|
24
|
+
parallelGroupId: string;
|
|
25
|
+
selectedMessageId: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
export type ThreadAugmentedState<UM extends UIMessage> =
|
|
23
29
|
BaseChatStoreState<UM> & {
|
|
24
30
|
threadEpoch: number;
|
|
@@ -41,6 +47,7 @@ export type ThreadAugmentedState<UM extends UIMessage> =
|
|
|
41
47
|
addMessageToTree: (message: UM) => void;
|
|
42
48
|
/** Look up sibling info for a message. */
|
|
43
49
|
getMessageSiblingInfo: (messageId: string) => MessageSiblingInfo<UM> | null;
|
|
50
|
+
getParallelGroupInfo: (messageId: string) => ParallelGroupInfo<UM> | null;
|
|
44
51
|
/**
|
|
45
52
|
* Switch to a sibling thread. Returns the new thread array,
|
|
46
53
|
* or null if no switch was possible.
|
|
@@ -49,6 +56,7 @@ export type ThreadAugmentedState<UM extends UIMessage> =
|
|
|
49
56
|
messageId: string,
|
|
50
57
|
direction: "prev" | "next"
|
|
51
58
|
) => UM[] | null;
|
|
59
|
+
switchToMessage: (messageId: string) => UM[] | null;
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
export const withThreads =
|
|
@@ -64,6 +72,33 @@ export const withThreads =
|
|
|
64
72
|
const rebuildMap = (msgs: UI_MESSAGE[]) =>
|
|
65
73
|
buildChildrenMap(msgs as (UI_MESSAGE & MessageNode)[]);
|
|
66
74
|
|
|
75
|
+
const mergeTreeMessages = (
|
|
76
|
+
serverMessages: UI_MESSAGE[],
|
|
77
|
+
existingTreeMessages: UI_MESSAGE[],
|
|
78
|
+
currentVisibleMessages: UI_MESSAGE[]
|
|
79
|
+
): UI_MESSAGE[] => {
|
|
80
|
+
const merged = new Map<string, UI_MESSAGE>();
|
|
81
|
+
|
|
82
|
+
for (const message of serverMessages) {
|
|
83
|
+
merged.set(message.id, message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Preserve every local-only tree node until the server returns a message with
|
|
87
|
+
// the same id. Restricting this to pending assistant shells orphaned optimistic
|
|
88
|
+
// user messages when switching away from an in-flight branch mid-stream.
|
|
89
|
+
for (const message of existingTreeMessages) {
|
|
90
|
+
if (!merged.has(message.id)) {
|
|
91
|
+
merged.set(message.id, message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const message of currentVisibleMessages) {
|
|
96
|
+
merged.set(message.id, message);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Array.from(merged.values());
|
|
100
|
+
};
|
|
101
|
+
|
|
67
102
|
return {
|
|
68
103
|
...base,
|
|
69
104
|
threadEpoch: 0,
|
|
@@ -96,10 +131,45 @@ export const withThreads =
|
|
|
96
131
|
},
|
|
97
132
|
|
|
98
133
|
setAllMessages: (messages: UI_MESSAGE[]) => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
134
|
+
const state = get();
|
|
135
|
+
const currentVisibleMessages = state.messages;
|
|
136
|
+
const existingTreeMessages = state.allMessages;
|
|
137
|
+
const mergedMessages = mergeTreeMessages(
|
|
138
|
+
messages,
|
|
139
|
+
existingTreeMessages,
|
|
140
|
+
currentVisibleMessages
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// While the SDK is actively streaming, updating the visible thread with
|
|
144
|
+
// server data would mix the SDK's client-generated message ID with the
|
|
145
|
+
// server's assistantMessageId. The mismatch causes the SDK to push a
|
|
146
|
+
// second assistant message on the next chunk, bumping the epoch and
|
|
147
|
+
// remounting ChatSync mid-stream. Only update the tree index here and
|
|
148
|
+
// let the normal post-stream invalidation apply the full visible update.
|
|
149
|
+
if (state.status === "streaming" || state.status === "submitted") {
|
|
150
|
+
set((prev) => ({
|
|
151
|
+
...prev,
|
|
152
|
+
allMessages: mergedMessages,
|
|
153
|
+
childrenMap: rebuildMap(mergedMessages),
|
|
154
|
+
}));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const currentLeafId = currentVisibleMessages.at(-1)?.id;
|
|
159
|
+
const nextVisibleThread = currentLeafId
|
|
160
|
+
? (buildThreadFromLeaf(
|
|
161
|
+
mergedMessages as (UI_MESSAGE & MessageNode)[],
|
|
162
|
+
currentLeafId
|
|
163
|
+
) as UI_MESSAGE[])
|
|
164
|
+
: currentVisibleMessages;
|
|
165
|
+
|
|
166
|
+
originalSetMessages(nextVisibleThread);
|
|
167
|
+
set((prev) => ({
|
|
168
|
+
...prev,
|
|
169
|
+
messages: nextVisibleThread,
|
|
170
|
+
threadInitialMessages: nextVisibleThread,
|
|
171
|
+
allMessages: mergedMessages,
|
|
172
|
+
childrenMap: rebuildMap(mergedMessages),
|
|
103
173
|
}));
|
|
104
174
|
},
|
|
105
175
|
|
|
@@ -107,11 +177,11 @@ export const withThreads =
|
|
|
107
177
|
set((state) => {
|
|
108
178
|
const idx = state.allMessages.findIndex((m) => m.id === message.id);
|
|
109
179
|
let next: UI_MESSAGE[];
|
|
110
|
-
if (idx
|
|
180
|
+
if (idx === -1) {
|
|
181
|
+
next = [...state.allMessages, message];
|
|
182
|
+
} else {
|
|
111
183
|
next = [...state.allMessages];
|
|
112
184
|
next[idx] = message;
|
|
113
|
-
} else {
|
|
114
|
-
next = [...state.allMessages, message];
|
|
115
185
|
}
|
|
116
186
|
return { ...state, allMessages: next, childrenMap: rebuildMap(next) };
|
|
117
187
|
});
|
|
@@ -135,6 +205,65 @@ export const withThreads =
|
|
|
135
205
|
return { siblings, siblingIndex };
|
|
136
206
|
},
|
|
137
207
|
|
|
208
|
+
getParallelGroupInfo: (
|
|
209
|
+
messageId: string
|
|
210
|
+
): ParallelGroupInfo<UI_MESSAGE> | null => {
|
|
211
|
+
const state = get();
|
|
212
|
+
const message = state.allMessages.find((item) => item.id === messageId);
|
|
213
|
+
if (!message) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const metadata = (message as UI_MESSAGE & MessageNode).metadata;
|
|
218
|
+
const parallelGroupId = metadata?.parallelGroupId || null;
|
|
219
|
+
const parentId =
|
|
220
|
+
message.role === "user"
|
|
221
|
+
? message.id
|
|
222
|
+
: metadata?.parentMessageId || null;
|
|
223
|
+
|
|
224
|
+
if (!(parentId && parallelGroupId)) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const groupMessages = (
|
|
229
|
+
(state.childrenMap.get(parentId) ?? []) as UI_MESSAGE[]
|
|
230
|
+
)
|
|
231
|
+
.filter(
|
|
232
|
+
(candidate) =>
|
|
233
|
+
(candidate as UI_MESSAGE & MessageNode).metadata
|
|
234
|
+
?.parallelGroupId === parallelGroupId
|
|
235
|
+
)
|
|
236
|
+
.sort((a, b) => {
|
|
237
|
+
const aIndex =
|
|
238
|
+
(a as UI_MESSAGE & MessageNode).metadata?.parallelIndex ??
|
|
239
|
+
Number.MAX_SAFE_INTEGER;
|
|
240
|
+
const bIndex =
|
|
241
|
+
(b as UI_MESSAGE & MessageNode).metadata?.parallelIndex ??
|
|
242
|
+
Number.MAX_SAFE_INTEGER;
|
|
243
|
+
|
|
244
|
+
if (aIndex !== bIndex) {
|
|
245
|
+
return aIndex - bIndex;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return 0;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (groupMessages.length <= 1) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const visibleMessageIds = new Set(state.messages.map((m) => m.id));
|
|
256
|
+
const selectedMessageId =
|
|
257
|
+
groupMessages.find((candidate) => visibleMessageIds.has(candidate.id))
|
|
258
|
+
?.id ?? null;
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
messages: groupMessages,
|
|
262
|
+
parallelGroupId,
|
|
263
|
+
selectedMessageId,
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
|
|
138
267
|
switchToSibling: (
|
|
139
268
|
messageId: string,
|
|
140
269
|
direction: "prev" | "next"
|
|
@@ -170,6 +299,29 @@ export const withThreads =
|
|
|
170
299
|
return newThread;
|
|
171
300
|
},
|
|
172
301
|
|
|
302
|
+
switchToMessage: (messageId: string): UI_MESSAGE[] | null => {
|
|
303
|
+
const state = get();
|
|
304
|
+
const { allMessages, childrenMap } = state;
|
|
305
|
+
const message = allMessages.find(
|
|
306
|
+
(candidate) => candidate.id === messageId
|
|
307
|
+
);
|
|
308
|
+
if (!message) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const leaf = findLeafDfsToRightFromMessageId(
|
|
313
|
+
childrenMap as Map<string | null, (UI_MESSAGE & MessageNode)[]>,
|
|
314
|
+
messageId
|
|
315
|
+
);
|
|
316
|
+
const newThread = buildThreadFromLeaf(
|
|
317
|
+
allMessages as (UI_MESSAGE & MessageNode)[],
|
|
318
|
+
leaf ? leaf.id : messageId
|
|
319
|
+
) as UI_MESSAGE[];
|
|
320
|
+
|
|
321
|
+
state.setMessagesWithEpoch(newThread);
|
|
322
|
+
return newThread;
|
|
323
|
+
},
|
|
324
|
+
|
|
173
325
|
// Override setMessages to auto-bump epoch when thread changes
|
|
174
326
|
setMessages: (messages: UI_MESSAGE[]) => {
|
|
175
327
|
const currentMessages = get().messages;
|
|
@@ -3,6 +3,9 @@ export interface MessageNode {
|
|
|
3
3
|
id: string;
|
|
4
4
|
metadata?: {
|
|
5
5
|
parentMessageId: string | null;
|
|
6
|
+
parallelGroupId?: string | null;
|
|
7
|
+
parallelIndex?: number | null;
|
|
8
|
+
activeStreamId?: string | null;
|
|
6
9
|
createdAt: Date;
|
|
7
10
|
};
|
|
8
11
|
}
|
|
@@ -101,10 +104,26 @@ export function buildChildrenMap<T extends MessageNode>(
|
|
|
101
104
|
map.get(parentId)?.push(message);
|
|
102
105
|
}
|
|
103
106
|
for (const siblings of map.values()) {
|
|
104
|
-
siblings.sort(
|
|
105
|
-
|
|
107
|
+
siblings.sort((a, b) => {
|
|
108
|
+
const aParallelIndex = a.metadata?.parallelIndex;
|
|
109
|
+
const bParallelIndex = b.metadata?.parallelIndex;
|
|
110
|
+
const sameParallelGroup =
|
|
111
|
+
a.metadata?.parallelGroupId &&
|
|
112
|
+
a.metadata?.parallelGroupId === b.metadata?.parallelGroupId;
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
sameParallelGroup &&
|
|
116
|
+
typeof aParallelIndex === "number" &&
|
|
117
|
+
typeof bParallelIndex === "number" &&
|
|
118
|
+
aParallelIndex !== bParallelIndex
|
|
119
|
+
) {
|
|
120
|
+
return aParallelIndex - bParallelIndex;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
106
124
|
toTimestamp(a.metadata?.createdAt) - toTimestamp(b.metadata?.createdAt)
|
|
107
|
-
|
|
125
|
+
);
|
|
126
|
+
});
|
|
108
127
|
}
|
|
109
128
|
return map;
|
|
110
129
|
}
|
|
@@ -135,8 +135,8 @@ export async function replaceFilePartUrlByBinaryDataInMessages(
|
|
|
135
135
|
);
|
|
136
136
|
|
|
137
137
|
const mapPart = (
|
|
138
|
-
part: TextPart | ImagePart | FilePart
|
|
139
|
-
): TextPart | ImagePart | FilePart
|
|
138
|
+
part: TextPart | ImagePart | FilePart
|
|
139
|
+
): TextPart | ImagePart | FilePart => {
|
|
140
140
|
if (part.type === "file") {
|
|
141
141
|
return mapFilePart(part as FilePart, downloaded);
|
|
142
142
|
}
|
|
@@ -148,14 +148,13 @@ export async function replaceFilePartUrlByBinaryDataInMessages(
|
|
|
148
148
|
};
|
|
149
149
|
|
|
150
150
|
return messages.map((message) => {
|
|
151
|
-
if (typeof message.content === "string") {
|
|
151
|
+
if (message.role !== "user" || typeof message.content === "string") {
|
|
152
152
|
return message;
|
|
153
153
|
}
|
|
154
|
+
|
|
154
155
|
return {
|
|
155
156
|
...message,
|
|
156
|
-
content: (
|
|
157
|
-
|
|
158
|
-
).map(mapPart),
|
|
159
|
-
} as ModelMessage;
|
|
157
|
+
content: message.content.map(mapPart),
|
|
158
|
+
};
|
|
160
159
|
});
|
|
161
160
|
}
|
|
@@ -2,6 +2,12 @@ import "server-only";
|
|
|
2
2
|
import { config } from "@/lib/config";
|
|
3
3
|
import { ANONYMOUS_LIMITS } from "@/lib/types/anonymous";
|
|
4
4
|
|
|
5
|
+
interface RedisClient {
|
|
6
|
+
expire(key: string, seconds: number): Promise<unknown>;
|
|
7
|
+
get(key: string): Promise<string | null>;
|
|
8
|
+
incr(key: string): Promise<number>;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
interface RateLimitResult {
|
|
6
12
|
error?: string;
|
|
7
13
|
remaining: number;
|
|
@@ -13,7 +19,7 @@ interface RateLimitOptions {
|
|
|
13
19
|
identifier: string;
|
|
14
20
|
keyPrefix: string;
|
|
15
21
|
limit: number;
|
|
16
|
-
redisClient:
|
|
22
|
+
redisClient: RedisClient | null;
|
|
17
23
|
windowSize: number;
|
|
18
24
|
}
|
|
19
25
|
|
|
@@ -82,7 +88,7 @@ const WINDOW_SIZE_MONTH = 30 * 24 * 60 * 60;
|
|
|
82
88
|
|
|
83
89
|
export async function checkAnonymousRateLimit(
|
|
84
90
|
ip: string,
|
|
85
|
-
redisClient:
|
|
91
|
+
redisClient: RedisClient | null
|
|
86
92
|
): Promise<{
|
|
87
93
|
success: boolean;
|
|
88
94
|
error?: string;
|
|
@@ -126,7 +132,7 @@ export async function checkAnonymousRateLimit(
|
|
|
126
132
|
);
|
|
127
133
|
return {
|
|
128
134
|
success: false,
|
|
129
|
-
error: `Monthly message limit exceeded. You can make ${RATE_LIMIT.REQUESTS_PER_MONTH} requests per month. You've made ${RATE_LIMIT.REQUESTS_PER_MONTH - monthResult.remaining} requests this month. Try again in ${daysUntilReset} day${daysUntilReset
|
|
135
|
+
error: `Monthly message limit exceeded. You can make ${RATE_LIMIT.REQUESTS_PER_MONTH} requests per month. You've made ${RATE_LIMIT.REQUESTS_PER_MONTH - monthResult.remaining} requests this month. Try again in ${daysUntilReset} day${daysUntilReset === 1 ? "" : "s"}.`,
|
|
130
136
|
headers: {
|
|
131
137
|
"X-RateLimit-Limit": RATE_LIMIT.REQUESTS_PER_MONTH.toString(),
|
|
132
138
|
"X-RateLimit-Remaining": monthResult.remaining.toString(),
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"analyze": "next experimental-analyze",
|
|
12
12
|
"start": "next start",
|
|
13
13
|
"prod": "bun run build && bun run start",
|
|
14
|
-
"lint": "bunx ultracite@7.
|
|
15
|
-
"format": "bunx ultracite@7.
|
|
14
|
+
"lint": "bunx ultracite@7.4.3 check",
|
|
15
|
+
"format": "bunx ultracite@7.4.3 fix",
|
|
16
16
|
"check-env": "bun scripts/check-env.ts",
|
|
17
17
|
"db:generate": "drizzle-kit generate",
|
|
18
18
|
"db:migrate": "export VERCEL_ENV=production && bash scripts/with-db.sh bunx tsx lib/db/migrate.ts",
|
|
@@ -40,15 +40,17 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@ai-sdk-tools/store": "1.2.0",
|
|
43
|
-
"@ai-sdk/anthropic": "^3.0.
|
|
43
|
+
"@ai-sdk/anthropic": "^3.0.67",
|
|
44
44
|
"@ai-sdk/devtools": "^0.0.15",
|
|
45
|
-
"@ai-sdk/gateway": "3.0.
|
|
46
|
-
"@ai-sdk/google": "^3.0.
|
|
47
|
-
"@ai-sdk/mcp": "^1.0.
|
|
48
|
-
"@ai-sdk/openai": "^3.0.
|
|
49
|
-
"@ai-sdk/openai-compatible": "^2.0.
|
|
50
|
-
"@ai-sdk/provider": "3.0.8",
|
|
51
|
-
"@ai-sdk/react": "^3.0.
|
|
45
|
+
"@ai-sdk/gateway": "^3.0.92",
|
|
46
|
+
"@ai-sdk/google": "^3.0.59",
|
|
47
|
+
"@ai-sdk/mcp": "^1.0.35",
|
|
48
|
+
"@ai-sdk/openai": "^3.0.51",
|
|
49
|
+
"@ai-sdk/openai-compatible": "^2.0.40",
|
|
50
|
+
"@ai-sdk/provider": "^3.0.8",
|
|
51
|
+
"@ai-sdk/react": "^3.0.152",
|
|
52
|
+
"@better-auth/core": "1.5.6",
|
|
53
|
+
"@better-auth/electron": "^1.5.6",
|
|
52
54
|
"@codemirror/lang-javascript": "^6.2.2",
|
|
53
55
|
"@codemirror/lang-python": "^6.1.6",
|
|
54
56
|
"@codemirror/state": "^6.5.0",
|
|
@@ -88,7 +90,7 @@
|
|
|
88
90
|
"@radix-ui/react-toggle": "^1.1.10",
|
|
89
91
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
90
92
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
|
91
|
-
"@streamdown/code": "^1.
|
|
93
|
+
"@streamdown/code": "^1.1.1",
|
|
92
94
|
"@streamdown/math": "^1.0.2",
|
|
93
95
|
"@streamdown/mermaid": "^1.0.2",
|
|
94
96
|
"@t3-oss/env-nextjs": "^0.13.8",
|
|
@@ -104,8 +106,8 @@
|
|
|
104
106
|
"@vercel/otel": "^2.1.0",
|
|
105
107
|
"@vercel/sandbox": "^1.0.2",
|
|
106
108
|
"@vercel/speed-insights": "^1.3.1",
|
|
107
|
-
"ai": "6.0.
|
|
108
|
-
"better-auth": "^1.
|
|
109
|
+
"ai": "^6.0.150",
|
|
110
|
+
"better-auth": "^1.5.6",
|
|
109
111
|
"browser-image-compression": "^2.0.2",
|
|
110
112
|
"class-variance-authority": "^0.7.1",
|
|
111
113
|
"clsx": "^2.1.1",
|
|
@@ -124,7 +126,7 @@
|
|
|
124
126
|
"lucide-react": "0.553.0",
|
|
125
127
|
"motion": "^12.23.24",
|
|
126
128
|
"nanoid": "^5.0.8",
|
|
127
|
-
"next": "16.
|
|
129
|
+
"next": "16.2.0",
|
|
128
130
|
"next-themes": "^0.4.6",
|
|
129
131
|
"nuqs": "^2.8.5",
|
|
130
132
|
"papaparse": "^5.5.2",
|
|
@@ -142,7 +144,7 @@
|
|
|
142
144
|
"server-only": "^0.0.1",
|
|
143
145
|
"shiki": "^3.19.0",
|
|
144
146
|
"sonner": "^2.0.7",
|
|
145
|
-
"streamdown": "^2.
|
|
147
|
+
"streamdown": "^2.5.0",
|
|
146
148
|
"superjson": "^2.2.2",
|
|
147
149
|
"tailwind-merge": "^3.3.1",
|
|
148
150
|
"throttleit": "^2.1.0",
|
|
@@ -162,7 +164,7 @@
|
|
|
162
164
|
}
|
|
163
165
|
},
|
|
164
166
|
"devDependencies": {
|
|
165
|
-
"@biomejs/biome": "
|
|
167
|
+
"@biomejs/biome": "2.4.10",
|
|
166
168
|
"@playwright/test": "^1.50.1",
|
|
167
169
|
"@tailwindcss/postcss": "^4.1.12",
|
|
168
170
|
"@tailwindcss/typography": "^0.5.19",
|
|
@@ -178,8 +180,8 @@
|
|
|
178
180
|
"tailwindcss": "^4.1.12",
|
|
179
181
|
"tsx": "^4.19.1",
|
|
180
182
|
"tw-animate-css": "^1.3.7",
|
|
181
|
-
"typescript": "
|
|
182
|
-
"ultracite": "7.
|
|
183
|
+
"typescript": "6.0.2",
|
|
184
|
+
"ultracite": "7.4.3",
|
|
183
185
|
"vite-tsconfig-paths": "^6.1.1",
|
|
184
186
|
"vitest": "^4.0.0"
|
|
185
187
|
}
|
|
@@ -51,44 +51,25 @@ export default defineConfig({
|
|
|
51
51
|
|
|
52
52
|
/* Configure projects */
|
|
53
53
|
projects: [
|
|
54
|
-
{
|
|
55
|
-
name: "setup:auth",
|
|
56
|
-
testMatch: /auth.setup.e2e.ts/,
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "setup:reasoning",
|
|
60
|
-
testMatch: /reasoning.setup.e2e.ts/,
|
|
61
|
-
dependencies: ["setup:auth"],
|
|
62
|
-
use: {
|
|
63
|
-
...devices["Desktop Chrome"],
|
|
64
|
-
storageState: "playwright/.auth/session.json",
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
54
|
{
|
|
68
55
|
name: "chat",
|
|
69
56
|
testMatch: /chat.e2e.ts/,
|
|
70
|
-
dependencies: ["setup:auth"],
|
|
71
57
|
use: {
|
|
72
58
|
...devices["Desktop Chrome"],
|
|
73
|
-
storageState: "playwright/.auth/session.json",
|
|
74
59
|
},
|
|
75
60
|
},
|
|
76
61
|
{
|
|
77
62
|
name: "reasoning",
|
|
78
63
|
testMatch: /reasoning.e2e.ts/,
|
|
79
|
-
dependencies: ["setup:reasoning"],
|
|
80
64
|
use: {
|
|
81
65
|
...devices["Desktop Chrome"],
|
|
82
|
-
storageState: "playwright/.reasoning/session.json",
|
|
83
66
|
},
|
|
84
67
|
},
|
|
85
68
|
{
|
|
86
69
|
name: "artifacts",
|
|
87
70
|
testMatch: /artifacts.e2e.ts/,
|
|
88
|
-
dependencies: ["setup:auth"],
|
|
89
71
|
use: {
|
|
90
72
|
...devices["Desktop Chrome"],
|
|
91
|
-
storageState: "playwright/.auth/session.json",
|
|
92
73
|
},
|
|
93
74
|
},
|
|
94
75
|
|
|
@@ -13,7 +13,12 @@ import React, {
|
|
|
13
13
|
} from "react";
|
|
14
14
|
import type { LexicalChatInputRef } from "@/components/lexical-chat-input";
|
|
15
15
|
import type { AppModelId } from "@/lib/ai/app-models";
|
|
16
|
-
import
|
|
16
|
+
import {
|
|
17
|
+
type Attachment,
|
|
18
|
+
getPrimarySelectedModelId,
|
|
19
|
+
type SelectedModelValue,
|
|
20
|
+
type UiToolName,
|
|
21
|
+
} from "@/lib/ai/types";
|
|
17
22
|
import { useChatModels } from "./chat-models-provider";
|
|
18
23
|
import { useDefaultModel, useModelChange } from "./default-model-provider";
|
|
19
24
|
|
|
@@ -24,10 +29,12 @@ interface ChatInputContextType {
|
|
|
24
29
|
getInputValue: () => string;
|
|
25
30
|
handleInputChange: (value: string) => void;
|
|
26
31
|
handleModelChange: (modelId: AppModelId) => Promise<void>;
|
|
32
|
+
handleModelSelectionChange: (selection: SelectedModelValue) => Promise<void>;
|
|
27
33
|
handleSubmit: (submitFn: () => void, isEditMode?: boolean) => void;
|
|
28
34
|
isEmpty: boolean;
|
|
29
35
|
isProjectContext: boolean;
|
|
30
36
|
selectedModelId: AppModelId;
|
|
37
|
+
selectedModelSelection: SelectedModelValue;
|
|
31
38
|
selectedTool: UiToolName | null;
|
|
32
39
|
setAttachments: Dispatch<SetStateAction<Attachment[]>>;
|
|
33
40
|
setSelectedTool: Dispatch<SetStateAction<UiToolName | null>>;
|
|
@@ -45,6 +52,7 @@ interface ChatInputProviderProps {
|
|
|
45
52
|
isProjectContext?: boolean;
|
|
46
53
|
localStorageEnabled?: boolean;
|
|
47
54
|
overrideModelId?: AppModelId; // For message editing where we want to use the original model
|
|
55
|
+
overrideModelSelection?: SelectedModelValue; // For message editing with multi-model selection
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export function ChatInputProvider({
|
|
@@ -53,6 +61,7 @@ export function ChatInputProvider({
|
|
|
53
61
|
initialTool = null,
|
|
54
62
|
initialAttachments = [],
|
|
55
63
|
overrideModelId,
|
|
64
|
+
overrideModelSelection,
|
|
56
65
|
localStorageEnabled = true,
|
|
57
66
|
isProjectContext = false,
|
|
58
67
|
}: ChatInputProviderProps) {
|
|
@@ -95,6 +104,10 @@ export function ChatInputProvider({
|
|
|
95
104
|
const [selectedModelId, setSelectedModelId] = useState<AppModelId>(
|
|
96
105
|
overrideModelId || defaultModel
|
|
97
106
|
);
|
|
107
|
+
const [selectedModelSelection, setSelectedModelSelection] =
|
|
108
|
+
useState<SelectedModelValue>(
|
|
109
|
+
overrideModelSelection ?? overrideModelId ?? defaultModel
|
|
110
|
+
);
|
|
98
111
|
|
|
99
112
|
// IMPORTANT: do not read localStorage during initial render.
|
|
100
113
|
// Next SSRs client components; localStorage is client-only and will cause hydration mismatches
|
|
@@ -128,7 +141,7 @@ export function ChatInputProvider({
|
|
|
128
141
|
|
|
129
142
|
const { getModelById } = useChatModels();
|
|
130
143
|
|
|
131
|
-
const
|
|
144
|
+
const persistPrimaryModelChange = useCallback(
|
|
132
145
|
async (modelId: AppModelId) => {
|
|
133
146
|
const modelDef = getModelById(modelId);
|
|
134
147
|
|
|
@@ -146,6 +159,29 @@ export function ChatInputProvider({
|
|
|
146
159
|
[selectedTool, changeModel, getModelById]
|
|
147
160
|
);
|
|
148
161
|
|
|
162
|
+
const handleModelChange = useCallback(
|
|
163
|
+
async (modelId: AppModelId) => {
|
|
164
|
+
setSelectedModelSelection(modelId);
|
|
165
|
+
await persistPrimaryModelChange(modelId);
|
|
166
|
+
},
|
|
167
|
+
[persistPrimaryModelChange]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const handleModelSelectionChange = useCallback(
|
|
171
|
+
async (selection: SelectedModelValue) => {
|
|
172
|
+
setSelectedModelSelection(selection);
|
|
173
|
+
|
|
174
|
+
const primaryModelId = getPrimarySelectedModelId(selection);
|
|
175
|
+
|
|
176
|
+
if (!primaryModelId) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await persistPrimaryModelChange(primaryModelId);
|
|
181
|
+
},
|
|
182
|
+
[persistPrimaryModelChange]
|
|
183
|
+
);
|
|
184
|
+
|
|
149
185
|
const clearInput = useCallback(() => {
|
|
150
186
|
editorRef.current?.clear();
|
|
151
187
|
setLocalStorageInput("");
|
|
@@ -207,7 +243,9 @@ export function ChatInputProvider({
|
|
|
207
243
|
attachments,
|
|
208
244
|
setAttachments,
|
|
209
245
|
selectedModelId,
|
|
246
|
+
selectedModelSelection,
|
|
210
247
|
handleModelChange,
|
|
248
|
+
handleModelSelectionChange,
|
|
211
249
|
getInputValue,
|
|
212
250
|
handleInputChange,
|
|
213
251
|
getInitialInput,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { NextRequest } from "next/server";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
3
|
import { auth } from "@/lib/auth";
|
|
4
|
+
import { config as appConfig } from "@/lib/config";
|
|
5
|
+
import { isPlaywrightTestEnvironment } from "@/lib/constants";
|
|
4
6
|
|
|
5
7
|
function isPublicApiRoute(pathname: string): boolean {
|
|
6
8
|
return (
|
|
@@ -33,7 +35,23 @@ function isPublicPage(pathname: string): boolean {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
function isAuthPage(pathname: string): boolean {
|
|
36
|
-
return
|
|
38
|
+
return (
|
|
39
|
+
pathname.startsWith("/login") ||
|
|
40
|
+
pathname.startsWith("/register") ||
|
|
41
|
+
isDeviceLoginPage(pathname)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isDeviceLoginPage(pathname: string): boolean {
|
|
46
|
+
return appConfig.desktopApp.enabled && pathname.startsWith("/device-login");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getSafeReturnTo(url: URL): string | null {
|
|
50
|
+
const returnTo = url.searchParams.get("returnTo");
|
|
51
|
+
if (!returnTo?.startsWith("/") || returnTo.startsWith("//")) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return returnTo;
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
export async function proxy(req: NextRequest) {
|
|
@@ -44,11 +62,18 @@ export async function proxy(req: NextRequest) {
|
|
|
44
62
|
return;
|
|
45
63
|
}
|
|
46
64
|
|
|
65
|
+
if (isPlaywrightTestEnvironment) {
|
|
66
|
+
// Playwright CI runs the app anonymously and should never reach session I/O.
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
const session = await auth.api.getSession({ headers: req.headers });
|
|
48
71
|
const isLoggedIn = !!session?.user;
|
|
72
|
+
const isDeviceLoginRoute = isDeviceLoginPage(pathname);
|
|
73
|
+
const returnTo = getSafeReturnTo(url);
|
|
49
74
|
|
|
50
|
-
if (isLoggedIn && isAuthPage(pathname)) {
|
|
51
|
-
return NextResponse.redirect(new URL("/", url));
|
|
75
|
+
if (isLoggedIn && isAuthPage(pathname) && !isDeviceLoginRoute) {
|
|
76
|
+
return NextResponse.redirect(new URL(returnTo ?? "/", url));
|
|
52
77
|
}
|
|
53
78
|
|
|
54
79
|
if (isAuthPage(pathname) || isPublicPage(pathname)) {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getMissingRequirement,
|
|
17
17
|
isRequirementSatisfied,
|
|
18
18
|
} from "../lib/config-requirements";
|
|
19
|
+
import { isPlaywrightTestEnvironment } from "../lib/playwright-test-environment";
|
|
19
20
|
|
|
20
21
|
interface ValidationError {
|
|
21
22
|
feature: string;
|
|
@@ -158,6 +159,15 @@ function checkGatewaySnapshot(): string | null {
|
|
|
158
159
|
|
|
159
160
|
function checkEnv(): void {
|
|
160
161
|
const env = process.env;
|
|
162
|
+
if (isPlaywrightTestEnvironment(env)) {
|
|
163
|
+
console.log(
|
|
164
|
+
"✅ Skipping optional environment validation in Playwright test mode"
|
|
165
|
+
);
|
|
166
|
+
// Playwright CI only exercises anonymous flows, so optional feature checks
|
|
167
|
+
// and the gateway snapshot warning stay enforced in non-Playwright builds.
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
161
171
|
const baseUrlError = validateBaseUrl(env);
|
|
162
172
|
const errors = [
|
|
163
173
|
...(baseUrlError ? [baseUrlError] : []),
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
4
|
BRANCH_NAME="${1:-dev-local}"
|
|
5
|
-
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
MONOREPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
7
|
+
if [ -f "$MONOREPO_ROOT/turbo.json" ]; then
|
|
8
|
+
BRANCH_FILE="$MONOREPO_ROOT/.neon-branch"
|
|
9
|
+
else
|
|
10
|
+
BRANCH_FILE="$(cd "$SCRIPT_DIR/.." && pwd)/.neon-branch"
|
|
11
|
+
fi
|
|
6
12
|
|
|
7
13
|
# Check if we're currently on this branch
|
|
8
14
|
if [ -f "$BRANCH_FILE" ] && [ "$(cat "$BRANCH_FILE")" = "$BRANCH_NAME" ]; then
|