Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ E2B_API_KEY=your_e2b_api_key
GLM_MODLE=glm-4-flash

# Neon Auth(认证功能必填)
PUBLIC_NEON_AUTH_URL=your_neon_auth_url
NEON_DATA_PUBLIC_API_URL=your_neon_api_url
NEON_AUTH_BASE_URL=your_neon_auth_url
NEON_AUTH_COOKIE_SECRET=your_cookie_secret

# Cloudflare R2(文件上传功能必填)
CLOUDFLARE_ACCOUNT_ID=your_account_id
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ E2B_API_KEY=your_e2b_api_key
GLM_MODLE=glm-4-flash

# Neon Auth (Required for authentication)
PUBLIC_NEON_AUTH_URL=your_neon_auth_url
NEON_DATA_PUBLIC_API_URL=your_neon_api_url
NEON_AUTH_BASE_URL=your_neon_auth_url
NEON_AUTH_COOKIE_SECRET=your_cookie_secret

# Cloudflare R2 (Required for file upload)
CLOUDFLARE_ACCOUNT_ID=your_account_id
Expand Down
3 changes: 3 additions & 0 deletions app/api/auth/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { auth } from "@/lib/auth/server";

export const { GET, POST } = auth.handler();
16 changes: 10 additions & 6 deletions app/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,15 @@ const ChatPage = ({ params }: ChatPageProps) => {
<div className="flex h-screen w-screen flex-col">
<Header />
{isMobile ? (
<div className="flex flex-1 flex-col overflow-hidden">
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<MessageArea
messages={messages}
thinkingTime={thinkingTime}
className="flex-1"
className="min-h-0 flex-1"
onShowVnc={handleShowVnc}
/>
<DebugPanel />
<div className="flex w-full items-center justify-center border-t px-4 py-2">
<div className="flex w-full shrink-0 items-center justify-center border-t px-4 py-2">
<InputField
input={input}
setInput={setInput}
Expand Down Expand Up @@ -176,10 +176,14 @@ const ChatPage = ({ params }: ChatPageProps) => {
className="flex flex-1 overflow-hidden"
>
<ResizablePanel defaultSize="30%" maxSize="50%" minSize="30%">
<div className="flex h-full w-full flex-col">
<MessageArea messages={messages} thinkingTime={thinkingTime} />
<div className="flex h-full min-h-0 w-full flex-col">
<MessageArea
messages={messages}
thinkingTime={thinkingTime}
className="min-h-0 flex-1"
/>
<DebugPanel />
<div className="flex w-full items-center justify-center px-4 py-2">
<div className="flex w-full shrink-0 items-center justify-center px-4 py-2">
<InputField
input={input}
setInput={setInput}
Expand Down
17 changes: 8 additions & 9 deletions app/chat/components/debug-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { toolEventsAtom } from "@/atoms/chat";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import type { ToolCallEvent } from "@/types/chat";
import { useAtomValue } from "jotai";
Expand Down Expand Up @@ -36,10 +35,10 @@ const ToolEventItem = memo(({ event }: ToolEventItemProps) => {
: "...";

return (
<div className="border-b last:border-b-0">
<div className="min-w-0 border-b last:border-b-0">
<button
type="button"
className="flex w-full items-center gap-2 px-3 py-2 text-left transition-colors duration-200 hover:bg-muted/50"
className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left transition-colors duration-200 hover:bg-muted/50"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
Expand All @@ -66,12 +65,12 @@ const ToolEventItem = memo(({ event }: ToolEventItemProps) => {
</span>
</button>
{isExpanded && (
<div className="space-y-2 bg-muted/20 px-3 pt-1 pb-3">
<div className="min-w-0 space-y-2 bg-muted/20 px-3 pt-1 pb-3">
<div>
<p className="mb-1 text-[9px] text-muted-foreground uppercase sm:text-[10px]">
{t("arguments")}
</p>
<pre className="overflow-x-auto rounded bg-background p-2 font-mono text-[10px] sm:text-[11px]">
<pre className="w-full max-w-full overflow-x-auto whitespace-pre-wrap break-all rounded bg-background p-2 font-mono text-[10px] sm:text-[11px]">
{JSON.stringify(event.args, null, 2)}
</pre>
</div>
Expand All @@ -80,7 +79,7 @@ const ToolEventItem = memo(({ event }: ToolEventItemProps) => {
<p className="mb-1 text-[9px] text-muted-foreground uppercase sm:text-[10px]">
{t("result")}
</p>
<pre className="overflow-x-auto rounded bg-background p-2 font-mono text-[10px] sm:text-[11px]">
<pre className="w-full max-w-full overflow-x-auto whitespace-pre-wrap break-all rounded bg-background p-2 font-mono text-[10px] sm:text-[11px]">
{JSON.stringify(event.result, null, 2)}
</pre>
</div>
Expand All @@ -99,7 +98,7 @@ const DebugPanel = memo(() => {
const t = useTranslations("debug");

return (
<div className="border-t">
<div className="min-w-0 shrink-0 border-t bg-background">
<button
type="button"
className="flex w-full items-center gap-2 px-4 py-2 text-left transition-colors duration-200 hover:bg-muted/50"
Expand All @@ -122,7 +121,7 @@ const DebugPanel = memo(() => {
)}
</button>
{isOpen && (
<ScrollArea className="max-h-64">
<div className="custom-scrollbar max-h-[40vh] min-w-0 overflow-y-auto overflow-x-hidden overscroll-contain">
{toolEvents.length === 0 ? (
<p className="px-4 py-3 text-[10px] text-muted-foreground sm:text-xs">
{t("noEvents")}
Expand All @@ -132,7 +131,7 @@ const DebugPanel = memo(() => {
<ToolEventItem key={event.id} event={event} />
))
)}
</ScrollArea>
</div>
)}
</div>
);
Expand Down
8 changes: 5 additions & 3 deletions app/chat/components/message-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const ReasoningBlock = memo(({ text }: { text: string }) => {
<Clock className="size-3" />
{t("thinking")}
</div>
<p className="whitespace-pre-wrap text-[10px] text-amber-800 sm:text-xs">
<p className="whitespace-pre-wrap break-words text-[10px] text-amber-800 [overflow-wrap:anywhere] sm:text-xs">
{text}
</p>
</div>
Expand Down Expand Up @@ -167,7 +167,7 @@ const MessageItem = memo(

<div
className={cn(
"max-w-[80%] space-y-1",
"min-w-0 max-w-[80%] space-y-1",
isUser ? "order-first flex flex-col items-end" : "",
)}
>
Expand Down Expand Up @@ -195,7 +195,9 @@ const MessageItem = memo(
isUser ? "bg-primary text-primary-foreground" : "bg-muted",
)}
>
<p className="whitespace-pre-wrap">{textPart.text}</p>
<p className="whitespace-pre-wrap break-words [overflow-wrap:anywhere]">
{textPart.text}
</p>
</div>
);
})}
Expand Down
24 changes: 21 additions & 3 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
"use client";

import jotaiStore, { logoutAtom, userAtom } from "@/atoms";
import { Toaster } from "@/components/ui/sonner";
import { getUserInfo } from "@/services/user";
import { authClient } from "@/lib/auth/client";
import type { User } from "@/types";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useEffect } from "react";

const queryClient = new QueryClient();

const AppProviders = ({ children }: { children: React.ReactNode }) => {
const { data: session } = authClient.useSession();

useEffect(() => {
getUserInfo();
}, []);
if (session?.user) {
const { id, name, email, image, createdAt, updatedAt } = session.user;
const user: User = {
id,
userName: name,
email,
avatar: image || null,
createdAt: new Date(createdAt).toISOString(),
updatedAt: new Date(updatedAt).toISOString(),
};
jotaiStore.set(userAtom, user);
return;
}

jotaiStore.set(logoutAtom);
}, [session]);

return (
<QueryClientProvider client={queryClient}>
Expand Down
8 changes: 5 additions & 3 deletions components/share/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,16 @@ const Avatar = ({ mode = "default", className = "" }: AvatarProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{/* Avatar */}
<div className="relative size-6 cursor-pointer rounded-full md:size-8">
<button
type="button"
className="relative size-6 cursor-pointer rounded-full md:size-8"
>
<img
src="https://img.daisyui.com/images/profile/demo/yellingwoman@192.webp"
alt="User avatar"
className="rounded-full"
/>
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end">
<DropdownMenuLabel>{t("myAccount")}</DropdownMenuLabel>
Expand Down
30 changes: 20 additions & 10 deletions components/share/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ const Header = () => {
return (
<>
<Sidebar />
{!user?.id && (
<LoginDialog
open={isLoginDialogOpen}
onOpenChange={setIsLoginDialogOpen}
loginText={t("login")}
showTrigger={false}
/>
)}
<div className="relative flex w-full items-center justify-between border-b px-3 py-3 md:px-4">
{/* Sidebar toggle button */}
<Button
Expand All @@ -60,11 +68,12 @@ const Header = () => {
) : user?.id ? (
<Avatar />
) : (
<LoginDialog
open={isLoginDialogOpen}
onOpenChange={setIsLoginDialogOpen}
loginText={t("login")}
/>
<Button
className="cursor-pointer rounded-full"
onClick={() => setIsLoginDialogOpen(true)}
>
{t("login")}
</Button>
)}
</div>
</div>
Expand Down Expand Up @@ -122,11 +131,12 @@ const Header = () => {
<Avatar />
) : (
<div className="flex items-center justify-center p-4">
<LoginDialog
open={isLoginDialogOpen}
onOpenChange={setIsLoginDialogOpen}
loginText={t("login")}
/>
<Button
className="cursor-pointer rounded-full"
onClick={() => setIsLoginDialogOpen(true)}
>
{t("login")}
</Button>
</div>
)}
</div>
Expand Down
10 changes: 9 additions & 1 deletion components/share/input-field.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { firstUserInputAtom } from "@/atoms";
import { firstUserInputAtom, userAtom } from "@/atoms";
import loginDialogAtom from "@/atoms/login-dialog";
import {
InputGroup,
InputGroupAddon,
Expand Down Expand Up @@ -78,6 +79,8 @@ const InputField = ({
const router = useRouter();

const [firstUserInput, setFirstUserInput] = useAtom(firstUserInputAtom);
const [user] = useAtom(userAtom);
const [, setIsLoginDialogOpen] = useAtom(loginDialogAtom);

const [attachments, setAttachments] = useState<File[]>([]);
const [uploadedFiles, setUploadedFiles] = useState<UploadResult[]>([]);
Expand Down Expand Up @@ -210,6 +213,11 @@ const InputField = ({
let newInput = "";

if (firstUserInput && isHome) {
if (!user?.id) {
setIsLoginDialogOpen(true);
return;
}

const sessionID = generateId();
router.push(`/chat/${sessionID}`);
return;
Expand Down
14 changes: 9 additions & 5 deletions components/share/login-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ const LoginDialog = ({
open,
onOpenChange,
loginText,
showTrigger = true,
}: {
open?: boolean;
onOpenChange?: (open: boolean) => void;
loginText?: string;
showTrigger?: boolean;
}) => {
const t = useTranslations("login");
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -142,11 +144,13 @@ const LoginDialog = ({
onOpenChange?.(isOpen);
}}
>
<DialogTrigger asChild>
<Button className="cursor-pointer rounded-full">
{loginText ?? t("startBuilding")}
</Button>
</DialogTrigger>
{showTrigger && (
<DialogTrigger asChild>
<Button className="cursor-pointer rounded-full">
{loginText ?? t("startBuilding")}
</Button>
</DialogTrigger>
)}
<DialogContent
className="rounded-xl sm:max-w-[400px]"
// showCloseButton={false} // Hide default close button
Expand Down
2 changes: 1 addition & 1 deletion components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function ScrollArea({
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
Expand Down
33 changes: 32 additions & 1 deletion hooks/use-chat.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { extractToolEventsFromMessages } from "@/hooks/use-chat";
import jotaiStore from "@/atoms";
import loginDialogAtom from "@/atoms/login-dialog";
import {
createAuthAwareChatFetch,
extractToolEventsFromMessages,
} from "@/hooks/use-chat";

type SimplePart = {
type: string;
Expand Down Expand Up @@ -157,6 +162,32 @@ describe("extractToolEventsFromMessages", () => {
});
});

describe("createAuthAwareChatFetch", () => {
beforeEach(() => {
jotaiStore.set(loginDialogAtom, false);
});

it("opens the login dialog when chat requests return 401", async () => {
const authAwareFetch = createAuthAwareChatFetch(async () => {
return { status: 401 } as Response;
});

await authAwareFetch("/api/chat");

expect(jotaiStore.get(loginDialogAtom)).toBe(true);
});

it("keeps the login dialog closed for non-401 responses", async () => {
const authAwareFetch = createAuthAwareChatFetch(async () => {
return { status: 200 } as Response;
});

await authAwareFetch("/api/chat");

expect(jotaiStore.get(loginDialogAtom)).toBe(false);
});
});

describe("formatRelativeDate", () => {
const { formatRelativeDate } = require("@/lib/utils");

Expand Down
Loading
Loading