@chat-js/cli 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1548 -969
- package/package.json +4 -3
- 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 +22 -19
- 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 +152 -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
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import type { DataUIPart } from "ai";
|
|
2
3
|
import { type Dispatch, type SetStateAction, useEffect, useRef } from "react";
|
|
4
|
+
import type { ArtifactMetadata } from "@/components/create-artifact";
|
|
3
5
|
import { useArtifact } from "@/hooks/use-artifact";
|
|
4
|
-
import type { UiToolName } from "@/lib/ai/types";
|
|
6
|
+
import type { CustomUIDataTypes, UiToolName } from "@/lib/ai/types";
|
|
7
|
+
import {
|
|
8
|
+
codeArtifact,
|
|
9
|
+
getCodeArtifactMetadata,
|
|
10
|
+
} from "@/lib/artifacts/code/client";
|
|
11
|
+
import {
|
|
12
|
+
getSheetArtifactMetadata,
|
|
13
|
+
sheetArtifact,
|
|
14
|
+
} from "@/lib/artifacts/sheet/client";
|
|
15
|
+
import { textArtifact } from "@/lib/artifacts/text/client";
|
|
5
16
|
import { useChatId } from "@/providers/chat-id-provider";
|
|
6
17
|
import { useChatInput } from "@/providers/chat-input-provider";
|
|
7
18
|
import { useSession } from "@/providers/session-provider";
|
|
8
|
-
import { artifactDefinitions } from "./artifact-panel";
|
|
9
19
|
import { useDataStream } from "./data-stream-provider";
|
|
10
20
|
|
|
21
|
+
function createTypedMetadataSetter<M extends ArtifactMetadata>(
|
|
22
|
+
setMetadata: Dispatch<SetStateAction<ArtifactMetadata>>,
|
|
23
|
+
coerce: (metadata: ArtifactMetadata) => M
|
|
24
|
+
): Dispatch<SetStateAction<M>> {
|
|
25
|
+
return (value) => {
|
|
26
|
+
setMetadata((current) => {
|
|
27
|
+
const typedCurrent = coerce(current);
|
|
28
|
+
return typeof value === "function" ? value(typedCurrent) : value;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
function handleResearchUpdate({
|
|
12
34
|
delta,
|
|
13
35
|
setSelectedTool,
|
|
14
36
|
}: {
|
|
15
|
-
delta:
|
|
37
|
+
delta: DataUIPart<CustomUIDataTypes>;
|
|
16
38
|
setSelectedTool: Dispatch<SetStateAction<UiToolName | null>>;
|
|
17
39
|
}): void {
|
|
18
40
|
if (delta.type === "data-researchUpdate") {
|
|
19
|
-
|
|
20
|
-
const update: any = (delta as any).data;
|
|
41
|
+
const update = delta.data;
|
|
21
42
|
if (update?.type === "completed") {
|
|
22
43
|
setSelectedTool((current) =>
|
|
23
44
|
current === "deepResearch" ? null : current
|
|
@@ -36,21 +57,41 @@ function processArtifactStreamPart({
|
|
|
36
57
|
setArtifact,
|
|
37
58
|
setMetadata,
|
|
38
59
|
}: {
|
|
39
|
-
delta:
|
|
60
|
+
delta: DataUIPart<CustomUIDataTypes>;
|
|
40
61
|
artifact: ReturnType<typeof useArtifact>["artifact"];
|
|
41
62
|
setArtifact: ReturnType<typeof useArtifact>["setArtifact"];
|
|
42
63
|
setMetadata: ReturnType<typeof useArtifact>["setMetadata"];
|
|
43
64
|
}): void {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
switch (artifact.kind) {
|
|
66
|
+
case "code":
|
|
67
|
+
codeArtifact.onStreamPart?.({
|
|
68
|
+
streamPart: delta,
|
|
69
|
+
setArtifact,
|
|
70
|
+
setMetadata: createTypedMetadataSetter(
|
|
71
|
+
setMetadata,
|
|
72
|
+
getCodeArtifactMetadata
|
|
73
|
+
),
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
case "sheet":
|
|
77
|
+
sheetArtifact.onStreamPart?.({
|
|
78
|
+
streamPart: delta,
|
|
79
|
+
setArtifact,
|
|
80
|
+
setMetadata: createTypedMetadataSetter(
|
|
81
|
+
setMetadata,
|
|
82
|
+
getSheetArtifactMetadata
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
case "text":
|
|
87
|
+
textArtifact.onStreamPart?.({
|
|
88
|
+
streamPart: delta,
|
|
89
|
+
setArtifact,
|
|
90
|
+
setMetadata,
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
break;
|
|
54
95
|
}
|
|
55
96
|
}
|
|
56
97
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CheckCircle2, LoaderCircle } from "lucide-react";
|
|
4
|
+
import { usePathname, useSearchParams } from "next/navigation";
|
|
5
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from "@/components/ui/card";
|
|
14
|
+
import authClient from "@/lib/auth-client";
|
|
15
|
+
import { config } from "@/lib/config";
|
|
16
|
+
import { isElectronTransferQuery } from "@/lib/electron-auth";
|
|
17
|
+
|
|
18
|
+
type DeviceLoginState = "checking-session" | "transferring" | "waiting-for-app";
|
|
19
|
+
|
|
20
|
+
const DEVICE_LOGIN_COMPLETED_PARAM = "done";
|
|
21
|
+
|
|
22
|
+
export function DeviceLoginPage() {
|
|
23
|
+
const pathname = usePathname();
|
|
24
|
+
const searchParams = useSearchParams();
|
|
25
|
+
const [state, setState] = useState<DeviceLoginState>("checking-session");
|
|
26
|
+
const transferStartedRef = useRef(false);
|
|
27
|
+
|
|
28
|
+
const query = useMemo(
|
|
29
|
+
() => Object.fromEntries(searchParams.entries()),
|
|
30
|
+
[searchParams]
|
|
31
|
+
);
|
|
32
|
+
const isCompletedView =
|
|
33
|
+
searchParams.get(DEVICE_LOGIN_COMPLETED_PARAM) === "1";
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (isCompletedView) {
|
|
37
|
+
setState("waiting-for-app");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isElectronTransferQuery(query)) {
|
|
42
|
+
setState("waiting-for-app");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let cancelled = false;
|
|
47
|
+
|
|
48
|
+
const checkSession = async () => {
|
|
49
|
+
const { data: session } = await authClient.getSession();
|
|
50
|
+
|
|
51
|
+
if (cancelled || transferStartedRef.current) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!session?.user) {
|
|
56
|
+
setState("waiting-for-app");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
transferStartedRef.current = true;
|
|
61
|
+
setState("transferring");
|
|
62
|
+
|
|
63
|
+
await authClient.electron.transferUser({
|
|
64
|
+
fetchOptions: {
|
|
65
|
+
query,
|
|
66
|
+
onSuccess: () => {
|
|
67
|
+
window.history.replaceState(
|
|
68
|
+
{},
|
|
69
|
+
"",
|
|
70
|
+
`${pathname}?${DEVICE_LOGIN_COMPLETED_PARAM}=1`
|
|
71
|
+
);
|
|
72
|
+
setState("waiting-for-app");
|
|
73
|
+
},
|
|
74
|
+
onError: () => {
|
|
75
|
+
transferStartedRef.current = false;
|
|
76
|
+
setState("waiting-for-app");
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
checkSession().catch(() => {
|
|
83
|
+
if (cancelled) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
transferStartedRef.current = false;
|
|
88
|
+
setState("waiting-for-app");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
cancelled = true;
|
|
93
|
+
};
|
|
94
|
+
}, [isCompletedView, pathname, query]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<DeviceAuthScreen
|
|
98
|
+
onRetry={() => {
|
|
99
|
+
transferStartedRef.current = false;
|
|
100
|
+
setState("transferring");
|
|
101
|
+
authClient.electron
|
|
102
|
+
.transferUser({
|
|
103
|
+
fetchOptions: {
|
|
104
|
+
query,
|
|
105
|
+
onSuccess: () => {
|
|
106
|
+
window.history.replaceState(
|
|
107
|
+
{},
|
|
108
|
+
"",
|
|
109
|
+
`${pathname}?${DEVICE_LOGIN_COMPLETED_PARAM}=1`
|
|
110
|
+
);
|
|
111
|
+
setState("waiting-for-app");
|
|
112
|
+
},
|
|
113
|
+
onError: () => {
|
|
114
|
+
transferStartedRef.current = false;
|
|
115
|
+
setState("waiting-for-app");
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
.catch(() => {
|
|
120
|
+
transferStartedRef.current = false;
|
|
121
|
+
setState("waiting-for-app");
|
|
122
|
+
});
|
|
123
|
+
}}
|
|
124
|
+
state={state}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function DeviceAuthScreen({
|
|
130
|
+
state,
|
|
131
|
+
onRetry,
|
|
132
|
+
}: {
|
|
133
|
+
state: "checking-session" | "transferring" | "waiting-for-app";
|
|
134
|
+
onRetry: () => void;
|
|
135
|
+
}) {
|
|
136
|
+
const isLoading = state === "checking-session" || state === "transferring";
|
|
137
|
+
let title = "You're signed in";
|
|
138
|
+
|
|
139
|
+
if (isLoading) {
|
|
140
|
+
title =
|
|
141
|
+
state === "checking-session"
|
|
142
|
+
? "Checking your session..."
|
|
143
|
+
: "Opening the desktop app...";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="flex min-h-dvh w-screen items-center justify-center bg-background">
|
|
148
|
+
<div className="w-full max-w-sm px-6">
|
|
149
|
+
<Card>
|
|
150
|
+
<CardHeader className="text-center">
|
|
151
|
+
<div className="mb-2 flex justify-center">
|
|
152
|
+
{isLoading ? (
|
|
153
|
+
<LoaderCircle className="size-8 animate-spin text-muted-foreground" />
|
|
154
|
+
) : (
|
|
155
|
+
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-foreground text-background">
|
|
156
|
+
<CheckCircle2 className="size-7" />
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
<CardTitle className="text-xl">{title}</CardTitle>
|
|
161
|
+
{!isLoading && (
|
|
162
|
+
<CardDescription>
|
|
163
|
+
You can close this tab and return to {config.appName}.
|
|
164
|
+
</CardDescription>
|
|
165
|
+
)}
|
|
166
|
+
</CardHeader>
|
|
167
|
+
{!isLoading && (
|
|
168
|
+
<CardContent className="text-center">
|
|
169
|
+
<div className="mb-4">
|
|
170
|
+
<Button asChild className="w-full" variant="outline">
|
|
171
|
+
<a href="/">Continue on web</a>
|
|
172
|
+
</Button>
|
|
173
|
+
</div>
|
|
174
|
+
<p className="text-muted-foreground/60 text-xs">
|
|
175
|
+
Didn't open?{" "}
|
|
176
|
+
<Button
|
|
177
|
+
className="h-auto p-0 text-muted-foreground/60 text-xs underline underline-offset-2 hover:text-muted-foreground hover:no-underline"
|
|
178
|
+
onClick={onRetry}
|
|
179
|
+
type="button"
|
|
180
|
+
variant="link"
|
|
181
|
+
>
|
|
182
|
+
Try again
|
|
183
|
+
</Button>
|
|
184
|
+
</p>
|
|
185
|
+
</CardContent>
|
|
186
|
+
)}
|
|
187
|
+
</Card>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
$getRoot,
|
|
10
10
|
type EditorConfig,
|
|
11
11
|
type LexicalEditor,
|
|
12
|
+
type SerializedTextNode,
|
|
12
13
|
TextNode,
|
|
13
14
|
} from "lexical";
|
|
14
15
|
import { useEffect } from "react";
|
|
@@ -23,6 +24,11 @@ const DiffType = {
|
|
|
23
24
|
|
|
24
25
|
// Define diff types
|
|
25
26
|
type DiffTypeValue = (typeof DiffType)[keyof typeof DiffType];
|
|
27
|
+
type SerializedDiffTextNode = SerializedTextNode & {
|
|
28
|
+
diffType?: DiffTypeValue;
|
|
29
|
+
type: "diff-text";
|
|
30
|
+
version: 1;
|
|
31
|
+
};
|
|
26
32
|
|
|
27
33
|
// Custom diff text node that supports styling
|
|
28
34
|
class DiffTextNode extends TextNode {
|
|
@@ -38,7 +44,7 @@ class DiffTextNode extends TextNode {
|
|
|
38
44
|
return newNode;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
static importJSON(serializedNode:
|
|
47
|
+
static importJSON(serializedNode: SerializedDiffTextNode): DiffTextNode {
|
|
42
48
|
const { text, diffType } = serializedNode;
|
|
43
49
|
const node = new DiffTextNode(text);
|
|
44
50
|
if (diffType !== undefined) {
|
|
@@ -47,7 +53,7 @@ class DiffTextNode extends TextNode {
|
|
|
47
53
|
return node;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
exportJSON():
|
|
56
|
+
exportJSON(): SerializedDiffTextNode {
|
|
51
57
|
return {
|
|
52
58
|
...super.exportJSON(),
|
|
53
59
|
diffType: this.__diffType,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AlertCircle, LoaderCircle } from "lucide-react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
import { Button } from "@/components/ui/button";
|
|
8
|
+
import authClient from "@/lib/auth-client";
|
|
9
|
+
import { config } from "@/lib/config";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handles the electron auth redirect after OAuth completes in the browser.
|
|
13
|
+
* When the user finishes OAuth, `ensureElectronRedirect` detects the
|
|
14
|
+
* electron redirect cookie and sends the user back to the Electron app
|
|
15
|
+
* via deep link.
|
|
16
|
+
*
|
|
17
|
+
* Mount this in the root layout so it runs on every page.
|
|
18
|
+
*/
|
|
19
|
+
export function ElectronAuthHandler() {
|
|
20
|
+
const isDesktopAppEnabled = config.desktopApp.enabled;
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
const [authState, setAuthState] = useState<ElectronRendererAuthState>({
|
|
23
|
+
status: "idle",
|
|
24
|
+
message: null,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!isDesktopAppEnabled) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const id = authClient.ensureElectronRedirect();
|
|
33
|
+
return () => clearInterval(id);
|
|
34
|
+
}, [isDesktopAppEnabled]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!isDesktopAppEnabled) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof window.requestAuth !== "function") {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
typeof window.onAuthenticated !== "function" ||
|
|
47
|
+
typeof window.onUserUpdated !== "function" ||
|
|
48
|
+
typeof window.onAuthError !== "function" ||
|
|
49
|
+
typeof window.electronAPI?.onAuthStateChanged !== "function"
|
|
50
|
+
) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const authStatePromise = window.electronAPI?.getAuthState?.();
|
|
55
|
+
authStatePromise
|
|
56
|
+
?.then((state) => {
|
|
57
|
+
if (state) {
|
|
58
|
+
setAuthState(state);
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
?.catch((error) => {
|
|
62
|
+
console.error("Failed to read Electron auth state", error);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const syncAndRefresh = async () => {
|
|
66
|
+
await window.electronAPI?.syncAuthSession?.();
|
|
67
|
+
router.refresh();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const unsubscribeAuthenticated = window.onAuthenticated(() => {
|
|
71
|
+
syncAndRefresh().catch((error) => {
|
|
72
|
+
console.error(
|
|
73
|
+
"Failed to sync auth session after authentication",
|
|
74
|
+
error
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
const unsubscribeUserUpdated = window.onUserUpdated(() => {
|
|
79
|
+
syncAndRefresh().catch((error) => {
|
|
80
|
+
console.error("Failed to sync auth session after user update", error);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
const unsubscribeAuthError = window.onAuthError(
|
|
84
|
+
(ctx: ElectronAuthErrorContext) => {
|
|
85
|
+
toast.error(ctx.message || "Authentication failed");
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
const unsubscribeAuthState = window.electronAPI.onAuthStateChanged(
|
|
89
|
+
(state) => {
|
|
90
|
+
setAuthState(state);
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
unsubscribeAuthenticated();
|
|
96
|
+
unsubscribeUserUpdated();
|
|
97
|
+
unsubscribeAuthError();
|
|
98
|
+
unsubscribeAuthState();
|
|
99
|
+
};
|
|
100
|
+
}, [isDesktopAppEnabled, router]);
|
|
101
|
+
|
|
102
|
+
if (!isDesktopAppEnabled) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const overlayKey = `${authState.status}:${authState.message ?? ""}:${
|
|
107
|
+
authState.status === "idle" ? "" : (authState.detail ?? "")
|
|
108
|
+
}`;
|
|
109
|
+
|
|
110
|
+
return <ElectronAuthOverlay key={overlayKey} state={authState} />;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ElectronAuthOverlay({ state }: { state: ElectronRendererAuthState }) {
|
|
114
|
+
const [isDismissed, setIsDismissed] = useState(false);
|
|
115
|
+
|
|
116
|
+
if (state.status === "idle" || !state.message) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isLoading =
|
|
121
|
+
state.status === "awaiting-browser" || state.status === "finishing";
|
|
122
|
+
const canCancel = state.status === "awaiting-browser";
|
|
123
|
+
let detailMessage: string;
|
|
124
|
+
|
|
125
|
+
if (state.status === "awaiting-browser") {
|
|
126
|
+
detailMessage = "Complete sign-in in your browser, then come back here.";
|
|
127
|
+
} else if (state.status === "finishing") {
|
|
128
|
+
detailMessage =
|
|
129
|
+
"Your browser has returned to ChatJS. We're finalizing the session now.";
|
|
130
|
+
} else {
|
|
131
|
+
detailMessage =
|
|
132
|
+
state.detail || "If nothing changes, try the browser flow again.";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!isLoading && isDismissed) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="pointer-events-auto fixed inset-0 z-[999999] flex items-center justify-center bg-background/90 px-4 backdrop-blur-sm">
|
|
141
|
+
<div className="w-full max-w-sm rounded-2xl border bg-background p-6 shadow-2xl">
|
|
142
|
+
<div className="flex items-start gap-3">
|
|
143
|
+
<div className="mt-0.5 text-muted-foreground">
|
|
144
|
+
{isLoading ? (
|
|
145
|
+
<LoaderCircle className="size-5 animate-spin" />
|
|
146
|
+
) : (
|
|
147
|
+
<AlertCircle className="size-5 text-amber-600" />
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
<p className="font-medium">{state.message}</p>
|
|
152
|
+
<p className="text-muted-foreground text-sm">{detailMessage}</p>
|
|
153
|
+
{canCancel ? (
|
|
154
|
+
<Button
|
|
155
|
+
className="mt-2"
|
|
156
|
+
onClick={() => {
|
|
157
|
+
window.electronAPI?.cancelAuthFlow?.().catch((error) => {
|
|
158
|
+
console.error("Failed to cancel Electron auth flow", error);
|
|
159
|
+
});
|
|
160
|
+
}}
|
|
161
|
+
size="sm"
|
|
162
|
+
type="button"
|
|
163
|
+
variant="outline"
|
|
164
|
+
>
|
|
165
|
+
Go back
|
|
166
|
+
</Button>
|
|
167
|
+
) : null}
|
|
168
|
+
{isLoading ? null : (
|
|
169
|
+
<Button
|
|
170
|
+
className="mt-2"
|
|
171
|
+
onClick={() => setIsDismissed(true)}
|
|
172
|
+
size="sm"
|
|
173
|
+
type="button"
|
|
174
|
+
variant="outline"
|
|
175
|
+
>
|
|
176
|
+
Dismiss
|
|
177
|
+
</Button>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ExternalLink, LoaderCircle } from "lucide-react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useEffect, useMemo, useRef, useState, useTransition } from "react";
|
|
6
|
+
import config from "@/chat.config";
|
|
7
|
+
import type { Session } from "@/lib/auth";
|
|
8
|
+
import authClient from "@/lib/auth-client";
|
|
9
|
+
import { Button } from "./ui/button";
|
|
10
|
+
|
|
11
|
+
export function ElectronBrowserSignIn({
|
|
12
|
+
buttonLabel = "Continue with browser",
|
|
13
|
+
}: {
|
|
14
|
+
buttonLabel?: string;
|
|
15
|
+
}) {
|
|
16
|
+
const [opened, setOpened] = useState(false);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="space-y-3">
|
|
20
|
+
<p className="text-center text-muted-foreground text-sm">
|
|
21
|
+
Sign-in opens in your browser. On macOS, {config.appName} may ask to use
|
|
22
|
+
Keychain so it can store your session securely.
|
|
23
|
+
</p>
|
|
24
|
+
<Button
|
|
25
|
+
className="w-full"
|
|
26
|
+
onClick={() => {
|
|
27
|
+
const requestAuth = window.requestAuth;
|
|
28
|
+
if (typeof requestAuth !== "function") {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
Promise.resolve()
|
|
32
|
+
.then(() => requestAuth())
|
|
33
|
+
.catch((error) => {
|
|
34
|
+
console.error("Failed to launch browser sign-in", error);
|
|
35
|
+
});
|
|
36
|
+
window.setTimeout(() => setOpened(true), 300);
|
|
37
|
+
}}
|
|
38
|
+
type="button"
|
|
39
|
+
variant="outline"
|
|
40
|
+
>
|
|
41
|
+
<ExternalLink className="mr-2 size-4" />
|
|
42
|
+
{buttonLabel}
|
|
43
|
+
</Button>
|
|
44
|
+
|
|
45
|
+
{opened ? (
|
|
46
|
+
<p className="text-center text-muted-foreground text-sm">
|
|
47
|
+
Finish signing in through your browser. If macOS asks about Keychain
|
|
48
|
+
access, allow it to keep your session saved securely.
|
|
49
|
+
</p>
|
|
50
|
+
) : null}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ElectronTransferUser({
|
|
56
|
+
query,
|
|
57
|
+
session,
|
|
58
|
+
}: {
|
|
59
|
+
query: Record<string, string>;
|
|
60
|
+
session: Session;
|
|
61
|
+
}) {
|
|
62
|
+
const [isPending, startTransition] = useTransition();
|
|
63
|
+
const router = useRouter();
|
|
64
|
+
const hasStartedTransferRef = useRef(false);
|
|
65
|
+
const useAnotherAccountHref = useMemo(() => {
|
|
66
|
+
const params = new URLSearchParams(query);
|
|
67
|
+
params.delete("client_id");
|
|
68
|
+
params.delete("state");
|
|
69
|
+
params.delete("code_challenge");
|
|
70
|
+
params.delete("code_challenge_method");
|
|
71
|
+
|
|
72
|
+
const nextQuery = params.toString();
|
|
73
|
+
return nextQuery ? `/login?${nextQuery}` : "/login";
|
|
74
|
+
}, [query]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (hasStartedTransferRef.current) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
hasStartedTransferRef.current = true;
|
|
81
|
+
|
|
82
|
+
startTransition(async () => {
|
|
83
|
+
await authClient.electron.transferUser({ fetchOptions: { query } });
|
|
84
|
+
router.refresh();
|
|
85
|
+
});
|
|
86
|
+
}, [query, router]);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="space-y-4">
|
|
90
|
+
<div className="rounded-lg border px-4 py-3 text-sm">
|
|
91
|
+
<p className="font-medium">{session.user.name || session.user.email}</p>
|
|
92
|
+
<p className="text-muted-foreground">{session.user.email}</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<Button
|
|
96
|
+
className="w-full"
|
|
97
|
+
disabled={isPending}
|
|
98
|
+
onClick={() => {
|
|
99
|
+
startTransition(async () => {
|
|
100
|
+
await authClient.electron.transferUser({ fetchOptions: { query } });
|
|
101
|
+
router.refresh();
|
|
102
|
+
});
|
|
103
|
+
}}
|
|
104
|
+
type="button"
|
|
105
|
+
>
|
|
106
|
+
{isPending ? (
|
|
107
|
+
<>
|
|
108
|
+
<LoaderCircle className="mr-2 size-4 animate-spin" />
|
|
109
|
+
Connecting…
|
|
110
|
+
</>
|
|
111
|
+
) : (
|
|
112
|
+
"Continue to desktop app"
|
|
113
|
+
)}
|
|
114
|
+
</Button>
|
|
115
|
+
|
|
116
|
+
<Button asChild className="w-full" variant="ghost">
|
|
117
|
+
<a href={useAnotherAccountHref}>Use another account</a>
|
|
118
|
+
</Button>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -27,7 +27,7 @@ export const FaviconGroup: React.FC<FaviconGroupProps> = ({
|
|
|
27
27
|
<Favicon
|
|
28
28
|
alt={`Favicon for ${source.title || new URL(source.url).hostname}`}
|
|
29
29
|
className={cn(
|
|
30
|
-
"h-5 w-5 rounded-full border-2 border-
|
|
30
|
+
"h-5 w-5 rounded-full border-2 border-background",
|
|
31
31
|
index > 0 ? "-ml-2" : ""
|
|
32
32
|
)}
|
|
33
33
|
key={source.url || index}
|
|
@@ -2,7 +2,7 @@ import { useMessageById } from "@ai-sdk-tools/store";
|
|
|
2
2
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import { ThumbsDown, ThumbsUp } from "lucide-react";
|
|
4
4
|
import { toast } from "sonner";
|
|
5
|
-
import {
|
|
5
|
+
import { type ChatMessage, getPrimarySelectedModelId } from "@/lib/ai/types";
|
|
6
6
|
import type { Vote } from "@/lib/db/schema";
|
|
7
7
|
import { useSession } from "@/providers/session-provider";
|
|
8
8
|
import { useTRPC } from "@/trpc/react";
|
|
@@ -16,7 +16,7 @@ export const Greeting = () => (
|
|
|
16
16
|
</motion.div>
|
|
17
17
|
<motion.div
|
|
18
18
|
animate={{ opacity: 1, y: 0 }}
|
|
19
|
-
className="text-2xl text-
|
|
19
|
+
className="text-2xl text-muted-foreground"
|
|
20
20
|
exit={{ opacity: 0, y: 10 }}
|
|
21
21
|
initial={{ opacity: 0, y: 10 }}
|
|
22
22
|
transition={{ delay: 0.6 }}
|
|
@@ -82,8 +82,7 @@ function InteractiveChart({ chart }: { chart: BaseChart }) {
|
|
|
82
82
|
backgroundColor: tooltipBg,
|
|
83
83
|
borderWidth: 0,
|
|
84
84
|
padding: [6, 10],
|
|
85
|
-
className:
|
|
86
|
-
"echarts-tooltip rounded-lg! border! border-neutral-200! dark:border-neutral-800!",
|
|
85
|
+
className: "echarts-tooltip rounded-lg! border! border-border!",
|
|
87
86
|
textStyle: {
|
|
88
87
|
color: textColor,
|
|
89
88
|
fontSize: 13,
|
|
@@ -247,10 +246,10 @@ function InteractiveChart({ chart }: { chart: BaseChart }) {
|
|
|
247
246
|
initial={{ opacity: 0, y: 20 }}
|
|
248
247
|
transition={{ duration: 0.5 }}
|
|
249
248
|
>
|
|
250
|
-
<Card className="overflow-hidden border-
|
|
249
|
+
<Card className="overflow-hidden border-border bg-card">
|
|
251
250
|
<div className="p-6">
|
|
252
251
|
{chart.title && (
|
|
253
|
-
<h3 className="mb-4 font-medium text-
|
|
252
|
+
<h3 className="mb-4 font-medium text-foreground text-lg">
|
|
254
253
|
{chart.title}
|
|
255
254
|
</h3>
|
|
256
255
|
)}
|