@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
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
ToolName,
|
|
18
18
|
ToolOutput,
|
|
19
19
|
} from "@/lib/ai/types";
|
|
20
|
+
import { isSelectedModelValue } from "@/lib/ai/types";
|
|
20
21
|
import { createModuleLogger } from "@/lib/logger";
|
|
21
22
|
import { chatMessageToDbMessage } from "@/lib/message-conversion";
|
|
22
23
|
|
|
@@ -79,6 +80,35 @@ export async function saveChat({
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
export async function saveChatIfNotExists({
|
|
84
|
+
id,
|
|
85
|
+
userId,
|
|
86
|
+
title,
|
|
87
|
+
projectId,
|
|
88
|
+
}: {
|
|
89
|
+
id: string;
|
|
90
|
+
userId: string;
|
|
91
|
+
title: string;
|
|
92
|
+
projectId?: string;
|
|
93
|
+
}) {
|
|
94
|
+
try {
|
|
95
|
+
return await db
|
|
96
|
+
.insert(chat)
|
|
97
|
+
.values({
|
|
98
|
+
id,
|
|
99
|
+
createdAt: new Date(),
|
|
100
|
+
updatedAt: new Date(),
|
|
101
|
+
userId,
|
|
102
|
+
title,
|
|
103
|
+
projectId: projectId ?? null,
|
|
104
|
+
})
|
|
105
|
+
.onConflictDoNothing();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Failed to save chat in database");
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
export async function deleteChatById({ id }: { id: string }) {
|
|
83
113
|
try {
|
|
84
114
|
// Get all messages for this chat to clean up their attachments
|
|
@@ -341,6 +371,43 @@ export async function saveMessage({
|
|
|
341
371
|
}
|
|
342
372
|
}
|
|
343
373
|
|
|
374
|
+
export async function saveMessageIfNotExists({
|
|
375
|
+
id,
|
|
376
|
+
chatId,
|
|
377
|
+
message: chatMessage,
|
|
378
|
+
}: {
|
|
379
|
+
id: string;
|
|
380
|
+
chatId: string;
|
|
381
|
+
message: ChatMessage;
|
|
382
|
+
}) {
|
|
383
|
+
try {
|
|
384
|
+
return await db.transaction(async (tx) => {
|
|
385
|
+
const dbMessage = chatMessageToDbMessage(chatMessage, chatId);
|
|
386
|
+
dbMessage.id = id;
|
|
387
|
+
|
|
388
|
+
const insertedMessages = await tx
|
|
389
|
+
.insert(message)
|
|
390
|
+
.values(dbMessage)
|
|
391
|
+
.onConflictDoNothing()
|
|
392
|
+
.returning({ id: message.id });
|
|
393
|
+
|
|
394
|
+
if (insertedMessages.length === 0) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const mappedDBParts = mapUIMessagePartsToDBParts(chatMessage.parts, id);
|
|
399
|
+
if (mappedDBParts.length > 0) {
|
|
400
|
+
await tx.insert(part).values(mappedDBParts);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await updateChatUpdatedAt({ chatId });
|
|
404
|
+
});
|
|
405
|
+
} catch (error) {
|
|
406
|
+
logger.error({ error, chatId, id }, "saveMessageIfNotExists failed");
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
344
411
|
export async function saveChatMessages({
|
|
345
412
|
messages,
|
|
346
413
|
}: {
|
|
@@ -413,6 +480,11 @@ export async function updateMessage({
|
|
|
413
480
|
attachments: dbMessage.attachments,
|
|
414
481
|
createdAt: dbMessage.createdAt,
|
|
415
482
|
parentMessageId: dbMessage.parentMessageId,
|
|
483
|
+
selectedModel: dbMessage.selectedModel,
|
|
484
|
+
selectedTool: dbMessage.selectedTool,
|
|
485
|
+
parallelGroupId: dbMessage.parallelGroupId,
|
|
486
|
+
parallelIndex: dbMessage.parallelIndex,
|
|
487
|
+
isPrimaryParallel: dbMessage.isPrimaryParallel,
|
|
416
488
|
lastContext: dbMessage.lastContext,
|
|
417
489
|
activeStreamId: dbMessage.activeStreamId,
|
|
418
490
|
})
|
|
@@ -492,8 +564,12 @@ export async function getAllMessagesByChatId({
|
|
|
492
564
|
createdAt: msg.createdAt,
|
|
493
565
|
activeStreamId: msg.activeStreamId,
|
|
494
566
|
parentMessageId: msg.parentMessageId,
|
|
495
|
-
|
|
496
|
-
|
|
567
|
+
parallelGroupId: msg.parallelGroupId,
|
|
568
|
+
parallelIndex: msg.parallelIndex,
|
|
569
|
+
isPrimaryParallel: msg.isPrimaryParallel,
|
|
570
|
+
selectedModel: isSelectedModelValue(msg.selectedModel)
|
|
571
|
+
? msg.selectedModel
|
|
572
|
+
: ("" as ChatMessage["metadata"]["selectedModel"]),
|
|
497
573
|
selectedTool: (msg.selectedTool ||
|
|
498
574
|
undefined) as ChatMessage["metadata"]["selectedTool"],
|
|
499
575
|
usage: msg.lastContext as ChatMessage["metadata"]["usage"],
|
|
@@ -797,8 +873,12 @@ export async function getChatMessageWithPartsById({
|
|
|
797
873
|
createdAt: dbMessage.createdAt,
|
|
798
874
|
activeStreamId: dbMessage.activeStreamId,
|
|
799
875
|
parentMessageId: dbMessage.parentMessageId,
|
|
800
|
-
|
|
801
|
-
|
|
876
|
+
parallelGroupId: dbMessage.parallelGroupId,
|
|
877
|
+
parallelIndex: dbMessage.parallelIndex,
|
|
878
|
+
isPrimaryParallel: dbMessage.isPrimaryParallel,
|
|
879
|
+
selectedModel: isSelectedModelValue(dbMessage.selectedModel)
|
|
880
|
+
? dbMessage.selectedModel
|
|
881
|
+
: ("" as ChatMessage["metadata"]["selectedModel"]),
|
|
802
882
|
selectedTool: (dbMessage.selectedTool ||
|
|
803
883
|
undefined) as ChatMessage["metadata"]["selectedTool"],
|
|
804
884
|
usage: dbMessage.lastContext as ChatMessage["metadata"]["usage"],
|
|
@@ -113,8 +113,11 @@ export const message = pgTable("Message", {
|
|
|
113
113
|
attachments: json("attachments").notNull(),
|
|
114
114
|
createdAt: timestamp("createdAt").notNull(),
|
|
115
115
|
annotations: json("annotations"),
|
|
116
|
-
selectedModel:
|
|
116
|
+
selectedModel: json("selectedModel"),
|
|
117
117
|
selectedTool: varchar("selectedTool", { length: 256 }).default(""),
|
|
118
|
+
parallelGroupId: uuid("parallelGroupId"),
|
|
119
|
+
parallelIndex: integer("parallelIndex"),
|
|
120
|
+
isPrimaryParallel: boolean("isPrimaryParallel"),
|
|
118
121
|
lastContext: json("lastContext"),
|
|
119
122
|
activeStreamId: varchar("activeStreamId", { length: 64 }),
|
|
120
123
|
/** Timestamp when this message's stream was canceled by the user. Null means not canceled. */
|
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
type HeadingTagType,
|
|
9
9
|
QuoteNode,
|
|
10
10
|
} from "@lexical/rich-text";
|
|
11
|
-
import type { LexicalEditor } from "lexical";
|
|
12
|
-
import { $getSelection, $insertNodes } from "lexical";
|
|
11
|
+
import type { EditorState, LexicalEditor } from "lexical";
|
|
12
|
+
import { $getSelection, $insertNodes, type TextNode } from "lexical";
|
|
13
13
|
|
|
14
14
|
// Create initial editor configuration
|
|
15
15
|
export function createEditorConfig() {
|
|
@@ -37,7 +37,7 @@ function _createHeadingTransform(level: number) {
|
|
|
37
37
|
export: null,
|
|
38
38
|
importDOM: null,
|
|
39
39
|
regExp: new RegExp(`^(#{1,${level}})\\s$`),
|
|
40
|
-
replace: (_textNode:
|
|
40
|
+
replace: (_textNode: TextNode) => {
|
|
41
41
|
const selection = $getSelection();
|
|
42
42
|
if (selection) {
|
|
43
43
|
const headingTag = `h${level}` as HeadingTagType;
|
|
@@ -56,7 +56,7 @@ export const handleEditorChange = ({
|
|
|
56
56
|
editor,
|
|
57
57
|
onSaveContent,
|
|
58
58
|
}: {
|
|
59
|
-
editorState:
|
|
59
|
+
editorState: EditorState;
|
|
60
60
|
editor: LexicalEditor;
|
|
61
61
|
onSaveContent: (updatedContent: string, debounce: boolean) => void;
|
|
62
62
|
}) => {
|
|
@@ -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
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
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
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
type ChatMessage,
|
|
6
|
+
isSelectedModelValue,
|
|
7
|
+
type UiToolName,
|
|
8
|
+
} from "./ai/types";
|
|
5
9
|
|
|
6
10
|
// Helper functions for type conversion
|
|
7
11
|
export function dbChatToUIChat(chat: Chat): UIChat {
|
|
@@ -29,7 +33,12 @@ function _dbMessageToChatMessage(message: DBMessage): ChatMessage {
|
|
|
29
33
|
createdAt: message.createdAt,
|
|
30
34
|
activeStreamId: message.activeStreamId,
|
|
31
35
|
parentMessageId: message.parentMessageId,
|
|
32
|
-
|
|
36
|
+
parallelGroupId: message.parallelGroupId,
|
|
37
|
+
parallelIndex: message.parallelIndex,
|
|
38
|
+
isPrimaryParallel: message.isPrimaryParallel,
|
|
39
|
+
selectedModel: isSelectedModelValue(message.selectedModel)
|
|
40
|
+
? message.selectedModel
|
|
41
|
+
: ("" as ModelId),
|
|
33
42
|
selectedTool: (message.selectedTool as UiToolName | null) || undefined,
|
|
34
43
|
usage: message.lastContext as ChatMessage["metadata"]["usage"],
|
|
35
44
|
},
|
|
@@ -66,6 +75,9 @@ export function chatMessageToDbMessage(
|
|
|
66
75
|
parentMessageId,
|
|
67
76
|
selectedModel,
|
|
68
77
|
selectedTool: message.metadata?.selectedTool || null,
|
|
78
|
+
parallelGroupId: message.metadata?.parallelGroupId || null,
|
|
79
|
+
parallelIndex: message.metadata?.parallelIndex ?? null,
|
|
80
|
+
isPrimaryParallel: message.metadata?.isPrimaryParallel ?? null,
|
|
69
81
|
activeStreamId: message.metadata?.activeStreamId || null,
|
|
70
82
|
canceledAt: null,
|
|
71
83
|
};
|
|
@@ -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
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type CustomChatStoreState,
|
|
8
8
|
useCustomChatStoreApi,
|
|
9
9
|
} from "./custom-store-provider";
|
|
10
|
-
import type { MessageSiblingInfo } from "./with-threads";
|
|
10
|
+
import type { MessageSiblingInfo, ParallelGroupInfo } from "./with-threads";
|
|
11
11
|
|
|
12
12
|
function useThreadStore<T>(
|
|
13
13
|
selector: (store: CustomChatStoreState<ChatMessage>) => T,
|
|
@@ -95,3 +95,40 @@ export const useSwitchToSibling = () => {
|
|
|
95
95
|
[store]
|
|
96
96
|
);
|
|
97
97
|
};
|
|
98
|
+
|
|
99
|
+
export function useParallelGroupInfo(
|
|
100
|
+
messageId: string
|
|
101
|
+
): ParallelGroupInfo<ChatMessage> | null {
|
|
102
|
+
return useThreadStore(
|
|
103
|
+
(state) => state.getParallelGroupInfo(messageId),
|
|
104
|
+
(a, b) => {
|
|
105
|
+
if (a === null && b === null) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (a === null || b === null) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
a.parallelGroupId === b.parallelGroupId &&
|
|
115
|
+
a.selectedMessageId === b.selectedMessageId &&
|
|
116
|
+
a.messages.length === b.messages.length &&
|
|
117
|
+
a.messages.every(
|
|
118
|
+
(msg, i) =>
|
|
119
|
+
msg.id === b.messages[i]?.id &&
|
|
120
|
+
msg.metadata?.activeStreamId ===
|
|
121
|
+
b.messages[i]?.metadata?.activeStreamId
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const useSwitchToMessage = () => {
|
|
129
|
+
const store = useCustomChatStoreApi<ChatMessage>();
|
|
130
|
+
return useCallback(
|
|
131
|
+
(messageId: string) => store.getState().switchToMessage(messageId),
|
|
132
|
+
[store]
|
|
133
|
+
);
|
|
134
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import type { StoreState as BaseChatStoreState } from "@ai-sdk-tools/store";
|
|
3
|
+
import { describe, it } from "vitest";
|
|
4
|
+
import { createStore } from "zustand/vanilla";
|
|
5
|
+
import type { ChatMessage } from "../ai/types";
|
|
6
|
+
import { type ThreadAugmentedState, withThreads } from "./with-threads";
|
|
7
|
+
|
|
8
|
+
function createMessage({
|
|
9
|
+
id,
|
|
10
|
+
role,
|
|
11
|
+
createdAt,
|
|
12
|
+
parentMessageId = null,
|
|
13
|
+
parallelGroupId = null,
|
|
14
|
+
parallelIndex = null,
|
|
15
|
+
activeStreamId = null,
|
|
16
|
+
}: {
|
|
17
|
+
id: string;
|
|
18
|
+
role: ChatMessage["role"];
|
|
19
|
+
createdAt: string;
|
|
20
|
+
parentMessageId?: string | null;
|
|
21
|
+
parallelGroupId?: string | null;
|
|
22
|
+
parallelIndex?: number | null;
|
|
23
|
+
activeStreamId?: string | null;
|
|
24
|
+
}): ChatMessage {
|
|
25
|
+
return {
|
|
26
|
+
id,
|
|
27
|
+
role,
|
|
28
|
+
parts: [],
|
|
29
|
+
metadata: {
|
|
30
|
+
createdAt: new Date(createdAt),
|
|
31
|
+
parentMessageId,
|
|
32
|
+
parallelGroupId,
|
|
33
|
+
parallelIndex,
|
|
34
|
+
isPrimaryParallel: parallelIndex === null ? null : parallelIndex === 0,
|
|
35
|
+
selectedModel: "openai/gpt-4o-mini",
|
|
36
|
+
activeStreamId,
|
|
37
|
+
selectedTool: undefined,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createThreadStore(initialMessages: ChatMessage[]) {
|
|
43
|
+
return createStore<ThreadAugmentedState<ChatMessage>>()(
|
|
44
|
+
withThreads<ChatMessage, BaseChatStoreState<ChatMessage>>(
|
|
45
|
+
(set) =>
|
|
46
|
+
({
|
|
47
|
+
messages: initialMessages,
|
|
48
|
+
setMessages: (messages: ChatMessage[]) => set({ messages }),
|
|
49
|
+
}) as BaseChatStoreState<ChatMessage>
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("withThreads", () => {
|
|
55
|
+
it("preserves local-only optimistic branch nodes across server syncs", () => {
|
|
56
|
+
const rootUser = createMessage({
|
|
57
|
+
id: "user-root",
|
|
58
|
+
role: "user",
|
|
59
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
60
|
+
});
|
|
61
|
+
const branchA = createMessage({
|
|
62
|
+
id: "assistant-a",
|
|
63
|
+
role: "assistant",
|
|
64
|
+
createdAt: "2024-01-01T00:00:01.000Z",
|
|
65
|
+
parentMessageId: rootUser.id,
|
|
66
|
+
parallelGroupId: "group-root",
|
|
67
|
+
parallelIndex: 0,
|
|
68
|
+
});
|
|
69
|
+
const branchB = createMessage({
|
|
70
|
+
id: "assistant-b",
|
|
71
|
+
role: "assistant",
|
|
72
|
+
createdAt: "2024-01-01T00:00:02.000Z",
|
|
73
|
+
parentMessageId: rootUser.id,
|
|
74
|
+
parallelGroupId: "group-root",
|
|
75
|
+
parallelIndex: 1,
|
|
76
|
+
});
|
|
77
|
+
const nestedUser = createMessage({
|
|
78
|
+
id: "user-nested",
|
|
79
|
+
role: "user",
|
|
80
|
+
createdAt: "2024-01-01T00:00:03.000Z",
|
|
81
|
+
parentMessageId: branchA.id,
|
|
82
|
+
});
|
|
83
|
+
const nestedBranchA = createMessage({
|
|
84
|
+
id: "assistant-nested-a",
|
|
85
|
+
role: "assistant",
|
|
86
|
+
createdAt: "2024-01-01T00:00:04.000Z",
|
|
87
|
+
parentMessageId: nestedUser.id,
|
|
88
|
+
parallelGroupId: "group-nested",
|
|
89
|
+
parallelIndex: 0,
|
|
90
|
+
activeStreamId: "pending:assistant-nested-a",
|
|
91
|
+
});
|
|
92
|
+
const nestedBranchB = createMessage({
|
|
93
|
+
id: "assistant-nested-b",
|
|
94
|
+
role: "assistant",
|
|
95
|
+
createdAt: "2024-01-01T00:00:05.000Z",
|
|
96
|
+
parentMessageId: nestedUser.id,
|
|
97
|
+
parallelGroupId: "group-nested",
|
|
98
|
+
parallelIndex: 1,
|
|
99
|
+
activeStreamId: "pending:assistant-nested-b",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const store = createThreadStore([
|
|
103
|
+
rootUser,
|
|
104
|
+
branchA,
|
|
105
|
+
nestedUser,
|
|
106
|
+
nestedBranchA,
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
store.getState().addMessageToTree(branchB);
|
|
110
|
+
store.getState().addMessageToTree(nestedBranchB);
|
|
111
|
+
|
|
112
|
+
store.getState().setMessagesWithEpoch([rootUser, branchB]);
|
|
113
|
+
store.getState().setAllMessages([rootUser, branchA, branchB]);
|
|
114
|
+
|
|
115
|
+
const allMessageIds = store
|
|
116
|
+
.getState()
|
|
117
|
+
.allMessages.map((message: ChatMessage) => message.id);
|
|
118
|
+
assert.deepEqual(allMessageIds, [
|
|
119
|
+
"user-root",
|
|
120
|
+
"assistant-a",
|
|
121
|
+
"assistant-b",
|
|
122
|
+
"user-nested",
|
|
123
|
+
"assistant-nested-a",
|
|
124
|
+
"assistant-nested-b",
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const restoredThread = store.getState().switchToMessage(branchA.id);
|
|
128
|
+
assert.deepEqual(
|
|
129
|
+
restoredThread?.map((message: ChatMessage) => message.id),
|
|
130
|
+
["user-root", "assistant-a", "user-nested", "assistant-nested-b"]
|
|
131
|
+
);
|
|
132
|
+
assert.equal(
|
|
133
|
+
restoredThread?.at(-1)?.metadata.activeStreamId,
|
|
134
|
+
"pending:assistant-nested-b"
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|