@chat-js/cli 0.4.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 +1163 -959
- 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/route.ts +13 -5
- 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 +144 -141
- 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 +3 -8
- 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 +1 -1
- 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 +4 -5
- package/templates/chat-app/components/model-selector.tsx +661 -655
- package/templates/chat-app/components/multimodal-input.tsx +13 -10
- package/templates/chat-app/components/parallel-response-cards.tsx +53 -35
- 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 +13 -9
- 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 +1 -1
- 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 +135 -134
- 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/use-artifact.tsx +13 -13
- 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/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 +13 -1
- package/templates/chat-app/lib/constants.ts +3 -4
- package/templates/chat-app/lib/db/migrations/meta/0044_snapshot.json +42 -129
- package/templates/chat-app/lib/db/migrations/meta/_journal.json +1 -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 +1 -1
- 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 +2 -1
- package/templates/chat-app/lib/stores/with-threads.test.ts +1 -1
- package/templates/chat-app/lib/stores/with-threads.ts +5 -6
- package/templates/chat-app/lib/stores/with-tracing.ts +1 -1
- package/templates/chat-app/lib/thread-utils.ts +19 -21
- 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 +1 -1
- package/templates/chat-app/proxy.ts +28 -3
- package/templates/chat-app/scripts/check-env.ts +10 -0
- 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/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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { config } from "@/lib/config";
|
|
2
|
+
import type { SocialAuthSignInOptions } from "@/lib/social-auth";
|
|
3
|
+
|
|
4
|
+
export const ELECTRON_AUTH_CLIENT_ID = "electron";
|
|
5
|
+
export const ELECTRON_AUTH_COOKIE_PREFIX = "better-auth";
|
|
6
|
+
export const ELECTRON_AUTH_CALLBACK_PATH = "/auth/callback";
|
|
7
|
+
export const ELECTRON_APP_SCHEME = config.appPrefix;
|
|
8
|
+
// @better-auth/electron uses `${scheme}:/...` for its synthetic Origin header
|
|
9
|
+
// and deep-link callback URLs. Keep the legacy `scheme://` form alongside it so
|
|
10
|
+
// existing packaged registrations continue to validate too.
|
|
11
|
+
export const ELECTRON_TRUSTED_ORIGINS = [
|
|
12
|
+
`${ELECTRON_APP_SCHEME}:/`,
|
|
13
|
+
`${ELECTRON_APP_SCHEME}://`,
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export function isDesktopAppEnabled(): boolean {
|
|
17
|
+
return config.desktopApp.enabled;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isElectronRenderer(): boolean {
|
|
21
|
+
return (
|
|
22
|
+
isDesktopAppEnabled() &&
|
|
23
|
+
typeof window !== "undefined" &&
|
|
24
|
+
typeof window.requestAuth === "function"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type SearchParamValue = string | string[] | undefined;
|
|
29
|
+
|
|
30
|
+
export function toSearchParamRecord(
|
|
31
|
+
searchParams: Record<string, SearchParamValue>
|
|
32
|
+
): Record<string, string> {
|
|
33
|
+
const query: Record<string, string> = {};
|
|
34
|
+
|
|
35
|
+
for (const [key, value] of Object.entries(searchParams)) {
|
|
36
|
+
if (typeof value === "string") {
|
|
37
|
+
query[key] = value;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(value) && value[0]) {
|
|
42
|
+
query[key] = value[0];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return query;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildAuthPageHref(
|
|
50
|
+
pathname: string,
|
|
51
|
+
searchParams: Record<string, SearchParamValue>
|
|
52
|
+
): string {
|
|
53
|
+
const query = new URLSearchParams(
|
|
54
|
+
toSearchParamRecord(searchParams)
|
|
55
|
+
).toString();
|
|
56
|
+
return query ? `${pathname}?${query}` : pathname;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isElectronTransferQuery(
|
|
60
|
+
query: Record<string, string>
|
|
61
|
+
): boolean {
|
|
62
|
+
return query.client_id === ELECTRON_AUTH_CLIENT_ID;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildSocialAuthRequest(
|
|
66
|
+
query: Record<string, string>,
|
|
67
|
+
origin?: string
|
|
68
|
+
): {
|
|
69
|
+
callbackURL?: string;
|
|
70
|
+
onRedirectToUrl?: (url: string) => void;
|
|
71
|
+
signInOptions?: SocialAuthSignInOptions;
|
|
72
|
+
} {
|
|
73
|
+
const isElectronTransfer =
|
|
74
|
+
isDesktopAppEnabled() && isElectronTransferQuery(query);
|
|
75
|
+
const deviceLoginCallbackURL = origin
|
|
76
|
+
? new URL("/device-login", origin).toString()
|
|
77
|
+
: "/device-login";
|
|
78
|
+
|
|
79
|
+
if (isElectronTransfer) {
|
|
80
|
+
return {
|
|
81
|
+
callbackURL: deviceLoginCallbackURL,
|
|
82
|
+
onRedirectToUrl: (url: string) => {
|
|
83
|
+
globalThis.location?.assign(url);
|
|
84
|
+
},
|
|
85
|
+
signInOptions: {
|
|
86
|
+
disableRedirect: true,
|
|
87
|
+
errorCallbackURL: deviceLoginCallbackURL,
|
|
88
|
+
newUserCallbackURL: deviceLoginCallbackURL,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
callbackURL: query.returnTo,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { isPlaywrightTestEnvironment } from "@/lib/playwright-test-environment";
|
|
3
|
+
|
|
4
|
+
const isPlaywrightTestEnvironmentEnabled = isPlaywrightTestEnvironment(
|
|
5
|
+
process.env
|
|
6
|
+
);
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Server environment variable schemas with descriptions.
|
|
@@ -12,10 +17,23 @@ import { z } from "zod";
|
|
|
12
17
|
*/
|
|
13
18
|
export const serverEnvSchema = {
|
|
14
19
|
// Required core
|
|
15
|
-
DATABASE_URL: z
|
|
20
|
+
DATABASE_URL: z
|
|
21
|
+
.preprocess(
|
|
22
|
+
(value) =>
|
|
23
|
+
isPlaywrightTestEnvironmentEnabled && (value == null || value === "")
|
|
24
|
+
? "postgres://postgres:postgres@127.0.0.1:5432/playwright"
|
|
25
|
+
: value,
|
|
26
|
+
z.string().min(1)
|
|
27
|
+
)
|
|
28
|
+
.describe("Postgres connection string"),
|
|
16
29
|
AUTH_SECRET: z
|
|
17
|
-
.
|
|
18
|
-
|
|
30
|
+
.preprocess(
|
|
31
|
+
(value) =>
|
|
32
|
+
isPlaywrightTestEnvironmentEnabled && (value == null || value === "")
|
|
33
|
+
? "playwright-test-auth-secret"
|
|
34
|
+
: value,
|
|
35
|
+
z.string().min(1)
|
|
36
|
+
)
|
|
19
37
|
.describe("NextAuth.js secret for signing session tokens"),
|
|
20
38
|
|
|
21
39
|
// Optional blob storage (enable in chat.config.ts)
|
|
@@ -102,8 +120,19 @@ export const serverEnvSchema = {
|
|
|
102
120
|
.describe("Vercel API token for sandbox (non-Vercel deployments)"),
|
|
103
121
|
VERCEL_SANDBOX_RUNTIME: z
|
|
104
122
|
.string()
|
|
123
|
+
.min(1)
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("Legacy default Vercel sandbox runtime identifier for Python"),
|
|
126
|
+
VERCEL_SANDBOX_RUNTIME_PYTHON: z
|
|
127
|
+
.string()
|
|
128
|
+
.min(1)
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("Vercel sandbox runtime identifier for Python execution"),
|
|
131
|
+
VERCEL_SANDBOX_RUNTIME_JAVASCRIPT: z
|
|
132
|
+
.string()
|
|
133
|
+
.min(1)
|
|
105
134
|
.optional()
|
|
106
|
-
.describe("Vercel sandbox runtime identifier"),
|
|
135
|
+
.describe("Vercel sandbox runtime identifier for JavaScript execution"),
|
|
107
136
|
|
|
108
137
|
// App URL (for non-Vercel deployments) - full URL including https://
|
|
109
138
|
APP_URL: z
|
|
@@ -2,8 +2,8 @@ import type { ModelId } from "@/lib/ai/app-models";
|
|
|
2
2
|
import type { Chat, DBMessage } from "@/lib/db/schema";
|
|
3
3
|
import type { UIChat } from "@/lib/types/ui-chat";
|
|
4
4
|
import {
|
|
5
|
-
isSelectedModelValue,
|
|
6
5
|
type ChatMessage,
|
|
6
|
+
isSelectedModelValue,
|
|
7
7
|
type UiToolName,
|
|
8
8
|
} from "./ai/types";
|
|
9
9
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function isEnabledFlag(value: string | undefined): boolean {
|
|
2
|
+
if (!value) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
7
|
+
return !["0", "false", "no", "off"].includes(normalizedValue);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isPlaywrightTestEnvironment(
|
|
11
|
+
env: NodeJS.ProcessEnv = process.env
|
|
12
|
+
): boolean {
|
|
13
|
+
return Boolean(
|
|
14
|
+
env.PLAYWRIGHT_TEST_BASE_URL ||
|
|
15
|
+
isEnabledFlag(env.PLAYWRIGHT) ||
|
|
16
|
+
isEnabledFlag(env.CI_PLAYWRIGHT)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -117,7 +117,8 @@ export function useParallelGroupInfo(
|
|
|
117
117
|
a.messages.every(
|
|
118
118
|
(msg, i) =>
|
|
119
119
|
msg.id === b.messages[i]?.id &&
|
|
120
|
-
msg.metadata?.activeStreamId ===
|
|
120
|
+
msg.metadata?.activeStreamId ===
|
|
121
|
+
b.messages[i]?.metadata?.activeStreamId
|
|
121
122
|
)
|
|
122
123
|
);
|
|
123
124
|
}
|
|
@@ -3,7 +3,7 @@ import type { StoreState as BaseChatStoreState } from "@ai-sdk-tools/store";
|
|
|
3
3
|
import { describe, it } from "vitest";
|
|
4
4
|
import { createStore } from "zustand/vanilla";
|
|
5
5
|
import type { ChatMessage } from "../ai/types";
|
|
6
|
-
import {
|
|
6
|
+
import { type ThreadAugmentedState, withThreads } from "./with-threads";
|
|
7
7
|
|
|
8
8
|
function createMessage({
|
|
9
9
|
id,
|
|
@@ -177,11 +177,11 @@ export const withThreads =
|
|
|
177
177
|
set((state) => {
|
|
178
178
|
const idx = state.allMessages.findIndex((m) => m.id === message.id);
|
|
179
179
|
let next: UI_MESSAGE[];
|
|
180
|
-
if (idx
|
|
180
|
+
if (idx === -1) {
|
|
181
|
+
next = [...state.allMessages, message];
|
|
182
|
+
} else {
|
|
181
183
|
next = [...state.allMessages];
|
|
182
184
|
next[idx] = message;
|
|
183
|
-
} else {
|
|
184
|
-
next = [...state.allMessages, message];
|
|
185
185
|
}
|
|
186
186
|
return { ...state, allMessages: next, childrenMap: rebuildMap(next) };
|
|
187
187
|
});
|
|
@@ -254,9 +254,8 @@ export const withThreads =
|
|
|
254
254
|
|
|
255
255
|
const visibleMessageIds = new Set(state.messages.map((m) => m.id));
|
|
256
256
|
const selectedMessageId =
|
|
257
|
-
groupMessages.find((candidate) =>
|
|
258
|
-
|
|
259
|
-
)?.id ?? null;
|
|
257
|
+
groupMessages.find((candidate) => visibleMessageIds.has(candidate.id))
|
|
258
|
+
?.id ?? null;
|
|
260
259
|
|
|
261
260
|
return {
|
|
262
261
|
messages: groupMessages,
|
|
@@ -104,28 +104,26 @@ export function buildChildrenMap<T extends MessageNode>(
|
|
|
104
104
|
map.get(parentId)?.push(message);
|
|
105
105
|
}
|
|
106
106
|
for (const siblings of map.values()) {
|
|
107
|
-
siblings.sort(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return aParallelIndex - bParallelIndex;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
toTimestamp(a.metadata?.createdAt) - toTimestamp(b.metadata?.createdAt)
|
|
126
|
-
);
|
|
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;
|
|
127
121
|
}
|
|
128
|
-
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
toTimestamp(a.metadata?.createdAt) - toTimestamp(b.metadata?.createdAt)
|
|
125
|
+
);
|
|
126
|
+
});
|
|
129
127
|
}
|
|
130
128
|
return map;
|
|
131
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
|
|
|
@@ -14,8 +14,8 @@ import React, {
|
|
|
14
14
|
import type { LexicalChatInputRef } from "@/components/lexical-chat-input";
|
|
15
15
|
import type { AppModelId } from "@/lib/ai/app-models";
|
|
16
16
|
import {
|
|
17
|
-
getPrimarySelectedModelId,
|
|
18
17
|
type Attachment,
|
|
18
|
+
getPrimarySelectedModelId,
|
|
19
19
|
type SelectedModelValue,
|
|
20
20
|
type UiToolName,
|
|
21
21
|
} from "@/lib/ai/types";
|
|
@@ -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,6 +2,7 @@ import "server-only"; // <-- ensure this file cannot be imported from the client
|
|
|
2
2
|
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
|
3
3
|
import {
|
|
4
4
|
createTRPCOptionsProxy,
|
|
5
|
+
type ResolverDef,
|
|
5
6
|
type TRPCQueryOptions,
|
|
6
7
|
} from "@trpc/tanstack-react-query";
|
|
7
8
|
import { cache } from "react";
|
|
@@ -28,12 +29,16 @@ export function HydrateClient(props: { children: React.ReactNode }) {
|
|
|
28
29
|
);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
export function prefetch<T extends ReturnType<TRPCQueryOptions<
|
|
32
|
+
export function prefetch<T extends ReturnType<TRPCQueryOptions<ResolverDef>>>(
|
|
32
33
|
queryOptions: T
|
|
33
34
|
) {
|
|
34
35
|
const queryClient = getQueryClient();
|
|
35
36
|
if (queryOptions.queryKey[1]?.type === "infinite") {
|
|
36
|
-
queryClient.prefetchInfiniteQuery(
|
|
37
|
+
queryClient.prefetchInfiniteQuery(
|
|
38
|
+
queryOptions as unknown as Parameters<
|
|
39
|
+
typeof queryClient.prefetchInfiniteQuery
|
|
40
|
+
>[0]
|
|
41
|
+
);
|
|
37
42
|
} else {
|
|
38
43
|
queryClient.prefetchQuery(queryOptions);
|
|
39
44
|
}
|
|
@@ -7,15 +7,5 @@
|
|
|
7
7
|
"path": "/api/cron/cleanup",
|
|
8
8
|
"schedule": "0 2 * * *"
|
|
9
9
|
}
|
|
10
|
-
],
|
|
11
|
-
"rewrites": [
|
|
12
|
-
{
|
|
13
|
-
"source": "/docs",
|
|
14
|
-
"destination": "https://chatjs.mintlify.dev/docs"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"source": "/docs/:match*",
|
|
18
|
-
"destination": "https://chatjs.mintlify.dev/docs/:match*"
|
|
19
|
-
}
|
|
20
10
|
]
|
|
21
11
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @chatjs/electron
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#136](https://github.com/FranciscoMoretti/chat-js/pull/136) [`6b1458c`](https://github.com/FranciscoMoretti/chat-js/commit/6b1458cb071fe4e67bca99fa86ff55bfb4852c64) Thanks [@FranciscoMoretti](https://github.com/FranciscoMoretti)! - Add a PR-driven desktop release flow for the Electron app. Desktop releases now follow the same Changesets version-bump pattern as the CLI: merge the generated version PR, then publish macOS and Windows installers to GitHub Releases using stable download asset names for the site.
|