@@ -188,15 +191,9 @@ const CreateBookmark = () => {
return (
-
-
-
+
{id && (
@@ -220,6 +217,17 @@ const CreateBookmark = () => {
value={bookmark.summaryAI}
onChange={onChangeText}
placeholder="북마크에 대한 요약을 작성할 수 있어요."
+ rightElement={
+
+ }
/>
-
diff --git a/apps/extension/src/state/mutation/star.ts b/apps/extension/src/state/mutation/star.ts
index 3c11461..c615bb8 100644
--- a/apps/extension/src/state/mutation/star.ts
+++ b/apps/extension/src/state/mutation/star.ts
@@ -9,7 +9,7 @@ import { useNavigate } from "react-router-dom";
export const useCreateStar = () => {
const navigate = useNavigate();
- const { setIsLoading } = useLoadingStore();
+ const setIsLoading = useLoadingStore((state) => state.setIsLoading);
return useMutation({
mutationFn: createStar,
@@ -30,15 +30,16 @@ export const useCreateStar = () => {
export const useCompleteCreateStar = () => {
const navigate = useReplaceNavigate();
- const { setIsLoading } = useLoadingStore();
+ const setIsLoading = useLoadingStore((state) => state.setIsLoading);
return useMutation({
mutationFn: completeCreateStar,
onMutate: () => {
setIsLoading(true);
},
- onSuccess: () => {
- navigate("/bookmark");
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: [STAR.GET_ALL] });
+ navigate(`/create-bookmark?id=${data?.result?.starId}`);
},
onError: (error) => {
console.error(error);
@@ -50,8 +51,7 @@ export const useCompleteCreateStar = () => {
};
export const useUpdateStar = () => {
- const navigate = useReplaceNavigate();
- const { setIsLoading } = useLoadingStore();
+ const setIsLoading = useLoadingStore((state) => state.setIsLoading);
return useMutation({
mutationFn: updateStar,
@@ -60,7 +60,6 @@ export const useUpdateStar = () => {
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: [STAR.GET_BY_ID, data?.result?.starId] });
- navigate("/bookmark");
},
onError: (error) => {
console.error(error);
diff --git a/apps/www/package.json b/apps/www/package.json
index 1d34161..148c966 100644
--- a/apps/www/package.json
+++ b/apps/www/package.json
@@ -16,9 +16,11 @@
"dompurify": "^3.2.6",
"marked": "^15.0.12",
"next": "15.0.3",
+ "next-themes": "^0.4.6",
"react": "19.0.0-rc-66855b96-20241106",
"react-d3-tree": "^3.6.6",
"react-dom": "19.0.0-rc-66855b96-20241106",
+ "sonner": "^2.0.6",
"three": "^0.175.0",
"wordcloud": "^1.2.3",
"zustand": "^5.0.1"
diff --git a/apps/www/src/app/api/chat/route.ts b/apps/www/src/app/api/chat/route.ts
index 1f4f422..5b605d3 100644
--- a/apps/www/src/app/api/chat/route.ts
+++ b/apps/www/src/app/api/chat/route.ts
@@ -7,16 +7,14 @@ export async function POST(req: NextRequest) {
const body = await req.json();
const { userId, message, session_id } = body;
+ const requestBody = session_id ? { userId, message, session_id } : { userId, message };
+
const response = await fetch(`${baseUrl}/chat/stream`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
- body: JSON.stringify({
- userId,
- message,
- session_id,
- }),
+ body: JSON.stringify(requestBody),
});
if (!response.ok) {
diff --git a/apps/www/src/app/bookmarks/page.tsx b/apps/www/src/app/bookmarks/page.tsx
index f4669bc..5ef0f2c 100644
--- a/apps/www/src/app/bookmarks/page.tsx
+++ b/apps/www/src/app/bookmarks/page.tsx
@@ -1,71 +1,14 @@
-"use client";
-
-import { useEffect, useState } from "react";
-
-import Graph from "@/components/bookmarks/graph";
-import GraphDetail from "@/components/bookmarks/graph/graph-detail";
-import Planet from "@/components/bookmarks/graph/planet";
-import Tree from "@/components/bookmarks/tree";
+import BookmarksPage from "@/app/page/bookmarks";
import { GRAPH_THEME } from "@/constants/bookmark";
-import { useGetAllStars } from "@/lib/tanstack/query/star";
-import { useBookmarkStore } from "@/lib/zustand/bookmark";
-import { Spinner } from "@repo/ui";
-
-const BookmarkPage = () => {
- const [detail, setDetail] = useState({
- open: false,
- id: "",
- });
-
- const { data, isPending } = useGetAllStars();
-
- const bookmarkStore = useBookmarkStore();
-
- const onOpen = (id: string) => {
- setDetail({
- open: true,
- id,
- });
- };
-
- const onClose = () => {
- setDetail({
- open: false,
- id: "",
- });
- };
-
- const renderTheme = () => {
- switch (bookmarkStore.selectedTheme) {
- case GRAPH_THEME.GRAPH:
- return
;
- case GRAPH_THEME.PLANET:
- return
;
- case GRAPH_THEME.TREE:
- return
;
- }
- };
-
- useEffect(() => {
- if (data) {
- bookmarkStore.setStars(data.result);
- }
- }, [data]);
- if (isPending || !data) {
- return (
-
- {isPending ? : 북마크가 없습니다.
}
-
- );
- }
+interface BookmarksProps {
+ searchParams: Promise<{
+ theme: GRAPH_THEME;
+ }>;
+}
- return (
- <>
- {renderTheme()}
-
- >
- );
-};
+export default async function Bookmarks({ searchParams }: BookmarksProps) {
+ const { theme } = await searchParams;
-export default BookmarkPage;
+ return
;
+}
diff --git a/apps/www/src/app/page/bookmarks/index.tsx b/apps/www/src/app/page/bookmarks/index.tsx
new file mode 100644
index 0000000..04985c3
--- /dev/null
+++ b/apps/www/src/app/page/bookmarks/index.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import { useMemo, useState } from "react";
+
+import Graph from "@/components/bookmarks/graph";
+import GraphDetail from "@/components/bookmarks/graph/graph-detail";
+import Planet from "@/components/bookmarks/graph/planet";
+import Tree from "@/components/bookmarks/tree";
+import { GRAPH_THEME } from "@/constants/bookmark";
+import { useGetAllStars } from "@/lib/tanstack/query/star";
+import { AllStarDTO } from "@repo/types";
+import { Spinner } from "@repo/ui";
+
+interface BookmarkPageProps {
+ theme: GRAPH_THEME;
+}
+
+export default function BookmarksPage({ theme }: BookmarkPageProps) {
+ const [detail, setDetail] = useState({
+ open: false,
+ id: "",
+ });
+ const [deletedIds, setDeletedIds] = useState
([]);
+
+ const { data, isPending } = useGetAllStars();
+
+ const onOpen = (id: string) => {
+ setDetail({
+ open: true,
+ id,
+ });
+ };
+
+ const onClose = () => {
+ setDetail({
+ open: false,
+ id: "",
+ });
+ };
+
+ const onDelete = (id: string) => {
+ setDeletedIds((prev) => [...prev, id]);
+ };
+
+ const filteredData: AllStarDTO | undefined = useMemo(() => {
+ if (!data) return undefined;
+ return {
+ ...data.result,
+ starListDto: data.result.starListDto.filter((star) => !deletedIds.includes(star.starId)),
+ linkListDto: data.result.linkListDto.filter(
+ (link) => !link.linkedNodeIdList.some((nodeId) => deletedIds.includes(nodeId))
+ ),
+ };
+ }, [data, deletedIds]);
+
+ if (isPending || !data) {
+ return (
+
+ {isPending ? : 북마크가 없습니다.
}
+
+ );
+ }
+
+ const renderTheme = () => {
+ switch (theme) {
+ case GRAPH_THEME.GRAPH:
+ return ;
+ case GRAPH_THEME.TREE:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+ <>
+ {renderTheme()}
+
+ >
+ );
+}
diff --git a/apps/www/src/components/bookmarks/graph/chat-bot/chat.tsx b/apps/www/src/components/bookmarks/graph/chat-bot/chat.tsx
index d4e539e..96ee509 100644
--- a/apps/www/src/components/bookmarks/graph/chat-bot/chat.tsx
+++ b/apps/www/src/components/bookmarks/graph/chat-bot/chat.tsx
@@ -4,7 +4,6 @@ import { memo, useCallback, useEffect, useRef, useState } from "react";
import Icon from "@/components/common/icon";
import { useUserInfo } from "@/hooks/use-user-info";
-import { useCreateChatSession } from "@/lib/tanstack/mutation/chat";
import { useGetChatMessages } from "@/lib/tanstack/query/chat";
import { cn } from "@repo/ui";
@@ -19,6 +18,7 @@ interface Message {
interface ChatProps {
sessionId: string;
onSessionCreated: (newSessionId: string) => void;
+ isNewChat: boolean;
}
const AssistantMessage = memo(({ content }: { content: string }) => {
@@ -34,20 +34,27 @@ const AssistantMessage = memo(({ content }: { content: string }) => {
});
AssistantMessage.displayName = "AssistantMessage";
-export default function Chat({ sessionId, onSessionCreated }: ChatProps) {
+export default function Chat({ sessionId, onSessionCreated, isNewChat }: ChatProps) {
const { userInfo, isLoading: isUserLoading } = useUserInfo();
- const { data: chatHistory, isLoading: isHistoryLoading } = useGetChatMessages(sessionId);
- const { mutateAsync: createChatSession } = useCreateChatSession();
+ const { data: chatHistory, isLoading: isHistoryLoading } = useGetChatMessages({
+ sessionId,
+ isNewChat,
+ });
const [messages, setMessages] = useState([]);
const [streamingContent, setStreamingContent] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef(null);
- const formRef = useRef(null);
+ const chatFormRef = useRef(null);
+ const startChatFormRef = useRef(null);
+
+ const chatButtonDisabled = isUserLoading || isLoading;
useEffect(() => {
- setMessages([]);
- setStreamingContent(null);
- }, [sessionId]);
+ if (!isNewChat) {
+ setMessages([]);
+ setStreamingContent(null);
+ }
+ }, [sessionId, isNewChat]);
useEffect(() => {
if (chatHistory?.result && chatHistory.result.length > 0 && messages.length === 0) {
@@ -86,6 +93,9 @@ export default function Chat({ sessionId, onSessionCreated }: ChatProps) {
const data = line.split("data: ")[1];
if (data) {
const parsed = JSON.parse(data);
+ if (typeof parsed.data === "object" && "session_id" in parsed.data) {
+ onSessionCreated(parsed.data.session_id);
+ }
if (parsed.type === "chunk" && parsed.data) {
botResponse += parsed.data;
setStreamingContent(botResponse);
@@ -99,93 +109,91 @@ export default function Chat({ sessionId, onSessionCreated }: ChatProps) {
}
}, []);
- const onSubmitQuestion = useCallback(
- async (e: React.FormEvent) => {
- e.preventDefault();
- const formData = new FormData(e.target as HTMLFormElement);
- const question = formData.get("question") as string;
- if (question.trim() === "" || isLoading || isUserLoading || !userInfo) return;
+ const onSubmitQuestion = async (e: React.FormEvent) => {
+ e.preventDefault();
+ const formData = new FormData(e.target as HTMLFormElement);
+ const question = formData.get("question") as string;
+ if (question.trim() === "" || chatButtonDisabled || !userInfo) return;
- e.currentTarget.reset();
- (e.currentTarget.querySelector("textarea") as HTMLTextAreaElement).style.height = "auto";
+ chatFormRef.current?.reset();
+ (e.currentTarget.querySelector("textarea") as HTMLTextAreaElement).style.height = "auto";
- setMessages((prev) => [...prev, { role: "user", content: question }]);
- setIsLoading(true);
+ setMessages((prev) => [...prev, { role: "user", content: question }]);
+ setIsLoading(true);
- try {
- const response = await fetch("/api/chat", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ userId: userInfo.id, message: question, session_id: sessionId }),
- });
- await onStreamResponse(response);
- } catch (error) {
- console.error("Failed to send message:", error);
- } finally {
- setIsLoading(false);
- }
- },
- [isLoading, isUserLoading, userInfo, sessionId, onStreamResponse]
- );
+ try {
+ const response = await fetch("/api/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ userId: userInfo.id, message: question, session_id: sessionId }),
+ });
+ await onStreamResponse(response);
+ } catch (error) {
+ console.error("Failed to send message:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
- const onCreateSessionAndSendMessage = useCallback(
- async (e: React.FormEvent) => {
- e.preventDefault();
- const formData = new FormData(e.target as HTMLFormElement);
- const title = formData.get("title") as string;
- const question = formData.get("question") as string;
- if (!title.trim() || !question.trim() || isLoading || isUserLoading || !userInfo) return;
+ const onCreateSessionAndSendMessage = async (e: React.FormEvent) => {
+ e.preventDefault();
+ const formData = new FormData(e.target as HTMLFormElement);
+ const question = formData.get("question") as string;
+ if (!question.trim() || chatButtonDisabled || !userInfo) return;
- setIsLoading(true);
- try {
- const newSession = await createChatSession({ title });
- const newSessionId = newSession.result.sessionId;
- onSessionCreated(newSessionId);
- setMessages([{ role: "user", content: question }]);
+ setMessages([{ role: "user", content: question }]);
+ setIsLoading(true);
- const response = await fetch("/api/chat", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- userId: userInfo.id,
- message: question,
- session_id: newSessionId,
- }),
- });
- await onStreamResponse(response);
- } catch (error) {
- console.error("Failed to create chat session or send message:", error);
- } finally {
- setIsLoading(false);
- }
- },
- [isLoading, isUserLoading, userInfo, createChatSession, onSessionCreated, onStreamResponse]
- );
+ try {
+ const response = await fetch("/api/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ userId: userInfo.id,
+ message: question,
+ }),
+ });
+ startChatFormRef.current?.reset();
+ await onStreamResponse(response);
+ } catch (error) {
+ console.error("Failed to create chat session or send message:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
if (sessionId === "") {
return (
-