From 96d931aa4620a956ab5d88d6fde3f00f4f90022f Mon Sep 17 00:00:00 2001 From: stringnine Date: Sun, 24 May 2026 15:13:40 +0900 Subject: [PATCH 01/10] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=A0=9C=EA=B1=B0,=20=EB=8B=B5?= =?UTF-8?q?=EB=B3=80=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EB=92=B7=20=EB=B0=B0?= =?UTF-8?q?=EA=B2=BD=20=EC=A0=9C=EA=B1=B0,=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=20=EB=82=B4=EB=B6=80=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20?= =?UTF-8?q?px-16px=20=EC=A0=9C=EA=B1=B0=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/rightpanel/ChatMessages.tsx | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/features/chat/components/rightpanel/ChatMessages.tsx b/src/features/chat/components/rightpanel/ChatMessages.tsx index df6c6e3..468c515 100644 --- a/src/features/chat/components/rightpanel/ChatMessages.tsx +++ b/src/features/chat/components/rightpanel/ChatMessages.tsx @@ -5,10 +5,7 @@ import { useCallback, useState, } from "react"; -import { - ProfileIcon, - SubscriptionIcon, -} from "@/shared/components/icons/SettingsIcons"; +import { SubscriptionIcon } from "@/shared/components/icons/SettingsIcons"; import { SparkleIcon } from "@/shared/components/icons/SparkleIcon"; import { MessageContent } from "./MessageContent"; import { MessageAttachments } from "./MessageAttachments"; @@ -80,7 +77,7 @@ const UserMessage = ({ message }: { message: ChatMessage }) => ( )} {/* 텍스트 메시지 */} -
+
( />
-
- -
); @@ -110,7 +101,7 @@ const AssistantMessage = ({ statusText?: string; }) => (
-
+
) ) : ( -
+
@@ -250,16 +241,24 @@ export const ChatMessages = ({ messages }: ChatMessagesProps) => { return (
{/* 가운데 정렬 컨테이너 - ChatInput과 동일한 max-width */} -
+
{messages.map((message, index) => { const prevMessage = messages[index - 1]; const isNewGroup = prevMessage?.role === "assistant" && message.role === "user"; + const isAnswerAfterQuestion = + prevMessage?.role === "user" && message.role === "assistant"; const marginTop = - index === 0 ? "" : isNewGroup ? "mt-[40px]" : "mt-[12px]"; + index === 0 + ? "" + : isNewGroup + ? "mt-[40px]" + : isAnswerAfterQuestion + ? "mt-[24px]" + : "mt-[12px]"; return (
Date: Sun, 24 May 2026 15:34:53 +0900 Subject: [PATCH 02/10] =?UTF-8?q?fix:=20=EB=8B=B5=EB=B3=80=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=2050px=20=EB=9E=98=ED=8D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/components/rightpanel/ChatMessages.tsx | 12 ++++++------ .../chat/components/rightpanel/ThinkingBar.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/features/chat/components/rightpanel/ChatMessages.tsx b/src/features/chat/components/rightpanel/ChatMessages.tsx index 468c515..c162b2a 100644 --- a/src/features/chat/components/rightpanel/ChatMessages.tsx +++ b/src/features/chat/components/rightpanel/ChatMessages.tsx @@ -47,7 +47,7 @@ const FinalResponseLoadingBar = () => { }, []); return ( -
+
( )} {/* 텍스트 메시지 */}
-
+
( -
-
+
+
@@ -114,7 +114,7 @@ const AssistantMessage = ({ ) ) : ( -
+
diff --git a/src/features/chat/components/rightpanel/ThinkingBar.tsx b/src/features/chat/components/rightpanel/ThinkingBar.tsx index 1ea6c54..cb6b8d2 100644 --- a/src/features/chat/components/rightpanel/ThinkingBar.tsx +++ b/src/features/chat/components/rightpanel/ThinkingBar.tsx @@ -26,7 +26,7 @@ export const ThinkingBar = ({ statusText }: ThinkingBarProps) => { const iconColor = isBlue ? "#2A6AFF" : "#6B7280"; return ( -
+
Date: Sun, 24 May 2026 15:42:24 +0900 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20=EB=8B=B5=EB=B3=80=20=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=20=EC=B5=9C=EB=8C=80=20=EB=84=93=EC=9D=B4=20540px=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=20=EB=B0=8F=20=EC=A0=84=EC=B2=B4=20=EB=B0=B0?= =?UTF-8?q?=EA=B2=BD=EC=83=89=20#F1F4F8=20=EA=B3=A0=EC=A0=95=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/chat/components/rightpanel/ChatMessages.tsx | 2 +- src/pages/ChatPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/chat/components/rightpanel/ChatMessages.tsx b/src/features/chat/components/rightpanel/ChatMessages.tsx index c162b2a..6bb5b0c 100644 --- a/src/features/chat/components/rightpanel/ChatMessages.tsx +++ b/src/features/chat/components/rightpanel/ChatMessages.tsx @@ -114,7 +114,7 @@ const AssistantMessage = ({ ) ) : ( -
+
diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 5748fc7..1209ec5 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -67,7 +67,7 @@ const ChatPageContent = () => { return (
Date: Sun, 24 May 2026 15:57:10 +0900 Subject: [PATCH 04/10] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EC=B0=BD,=20=EB=94=94=EB=B0=94=EC=9D=B4=EB=8D=94?= =?UTF-8?q?=EB=A1=9C=20=EC=A4=84=EC=98=80=EC=9D=84=20=EB=95=8C=20=EC=B5=9C?= =?UTF-8?q?=EC=86=8C=20=ED=8C=A8=EB=94=A9=EA=B0=92=20=EC=84=B8=EB=B6=80=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/chat/components/rightpanel/ChatMessages.tsx | 2 +- src/features/chat/components/rightpanel/RightPanel.tsx | 2 +- src/features/chat/hooks/useChatPanel.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/chat/components/rightpanel/ChatMessages.tsx b/src/features/chat/components/rightpanel/ChatMessages.tsx index 6bb5b0c..49859fc 100644 --- a/src/features/chat/components/rightpanel/ChatMessages.tsx +++ b/src/features/chat/components/rightpanel/ChatMessages.tsx @@ -241,7 +241,7 @@ export const ChatMessages = ({ messages }: ChatMessagesProps) => { return (
{/* 가운데 정렬 컨테이너 - ChatInput과 동일한 max-width */}
diff --git a/src/features/chat/components/rightpanel/RightPanel.tsx b/src/features/chat/components/rightpanel/RightPanel.tsx index c47cc71..70ffc07 100644 --- a/src/features/chat/components/rightpanel/RightPanel.tsx +++ b/src/features/chat/components/rightpanel/RightPanel.tsx @@ -37,7 +37,7 @@ export const RightPanel = ({
{/* 입력 영역 */} -
+
Date: Sun, 24 May 2026 16:15:17 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=EB=B7=B0=EC=96=B4=20=EC=97=B4?= =?UTF-8?q?=EB=A6=BC,=20=EB=8B=AB=ED=9E=98=20=EB=B6=84=EA=B8=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/components/rightpanel/ChatMessages.tsx | 10 ++++++++-- .../chat/components/rightpanel/RightPanel.tsx | 15 +++++++++++++-- src/pages/ChatPage.tsx | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/features/chat/components/rightpanel/ChatMessages.tsx b/src/features/chat/components/rightpanel/ChatMessages.tsx index 49859fc..6940d02 100644 --- a/src/features/chat/components/rightpanel/ChatMessages.tsx +++ b/src/features/chat/components/rightpanel/ChatMessages.tsx @@ -14,6 +14,7 @@ import type { ChatMessage } from "../../types/chat_types"; interface ChatMessagesProps { messages: ChatMessage[]; + align?: "start" | "center"; } // FinalResponse 말풍선 대기 중 순환 메시지 @@ -123,7 +124,10 @@ const AssistantMessage = ({
); -export const ChatMessages = ({ messages }: ChatMessagesProps) => { +export const ChatMessages = ({ + messages, + align = "center", +}: ChatMessagesProps) => { const scrollRef = useRef(null); const lastUserMsgRef = useRef(null); const spacerRef = useRef(null); @@ -241,7 +245,9 @@ export const ChatMessages = ({ messages }: ChatMessagesProps) => { return (
{/* 가운데 정렬 컨테이너 - ChatInput과 동일한 max-width */}
diff --git a/src/features/chat/components/rightpanel/RightPanel.tsx b/src/features/chat/components/rightpanel/RightPanel.tsx index 70ffc07..474439c 100644 --- a/src/features/chat/components/rightpanel/RightPanel.tsx +++ b/src/features/chat/components/rightpanel/RightPanel.tsx @@ -11,6 +11,7 @@ interface RightPanelProps { isSending?: boolean; /** 대화 히스토리 로딩 중 여부 */ isLoading?: boolean; + isViewerOpen?: boolean; } export const RightPanel = ({ @@ -19,7 +20,10 @@ export const RightPanel = ({ onSend, isSending, isLoading = false, + isViewerOpen = false, }: RightPanelProps) => { + const contentAlign = isViewerOpen ? "start" : "center"; + return (
{/* 메시지 영역 */} @@ -32,12 +36,19 @@ export const RightPanel = ({
) : ( - + )}
{/* 입력 영역 */} -
+
{ onSend={handleSend} isSending={isSending} isLoading={isNoteLoading} + isViewerOpen={isViewerOpen} />
From 4cd07ae9ea93b3b97ce620e993637514c5759f0b Mon Sep 17 00:00:00 2001 From: stringnine Date: Fri, 29 May 2026 01:14:20 +0900 Subject: [PATCH 06/10] =?UTF-8?q?fix:=20=ED=99=94=EB=A9=B4=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=88=EA=B8=B0=20justify-center=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=98=81=EC=97=AD=20=ED=95=AD=EC=83=81=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/components/rightpanel/ChatMessages.tsx | 10 ++-------- .../chat/components/rightpanel/RightPanel.tsx | 15 ++------------- src/pages/ChatPage.tsx | 1 - 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/features/chat/components/rightpanel/ChatMessages.tsx b/src/features/chat/components/rightpanel/ChatMessages.tsx index 6940d02..f154b12 100644 --- a/src/features/chat/components/rightpanel/ChatMessages.tsx +++ b/src/features/chat/components/rightpanel/ChatMessages.tsx @@ -14,7 +14,6 @@ import type { ChatMessage } from "../../types/chat_types"; interface ChatMessagesProps { messages: ChatMessage[]; - align?: "start" | "center"; } // FinalResponse 말풍선 대기 중 순환 메시지 @@ -124,10 +123,7 @@ const AssistantMessage = ({
); -export const ChatMessages = ({ - messages, - align = "center", -}: ChatMessagesProps) => { +export const ChatMessages = ({ messages }: ChatMessagesProps) => { const scrollRef = useRef(null); const lastUserMsgRef = useRef(null); const spacerRef = useRef(null); @@ -245,9 +241,7 @@ export const ChatMessages = ({ return (
{/* 가운데 정렬 컨테이너 - ChatInput과 동일한 max-width */}
diff --git a/src/features/chat/components/rightpanel/RightPanel.tsx b/src/features/chat/components/rightpanel/RightPanel.tsx index 474439c..f00e82f 100644 --- a/src/features/chat/components/rightpanel/RightPanel.tsx +++ b/src/features/chat/components/rightpanel/RightPanel.tsx @@ -11,7 +11,6 @@ interface RightPanelProps { isSending?: boolean; /** 대화 히스토리 로딩 중 여부 */ isLoading?: boolean; - isViewerOpen?: boolean; } export const RightPanel = ({ @@ -20,10 +19,7 @@ export const RightPanel = ({ onSend, isSending, isLoading = false, - isViewerOpen = false, }: RightPanelProps) => { - const contentAlign = isViewerOpen ? "start" : "center"; - return (
{/* 메시지 영역 */} @@ -36,19 +32,12 @@ export const RightPanel = ({
) : ( - + )}
{/* 입력 영역 */} -
+
{ onSend={handleSend} isSending={isSending} isLoading={isNoteLoading} - isViewerOpen={isViewerOpen} />
From 1d388cab04fd32ef5bed5b57e5851ee6a2d5be47 Mon Sep 17 00:00:00 2001 From: stringnine Date: Fri, 29 May 2026 13:12:30 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=EC=B5=9C=EC=A2=85=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EB=B0=94=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/rightpanel/ChatMessages.tsx | 47 ++++++-- .../components/rightpanel/ToolStatusBar.tsx | 108 ++++++++++++++++++ .../chat/components/rightpanel/index.ts | 1 + src/features/chat/hooks/useChatMessages.ts | 60 +++++++++- src/features/chat/types/chat_types.ts | 9 ++ 5 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 src/features/chat/components/rightpanel/ToolStatusBar.tsx diff --git a/src/features/chat/components/rightpanel/ChatMessages.tsx b/src/features/chat/components/rightpanel/ChatMessages.tsx index f154b12..d5d0b0d 100644 --- a/src/features/chat/components/rightpanel/ChatMessages.tsx +++ b/src/features/chat/components/rightpanel/ChatMessages.tsx @@ -10,6 +10,7 @@ import { SparkleIcon } from "@/shared/components/icons/SparkleIcon"; import { MessageContent } from "./MessageContent"; import { MessageAttachments } from "./MessageAttachments"; import { ThinkingBar } from "./ThinkingBar"; +import { ToolStatusBar } from "./ToolStatusBar"; import type { ChatMessage } from "../../types/chat_types"; interface ChatMessagesProps { @@ -95,10 +96,12 @@ const AssistantMessage = ({ content, isStreaming, statusText, + toolStatuses, }: { content: string; isStreaming?: boolean; statusText?: string; + toolStatuses?: ChatMessage["toolStatuses"]; }) => (
@@ -107,17 +110,40 @@ const AssistantMessage = ({ isActive={true} />
- {isStreaming && !content ? ( - statusText ? ( - - ) : ( - - ) + {isStreaming && !content && toolStatuses?.length ? ( +
+ {toolStatuses.map((status) => ( + + ))} +
+ ) : isStreaming && !content ? ( +
+ {statusText ? ( + + ) : ( + + )} +
) : ( -
-
- -
+
+ {(content.trim() || !toolStatuses?.length) && ( +
+
+ +
+
+ )} + {toolStatuses?.map((status) => ( + + ))}
)}
@@ -274,6 +300,7 @@ export const ChatMessages = ({ messages }: ChatMessagesProps) => { content={message.content} isStreaming={message.isStreaming} statusText={message.statusText} + toolStatuses={message.toolStatuses} /> )}
diff --git a/src/features/chat/components/rightpanel/ToolStatusBar.tsx b/src/features/chat/components/rightpanel/ToolStatusBar.tsx new file mode 100644 index 0000000..3de723f --- /dev/null +++ b/src/features/chat/components/rightpanel/ToolStatusBar.tsx @@ -0,0 +1,108 @@ +import type { SVGProps } from "react"; + +type ToolStatusIconType = "python" | "transform"; + +interface ToolStatusBarProps { + icon: ToolStatusIconType; + label: string; + className?: string; +} + +const PythonIcon = (props: SVGProps) => ( + +); + +const TransformIcon = (props: SVGProps) => ( + +); + +const ICON_BY_TYPE: Record = { + python: PythonIcon, + transform: TransformIcon, +}; + +export const ToolStatusBar = ({ + icon, + label, + className = "", +}: ToolStatusBarProps) => { + const Icon = ICON_BY_TYPE[icon]; + + return ( +
+ +

+ {label} +

+
+ ); +}; diff --git a/src/features/chat/components/rightpanel/index.ts b/src/features/chat/components/rightpanel/index.ts index 73a93b3..b61559e 100644 --- a/src/features/chat/components/rightpanel/index.ts +++ b/src/features/chat/components/rightpanel/index.ts @@ -1,2 +1,3 @@ export { RightPanel } from "./RightPanel"; export { ChatMessages } from "./ChatMessages"; +export { ToolStatusBar } from "./ToolStatusBar"; diff --git a/src/features/chat/hooks/useChatMessages.ts b/src/features/chat/hooks/useChatMessages.ts index 30a06b9..186caae 100644 --- a/src/features/chat/hooks/useChatMessages.ts +++ b/src/features/chat/hooks/useChatMessages.ts @@ -28,6 +28,38 @@ import { showErrorToast } from "@/shared/lib/toast"; type PendingAttachment = ChatSendData["attachments"][number]; +const MOCK_TOOL_STATUSES: NonNullable = [ + { + id: "mock-python-create", + icon: "python", + label: "파이썬 코드 생성 중...", + }, + { + id: "mock-python-calculate", + icon: "python", + label: "파이썬 코드로 문제의 정답 계산 중...", + }, + { + id: "mock-transform-create", + icon: "transform", + label: "변형 문제가 생성되는 중...", + }, +]; + +const withMockToolStatuses = (messages: ChatMessage[]) => { + const firstAssistantIndex = messages.findIndex( + (message) => message.role === "assistant", + ); + + if (firstAssistantIndex === -1) return messages; + + return messages.map((message, index) => + index === firstAssistantIndex + ? { ...message, toolStatuses: MOCK_TOOL_STATUSES } + : message, + ); +}; + /** 서버 ConversationInfo[] → ChatMessage[] 변환 */ const convertConversations = ( conversations: ConversationInfo[], @@ -116,6 +148,8 @@ export const useChatMessages = () => { const chatEntrySource = (initialStateRef.current as { chatEntrySource?: string } | null) ?.chatEntrySource ?? "unknown"; + const shouldUseToolStatusMock = + import.meta.env.DEV && searchParams.get("toolStatusMock") === "true"; const viewerFile = firstMessageData?.viewerFile; @@ -206,10 +240,13 @@ export const useChatMessages = () => { setMessages((prev) => { const serverIds = new Set(serverMessages.map((m) => m.id)); const localOnly = prev.filter((m) => !serverIds.has(m.id)); - return [...serverMessages, ...localOnly]; + const mergedMessages = [...serverMessages, ...localOnly]; + return shouldUseToolStatusMock + ? withMockToolStatuses(mergedMessages) + : mergedMessages; }); } - }, [noteDetail, hasFirstMessage]); + }, [noteDetail, hasFirstMessage, shouldUseToolStatusMock]); // ─── 스트리밍 상태 관리 ─── const [isUploading, setIsUploading] = useState(false); @@ -249,12 +286,14 @@ export const useChatMessages = () => { // 컴포넌트 언마운트 시 진행 중인 스트리밍 + 미리보기 URL 정리 useEffect(() => { + const managedPreviewUrls = managedPreviewUrlsRef.current; + return () => { abortControllerRef.current?.abort(); - managedPreviewUrlsRef.current.forEach((url) => { + managedPreviewUrls.forEach((url) => { URL.revokeObjectURL(url); }); - managedPreviewUrlsRef.current.clear(); + managedPreviewUrls.clear(); }; }, []); @@ -501,6 +540,7 @@ export const useChatMessages = () => { role: "assistant", content: "", isStreaming: true, + toolStatuses: shouldUseToolStatusMock ? MOCK_TOOL_STATUSES : undefined, }, ]); @@ -659,6 +699,7 @@ export const useChatMessages = () => { registerPreviewUrl, searchParams, setSearchParams, + shouldUseToolStatusMock, ]); // ─── 후속 대화 전송 핸들러 ─── @@ -728,6 +769,9 @@ export const useChatMessages = () => { role: "assistant", content: "", isStreaming: true, + toolStatuses: shouldUseToolStatusMock + ? MOCK_TOOL_STATUSES + : undefined, }, ]); @@ -787,7 +831,13 @@ export const useChatMessages = () => { setIsStreamingResponse(false); } }, - [noteId, processStream, queryClient, toMessageAttachment], + [ + noteId, + processStream, + queryClient, + shouldUseToolStatusMock, + toMessageAttachment, + ], ); // ─── 파생 데이터 ─── diff --git a/src/features/chat/types/chat_types.ts b/src/features/chat/types/chat_types.ts index 137a254..a5b64bd 100644 --- a/src/features/chat/types/chat_types.ts +++ b/src/features/chat/types/chat_types.ts @@ -10,6 +10,13 @@ export interface MessageAttachment { previewUrl?: string; } +/** AI 도구 실행 상태바 */ +export interface ToolStatus { + id: string; + icon: "python" | "transform"; + label: string; +} + /** 채팅 메시지 */ export interface ChatMessage { id: string; @@ -21,4 +28,6 @@ export interface ChatMessage { isStreaming?: boolean; /** AI 진행 상황 텍스트 — message(custom) 이벤트의 status (assistant 메시지에만 사용) */ statusText?: string; + /** AI 도구 실행 상태바 목록 (assistant 메시지에만 사용) */ + toolStatuses?: ToolStatus[]; } From c2ad1e2ccd83cf8c67b957a1d066404148a28fcd Mon Sep 17 00:00:00 2001 From: stringnine Date: Thu, 4 Jun 2026 17:28:55 +0900 Subject: [PATCH 08/10] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=B0=94=20=ED=83=80=EC=9E=85/=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AA=A9=EC=97=85=20=EB=B6=84=EB=A6=AC=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ToolStatusIconType을 chat_types로 통합해 아이콘 유니온 타입 중복 제거 - 도구 상태바 목업 데이터/헬퍼를 tool_status_mock.ts로 분리 - 상태바 공통 컨테이너 클래스를 STATUS_BAR_BASE_CLASS 상수로 추출 (ToolStatusBar / ThinkingBar / FinalResponseLoadingBar 공유) Co-Authored-By: Claude Opus 4.8 --- .../components/rightpanel/ChatMessages.tsx | 3 +- .../components/rightpanel/ThinkingBar.tsx | 3 +- .../components/rightpanel/ToolStatusBar.tsx | 8 ++-- .../rightpanel/status_bar_styles.ts | 8 ++++ src/features/chat/hooks/tool_status_mock.ts | 42 +++++++++++++++++++ src/features/chat/hooks/useChatMessages.ts | 33 +-------------- src/features/chat/types/chat_types.ts | 5 ++- 7 files changed, 62 insertions(+), 40 deletions(-) create mode 100644 src/features/chat/components/rightpanel/status_bar_styles.ts create mode 100644 src/features/chat/hooks/tool_status_mock.ts diff --git a/src/features/chat/components/rightpanel/ChatMessages.tsx b/src/features/chat/components/rightpanel/ChatMessages.tsx index d5d0b0d..3316404 100644 --- a/src/features/chat/components/rightpanel/ChatMessages.tsx +++ b/src/features/chat/components/rightpanel/ChatMessages.tsx @@ -11,6 +11,7 @@ import { MessageContent } from "./MessageContent"; import { MessageAttachments } from "./MessageAttachments"; import { ThinkingBar } from "./ThinkingBar"; import { ToolStatusBar } from "./ToolStatusBar"; +import { STATUS_BAR_BASE_CLASS } from "./status_bar_styles"; import type { ChatMessage } from "../../types/chat_types"; interface ChatMessagesProps { @@ -48,7 +49,7 @@ const FinalResponseLoadingBar = () => { }, []); return ( -
+
{ const iconColor = isBlue ? "#2A6AFF" : "#6B7280"; return ( -
+
+

{label} diff --git a/src/features/chat/components/rightpanel/status_bar_styles.ts b/src/features/chat/components/rightpanel/status_bar_styles.ts new file mode 100644 index 0000000..a42a724 --- /dev/null +++ b/src/features/chat/components/rightpanel/status_bar_styles.ts @@ -0,0 +1,8 @@ +/** + * AI 상태바 공통 컨테이너 스타일 + * + * ThinkingBar / FinalResponseLoadingBar / ToolStatusBar가 공유하는 + * 둥근 회색 바 컨테이너의 기본 클래스. 패딩은 각 컴포넌트에서 덧붙인다. + */ +export const STATUS_BAR_BASE_CLASS = + "flex min-h-[30px] w-full items-center gap-[8px] overflow-hidden rounded-[12px] border-[0.5px] border-[#D1D6DE] bg-[#E3E7ED]"; diff --git a/src/features/chat/hooks/tool_status_mock.ts b/src/features/chat/hooks/tool_status_mock.ts new file mode 100644 index 0000000..b0d42ed --- /dev/null +++ b/src/features/chat/hooks/tool_status_mock.ts @@ -0,0 +1,42 @@ +import type { ChatMessage } from "../types/chat_types"; + +/** + * 도구 상태바(ToolStatusBar) UI 확인용 목업 데이터 + * + * 개발 환경(`import.meta.env.DEV`)에서 `?toolStatusMock=true` 쿼리 파라미터가 + * 있을 때만 사용된다. 실제 SSE 이벤트 연동 시 제거 예정. + */ +export const MOCK_TOOL_STATUSES: NonNullable = [ + { + id: "mock-python-create", + icon: "python", + label: "파이썬 코드 생성 중...", + }, + { + id: "mock-python-calculate", + icon: "python", + label: "파이썬 코드로 문제의 정답 계산 중...", + }, + { + id: "mock-transform-create", + icon: "transform", + label: "변형 문제가 생성되는 중...", + }, +]; + +/** 첫 번째 assistant 메시지에 목업 도구 상태바를 주입한다. */ +export const withMockToolStatuses = ( + messages: ChatMessage[], +): ChatMessage[] => { + const firstAssistantIndex = messages.findIndex( + (message) => message.role === "assistant", + ); + + if (firstAssistantIndex === -1) return messages; + + return messages.map((message, index) => + index === firstAssistantIndex + ? { ...message, toolStatuses: MOCK_TOOL_STATUSES } + : message, + ); +}; diff --git a/src/features/chat/hooks/useChatMessages.ts b/src/features/chat/hooks/useChatMessages.ts index 186caae..f6b33c5 100644 --- a/src/features/chat/hooks/useChatMessages.ts +++ b/src/features/chat/hooks/useChatMessages.ts @@ -25,41 +25,10 @@ import { creditKeys } from "@/features/settings/hooks/useCredit"; import { userKeys } from "@/features/settings/hooks/useUser"; import { assetKeys } from "@/features/storage/hooks/useAssets"; import { showErrorToast } from "@/shared/lib/toast"; +import { MOCK_TOOL_STATUSES, withMockToolStatuses } from "./tool_status_mock"; type PendingAttachment = ChatSendData["attachments"][number]; -const MOCK_TOOL_STATUSES: NonNullable = [ - { - id: "mock-python-create", - icon: "python", - label: "파이썬 코드 생성 중...", - }, - { - id: "mock-python-calculate", - icon: "python", - label: "파이썬 코드로 문제의 정답 계산 중...", - }, - { - id: "mock-transform-create", - icon: "transform", - label: "변형 문제가 생성되는 중...", - }, -]; - -const withMockToolStatuses = (messages: ChatMessage[]) => { - const firstAssistantIndex = messages.findIndex( - (message) => message.role === "assistant", - ); - - if (firstAssistantIndex === -1) return messages; - - return messages.map((message, index) => - index === firstAssistantIndex - ? { ...message, toolStatuses: MOCK_TOOL_STATUSES } - : message, - ); -}; - /** 서버 ConversationInfo[] → ChatMessage[] 변환 */ const convertConversations = ( conversations: ConversationInfo[], diff --git a/src/features/chat/types/chat_types.ts b/src/features/chat/types/chat_types.ts index a5b64bd..bb5e4b8 100644 --- a/src/features/chat/types/chat_types.ts +++ b/src/features/chat/types/chat_types.ts @@ -10,10 +10,13 @@ export interface MessageAttachment { previewUrl?: string; } +/** AI 도구 실행 상태바 아이콘 종류 */ +export type ToolStatusIconType = "python" | "transform"; + /** AI 도구 실행 상태바 */ export interface ToolStatus { id: string; - icon: "python" | "transform"; + icon: ToolStatusIconType; label: string; } From d1b1a46dd4f0478e7be6b629675f8447eaa302bf Mon Sep 17 00:00:00 2001 From: stringnine Date: Thu, 4 Jun 2026 22:04:42 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20ToolStatusBar=20transform=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20gradient=20id=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=EB=B0=A9=EC=A7=80=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 같은 transform 아이콘이 여러 개 렌더링되면 linearGradient의 하드코딩된 id가 문서 내에서 중복되어 SVG 스펙을 위반함. useId()로 인스턴스마다 고유 gradient id를 생성하도록 수정. --- .../components/rightpanel/ToolStatusBar.tsx | 89 ++++++++++--------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/src/features/chat/components/rightpanel/ToolStatusBar.tsx b/src/features/chat/components/rightpanel/ToolStatusBar.tsx index a1a0659..eab635a 100644 --- a/src/features/chat/components/rightpanel/ToolStatusBar.tsx +++ b/src/features/chat/components/rightpanel/ToolStatusBar.tsx @@ -1,4 +1,4 @@ -import type { SVGProps } from "react"; +import { useId, type SVGProps } from "react"; import type { ToolStatusIconType } from "../../types/chat_types"; import { STATUS_BAR_BASE_CLASS } from "./status_bar_styles"; @@ -41,47 +41,52 @@ const PythonIcon = (props: SVGProps) => ( ); -const TransformIcon = (props: SVGProps) => ( - -); +const TransformIcon = (props: SVGProps) => { + // 같은 아이콘이 여러 개 렌더링돼도 gradient id가 충돌하지 않도록 인스턴스마다 고유 id 생성 + const gradientId = useId(); + + return ( + + ); +}; const ICON_BY_TYPE: Record = { python: PythonIcon, From a115232cb1ad63e8cad011301263bc7668f20332 Mon Sep 17 00:00:00 2001 From: stringnine Date: Thu, 4 Jun 2026 22:08:01 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20assistant=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=B9=88=20content=20=EB=A7=90=ED=92=8D=EC=84=A0?= =?UTF-8?q?=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=B0=A9=EC=A7=80=20(#280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비스트리밍 분기에서 content가 비어 있고 toolStatuses도 없을 때 `content.trim() || !toolStatuses?.length` 조건이 true가 되어 빈 흰색 말풍선이 렌더링됨. 스트림이 토큰 없이 끊긴 경우 등에서 발생. content가 실제로 있을 때만 말풍선을 렌더링하도록 조건 수정. --- src/features/chat/components/rightpanel/ChatMessages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/chat/components/rightpanel/ChatMessages.tsx b/src/features/chat/components/rightpanel/ChatMessages.tsx index 3316404..eab4fa5 100644 --- a/src/features/chat/components/rightpanel/ChatMessages.tsx +++ b/src/features/chat/components/rightpanel/ChatMessages.tsx @@ -131,7 +131,7 @@ const AssistantMessage = ({

) : (
- {(content.trim() || !toolStatuses?.length) && ( + {content.trim().length > 0 && (