feat: implement basic web app

This commit is contained in:
Henry Li
2026-01-15 23:40:21 +08:00
parent c7d68c6d3f
commit cecc684de1
49 changed files with 4142 additions and 626 deletions

View File

@@ -0,0 +1,115 @@
"use client";
import { type HumanMessage } from "@langchain/core/messages";
import { useStream } from "@langchain/langgraph-sdk/react";
import { useQueryClient } from "@tanstack/react-query";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { BreadcrumbItem } from "@/components/ui/breadcrumb";
import { InputBox } from "@/components/workspace/input-box";
import { MessageList } from "@/components/workspace/message-list/message-list";
import {
WorkspaceContainer,
WorkspaceBody,
WorkspaceHeader,
WorkspaceFooter,
} from "@/components/workspace/workspace-container";
import { getLangGraphClient } from "@/core/api";
import type { MessageThread, MessageThreadState } from "@/core/thread";
import { titleOfThread } from "@/core/thread/utils";
import { uuid } from "@/core/utils/uuid";
const langGraphClient = getLangGraphClient();
export default function ChatPage() {
const router = useRouter();
const queryClient = useQueryClient();
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
const isNewThread = useMemo(
() => threadIdFromPath === "new",
[threadIdFromPath],
);
const [threadId, setThreadId] = useState<string | null>(null);
useEffect(() => {
if (threadIdFromPath !== "new") {
setThreadId(threadIdFromPath);
} else {
setThreadId(uuid());
}
}, [threadIdFromPath]);
const thread = useStream<MessageThreadState>({
client: langGraphClient,
assistantId: "lead_agent",
threadId: !isNewThread ? threadId : undefined,
reconnectOnMount: true,
fetchStateHistory: true,
onFinish() {
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
},
});
const handleSubmit = useCallback(
async (message: PromptInputMessage) => {
const text = message.text.trim();
if (isNewThread) {
router.replace(`/workspace/chats/${threadId}`);
}
await thread.submit(
{
messages: [
{
type: "human",
content: [
{
type: "text",
text,
},
],
},
] as HumanMessage[],
},
{
threadId: isNewThread ? threadId! : undefined,
streamSubgraphs: true,
streamResumable: true,
context: {
thread_id: threadId!,
model: "deepseek-v3.2",
thinking_enabled: true,
},
},
);
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
},
[isNewThread, queryClient, router, thread, threadId],
);
const handleStop = useCallback(async () => {
await thread.stop();
}, [thread]);
return (
<WorkspaceContainer>
<WorkspaceHeader>
<BreadcrumbItem className="hidden md:block">
{isNewThread
? "New"
: titleOfThread(thread as unknown as MessageThread)}
</BreadcrumbItem>
</WorkspaceHeader>
<WorkspaceBody>
<div className="flex size-full justify-center">
<MessageList className="size-full" thread={thread} />
</div>
</WorkspaceBody>
<WorkspaceFooter>
<InputBox
className="max-w-(--container-width-md)"
autoFocus={isNewThread}
status={thread.isLoading ? "streaming" : "ready"}
onSubmit={handleSubmit}
onStop={handleStop}
/>
</WorkspaceFooter>
</WorkspaceContainer>
);
}

View File

@@ -0,0 +1,18 @@
"use client";
import {
WorkspaceBody,
WorkspaceContainer,
WorkspaceHeader,
} from "@/components/workspace/workspace-container";
export default function ChatsPage() {
return (
<WorkspaceContainer>
<WorkspaceHeader></WorkspaceHeader>
<WorkspaceBody>
<div className="flex size-full justify-center"></div>
</WorkspaceBody>
</WorkspaceContainer>
);
}

View File

@@ -0,0 +1,29 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { Overscroll } from "@/components/workspace/overscroll";
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
const queryClient = new QueryClient();
export default function WorkspaceLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<QueryClientProvider client={queryClient}>
<SidebarProvider
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
} as React.CSSProperties
}
>
<Overscroll behavior="none" overflow="hidden" />
<WorkspaceSidebar />
<SidebarInset>{children}</SidebarInset>
</SidebarProvider>
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function WorkspacePage() {
return redirect("/workspace/chats/new");
}

View File

@@ -0,0 +1,26 @@
"use client";
import { useStream } from "@langchain/langgraph-sdk/react";
import { useParams } from "next/navigation";
import { getLangGraphClient } from "@/core/api";
import type { MessageThreadState } from "@/core/thread";
const apiClient = getLangGraphClient();
export default function TestPage() {
const { thread_id: threadId } = useParams<{ thread_id: string }>();
const thread = useStream<MessageThreadState>({
client: apiClient,
assistantId: "lead_agent",
threadId,
reconnectOnMount: true,
fetchStateHistory: true,
});
return (
<div className="p-4">
<div>{threadId}</div>
<div>{thread.isLoading ? "loading" : "not loading"}</div>
</div>
);
}