Skip to content

feat(docs-ai-assistant-panel): add resizable AI assistant panel to docs#1285

Open
junghyeonsu wants to merge 23 commits intodevfrom
feat/docs-ai-assistant-panel
Open

feat(docs-ai-assistant-panel): add resizable AI assistant panel to docs#1285
junghyeonsu wants to merge 23 commits intodevfrom
feat/docs-ai-assistant-panel

Conversation

@junghyeonsu
Copy link
Contributor

@junghyeonsu junghyeonsu commented Feb 27, 2026

Summary

  • add a top-level AI assistant panel layout that splits the whole docs viewport into docs (left) and assistant (right)
  • add panel open-close state management, header toggle, and motion-based open-close transitions
  • add chat UI with SEED components and an icon-only send action button
  • add the docs chat API route using Vercel AI SDK, internal LLM router settings, and docs MCP tools
  • add assistant tool rendering for component example, installation, and code block outputs
  • update docs environment variables, config, and dependencies for AI assistant integration
  • tune docs global styles for panel divider, layout behavior, and ToC handling while the AI panel is open

Validation

  • bun generate:all succeeded
  • bun test:all failed with 4 existing failures
    • docs/app/_llms/rules/token-reference-rule.test.ts (2)
    • docs/app/_llms/rules/platform-status-rule.test.ts (2, including Sanity CORS/network-dependent path)

Summary by CodeRabbit

  • New Features

    • SEED 문서에 AI 어시스턴트 패널 추가(데스크탑 분할 레이아웃, 모바일 전체화면), 토글/상태 지속 및 패널 리사이저
    • 스트리밍 채팅 인터페이스(자동 스크롤, 제안 클릭, 대화 관리) 및 서버 연동 채팅 API
    • 채팅 내 도구: 컴포넌트 미리보기, 설치 명령 제시, 코드 블록 표시, 관련 문서 링크 검색, 도구별 결과 렌더링
  • Documentation

    • AI 관련 환경 변수 예시 추가 및 시스템 프롬프트 문서화
  • Tests

    • 마크다운 코드 블록 파서 테스트 추가
  • Other

    • 문서 레이아웃/스타일 전환 애니메이션 개선 및 빌드 구성 프로퍼티 제거 변경

@changeset-bot
Copy link

changeset-bot bot commented Feb 27, 2026

⚠️ No Changeset found

Latest commit: ff51bbc

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

서버측 POST /api/chat 라우트, MCP 클라이언트로 툴 로드, 시스템 프롬프트와 클라이언트 툴, AI 패널(상태·레이아웃·토글·스타일) 및 채팅 UI/메시지·툴 렌더러가 추가되었고 Next.js output: "export" 설정이 제거되었습니다.

Changes

Cohort / File(s) Summary
환경 설정 및 빌드 구성
docs/.env.example, docs/next.config.mjs
AI 관련 환경 변수 항목(LLM_ROUTER_*, SEED_DOCS_MCP_SERVER_URL, NEXT_PUBLIC_CHAT_API_URL) 추가; Next.js output: "export" 제거.
서버 API
docs/app/api/chat/route.ts
새 POST /api/chat 핸들러 추가: JSON 파싱·스키마 검증, MCP 툴 병합, UI→모델 메시지 변환, llm-router 기반 스트리밍 응답 생성 및 UI 스트림 반환.
MCP 클라이언트 및 시스템 프롬프트
docs/lib/ai/mcp-client.ts, docs/lib/ai/system-prompt.ts
MCP HTTP/JSON‑RPC 클라이언트 구현(세션 관리, 초기화, 응답 파싱)과 getMCPTools() 공개; SEED Assistant용 시스템 프롬프트 문자열 추가.
서버/클라이언트 툴 정의 및 사이트맵 검색
docs/lib/ai/tools.ts, docs/lib/ai/sitemap-links.ts
클라이언트 툴(clientTools) 네 가지(showComponentExample, showInstallation, showCodeBlock, findRelatedLinks) 추가; 사이트맵 파싱·토큰화·스코어링으로 관련 링크 검색 로직 추가.
AI 패널 상태 관리
docs/components/ai-panel/ai-panel-provider.tsx
AIPanelProvider와 useAIPanel 훅 추가: 로컬스토리지 초기화·하이드레이션 처리, DefaultChatTransport 인스턴스 제공.
레이아웃 통합 및 토글
docs/components/ai-panel/ai-panel-layout.tsx, docs/components/ai-panel/ai-panel-toggle.tsx, docs/app/layout.config.tsx, docs/app/layout.tsx
MotionProvider·AIPanelProvider·AIPanelLayout로 레이아웃 래핑, 데스크톱 분할 패널(드래그 가능한 구분선) 및 모바일 오버레이 구현, 상단 네비게이션에 토글 삽입.
채팅 UI 및 메시지 렌더링
docs/components/ai-panel/chat-interface.tsx, docs/components/ai-panel/chat-message.tsx, docs/components/ai-panel/tool-result-renderer.tsx
채팅 인터페이스(입력, 제안, 자동 스크롤, 상태), 메시지 파싱(텍스트·동적/정적 툴 파트), 툴 결과 렌더러(컴포넌트 미리보기·설치 명령·코드 블록 등) 추가.
마크다운 코드 블록 파서 및 테스트
docs/components/ai-panel/parse-markdown-code-blocks.ts, docs/components/ai-panel/parse-markdown-code-blocks.test.ts
마크다운에서 코드 블록과 텍스트 세그먼트를 추출하는 파서와 해당 유닛 테스트 추가.
스타일 및 반응형 UI
docs/app/global.css
AI 패널 크기·구분선·전환·컨테이너 쿼리 기반 TOC 반응형 동작 등 스타일 추가.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ChatUI as ChatInterface
    participant API as POST /api/chat
    participant MCPClient as MCP Client
    participant MCPServer as MCP Server
    participant LLMRouter as LLM Router
    participant Renderer as ToolResultRenderer

    User->>ChatUI: 메시지 입력 및 전송
    ChatUI->>API: POST (messages)
    API->>MCPClient: getMCPTools()
    MCPClient->>MCPServer: HTTP JSON‑RPC (initialize / get tools)
    MCPServer-->>MCPClient: 툴 목록 응답
    MCPClient-->>API: 툴 맵 반환
    API->>LLMRouter: streamText(messages, tools, systemPrompt)
    LLMRouter-->>API: 스트리밍 응답 (텍스트 + 툴 호출 이벤트)
    API-->>ChatUI: UI 메시지 스트림 응답
    ChatUI->>Renderer: 툴 결과 렌더링 요청
    Renderer-->>ChatUI: 렌더된 툴 UI
    ChatUI-->>User: 응답 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇
새 패널 열어 채팅을 부르네
툴을 모아 답을 엮어주네
드래그로 나누고 모바일로 날아가네
문서가 말하고 코드가 반짝이네
✨🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 PR의 주요 변경사항인 문서에 리사이징 가능한 AI 어시스턴트 패널 추가를 명확하게 설명합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/docs-ai-assistant-panel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

Alpha Preview (Stackflow SPA)

@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

Alpha Preview (Storybook)

@junghyeonsu junghyeonsu changed the title feat/docs-ai-assistant-panel: add resizable AI assistant panel to docs feat(docs-ai-assistant-panel): add resizable AI assistant panel to docs Feb 27, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
docs/components/ai-panel/tool-result-renderer.tsx (1)

13-87: 컴포넌트 선언을 forwardRef + displayName 규칙에 맞춰 주세요.

ToolResultRenderer도 저장소 React 컴포넌트 규칙과 동일한 선언 패턴으로 맞추는 게 좋습니다.

As per coding guidelines, **/*.{tsx,jsx}: Use forwardRef and displayName for React components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/tool-result-renderer.tsx` around lines 13 - 87,
Convert the ToolResultRenderer function to a forwardRef-based component and set
its displayName: replace the standalone function ToolResultRenderer(props) with
a const exported forwardRef component (using React.forwardRef with the correct
ref and ToolResultRendererProps types) and then assign
ToolResultRenderer.displayName = "ToolResultRenderer"; also ensure you import
forwardRef/React if not present so the component follows the repository rule for
forwardRef + displayName.
docs/components/ai-panel/ai-panel-toggle.tsx (1)

6-20: 컴포넌트 선언을 forwardRef + displayName 규칙에 맞춰 주세요.

현재 AIPanelToggle은 일반 함수 컴포넌트로 선언되어 있어 저장소 React 컴포넌트 규칙과 불일치합니다.

As per coding guidelines, **/*.{tsx,jsx}: Use forwardRef and displayName for React components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/ai-panel-toggle.tsx` around lines 6 - 20, Convert
the AIPanelToggle function component to a ref-forwarding component using
React.forwardRef so it follows the repo rule: create an exported const
AIPanelToggle = forwardRef<HTMLButtonElement,
React.ComponentPropsWithoutRef<'button'>>((props, ref) => { ... }), pass the ref
to the <button> element and spread any incoming props (preserve useAIPanel and
IconSparkle2 usage), and then set AIPanelToggle.displayName = 'AIPanelToggle';
also add the necessary React import for forwardRef/typing if missing.
docs/lib/ai/mcp-client.ts (2)

49-58: fetch 요청에 타임아웃 추가 권장

MCP 서버 요청에 타임아웃이 설정되어 있지 않습니다. 서버가 응답하지 않을 경우 요청이 무한정 대기할 수 있으며, 이는 리소스 누수로 이어질 수 있습니다.

♻️ AbortSignal을 사용한 타임아웃 추가
+const MCP_TIMEOUT_MS = 30000;
+
 async function mcpRequest(
   method: string,
   params: Record<string, unknown> = {},
 ): Promise<JSONRPCResponse> {
   const url = process.env.SEED_DOCS_MCP_SERVER_URL;
   if (!url) throw new Error("SEED_DOCS_MCP_SERVER_URL is not set");

   const headers: Record<string, string> = {
     "Content-Type": "application/json",
     Accept: "application/json",
   };

   if (cachedSessionId) {
     headers["Mcp-Session-Id"] = cachedSessionId;
   }

   const response = await fetch(url, {
     method: "POST",
     headers,
     body: JSON.stringify({
       jsonrpc: "2.0",
       id: Date.now(),
       method,
       params,
     }),
+    signal: AbortSignal.timeout(MCP_TIMEOUT_MS),
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/lib/ai/mcp-client.ts` around lines 49 - 58, The fetch call that creates
"response" currently has no timeout; wrap the request in an AbortController and
pass controller.signal to fetch, start a setTimeout that calls
controller.abort() after a sensible default (e.g., 10s or configurable), and
clear that timeout once the fetch completes; update the logic in the function
that issues the fetch (the block creating "response" and sending the JSON-RPC
body) to handle the abort (reject/throw a timeout-specific error or translate
DOMException.name === 'AbortError') so callers get a clear timeout error instead
of hanging.

112-113: 배열 스키마의 아이템 타입 처리 제한

배열 타입에 대해 항상 z.array(z.string())으로 처리하고 있습니다. MCP 도구가 다른 타입의 배열(예: 숫자 배열, 객체 배열)을 사용하는 경우 타입 불일치가 발생할 수 있습니다.

현재 MCP 도구들이 문자열 배열만 사용한다면 문제없지만, 추후 확장성을 고려하면 items 스키마를 반영하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/lib/ai/mcp-client.ts` around lines 112 - 113, The array branch currently
hardcodes z.array(z.string()), which breaks non-string arrays; update the
"array" case to inspect the schema.items and recursively convert that item
schema into a Zod type (e.g., call the existing schema-to-Zod helper such as
convertSchemaToZod / buildZodFromSchema or the function used elsewhere in this
file) and then use z.array(elementZod); if items is missing fall back to z.any()
(or z.string() if you prefer backward compatibility), and preserve any other
modifiers (nullable/optional) applied to the field.
docs/components/ai-panel/ai-panel-layout.tsx (2)

14-20: SSR/하이드레이션 불일치 가능성

isMobilefalse로 초기화되어 있어, 모바일 기기에서 첫 렌더링 시 서버(false)와 클라이언트(true) 간 불일치가 발생할 수 있습니다. 이로 인해 레이아웃 깜빡임(flash)이 발생할 수 있습니다.

♻️ 하이드레이션 안전 처리 제안
 export function AIPanelLayout({ children }: { children: ReactNode }) {
   const { isOpen } = useAIPanel();
-  const [isMobile, setIsMobile] = useState(false);
-  const [renderDesktopContent, setRenderDesktopContent] = useState(isOpen);
+  const [isMobile, setIsMobile] = useState<boolean | null>(null);
+  const [renderDesktopContent, setRenderDesktopContent] = useState(isOpen);
   const [isVisibilityTransitioning, setIsVisibilityTransitioning] = useState(false);
   const aiPanelRef = usePanelRef();
   const closeTimerRef = useRef<number | null>(null);

   useEffect(() => {
     const check = () => setIsMobile(window.innerWidth < 768);
     check();
     window.addEventListener("resize", check);
     return () => window.removeEventListener("resize", check);
   }, []);

+  // 하이드레이션 전 로딩 상태
+  if (isMobile === null) {
+    return <>{children}</>;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/ai-panel-layout.tsx` around lines 14 - 20,
AIPanelLayout currently initializes isMobile to false which causes SSR/client
hydration mismatch on mobile; change initialization to a neutral server-safe
value and set isMobile on mount: initialize isMobile via useState(() => null or
undefined) (or use a typeof window check) and then in a useEffect run once to
detect client viewport (matchMedia or window.innerWidth) and call setIsMobile,
also attach a resize/matchMedia listener to update isMobile and clean it up on
unmount; update any logic that depends on isMobile (e.g., renderDesktopContent,
isVisibilityTransitioning behavior) to handle the initial null/undefined state
until the client detection runs.

14-133: displayName 추가 권장

코딩 가이드라인에 따라 displayName을 추가하는 것이 좋습니다.

♻️ displayName 추가
 export function AIPanelLayout({ children }: { children: ReactNode }) {
   // ... 구현
 }
+
+AIPanelLayout.displayName = "AIPanelLayout";

As per coding guidelines: **/*.{tsx,jsx}: Use forwardRef and displayName for React components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/ai-panel-layout.tsx` around lines 14 - 133, The
AIPanelLayout component should be converted to a forwardRef wrapper and given an
explicit displayName to satisfy the linting guideline; wrap the existing
function in React.forwardRef so it accepts a ref (e.g., export const
AIPanelLayout = forwardRef<HTMLDivElement, { children: ReactNode }>((props, ref)
=> { ... })) forward the ref to the appropriate root DOM/Panel element (or
aiPanelRef if intended), and then set AIPanelLayout.displayName =
"AIPanelLayout". Ensure the props type still includes children and update any
internal references to use props.children.
docs/components/ai-panel/chat-message.tsx (1)

6-70: 컴포넌트에 displayName 추가 권장

코딩 가이드라인에 따라 React 컴포넌트에는 displayName을 설정하는 것이 좋습니다. 이는 React DevTools에서 디버깅할 때 유용합니다.

♻️ displayName 추가 제안
 export function ChatMessage({ message }: { message: UIMessage }) {
   // ... 컴포넌트 구현
 }
+
+ChatMessage.displayName = "ChatMessage";

As per coding guidelines: **/*.{tsx,jsx}: Use forwardRef and displayName for React components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/chat-message.tsx` around lines 6 - 70, The
ChatMessage component lacks a React displayName; add one to follow the guideline
(either set ChatMessage.displayName = "ChatMessage" after the function or
convert the exported function to a forwardRef wrapper: const ChatMessage =
forwardRef<HTMLDivElement, { message: UIMessage }>(...) and then assign
ChatMessage.displayName = "ChatMessage"). Update the export to use the new
symbol if you switch to the forwardRef form and ensure the exported name remains
ChatMessage.
docs/components/ai-panel/chat-interface.tsx (1)

24-157: 구현이 잘 되어 있습니다. displayName 추가 권장

접근성(aria-label, sr-only), 로딩 상태 처리, 자동 스크롤 등이 잘 구현되어 있습니다. 코딩 가이드라인에 따라 displayName을 추가하면 좋겠습니다.

♻️ displayName 추가
 export function ChatInterface() {
   // ... 구현
 }
+
+ChatInterface.displayName = "ChatInterface";

As per coding guidelines: **/*.{tsx,jsx}: Use forwardRef and displayName for React components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/chat-interface.tsx` around lines 24 - 157, The
component ChatInterface is a plain function export but our guideline requires
using forwardRef and setting displayName; refactor ChatInterface to be a
forwarded component (wrap the function with React.forwardRef, accept a ref of
type HTMLDivElement and props as needed) and then set ChatInterface.displayName
= "ChatInterface" after the assignment; also import forwardRef/React if not
already present and keep the existing export (e.g., export const ChatInterface =
forwardRef(...); ChatInterface.displayName = "ChatInterface").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/app/api/chat/route.ts`:
- Around line 17-18: Wrap the POST handler's req.json() call in a try/catch to
return a 400 response on malformed JSON, and add explicit validation for the
extracted messages (e.g., ensure messages is an array via
Array.isArray(messages) and each item contains required fields like role and
content with correct types) before proceeding; update the POST function to
short-circuit with a clear 400 error when validation fails and only pass
well-formed messages to downstream logic.

In `@docs/components/ai-panel/ai-panel-provider.tsx`:
- Around line 20-35: localStorage accesses around STORAGE_KEY in the useEffect
blocks can throw (e.g. private mode); wrap the getItem and setItem calls in
try/catch and fall back safely: when reading (in the effect that calls
setIsOpen/setHydrated) catch errors and treat as "no stored value" so you
default to window.innerWidth >= 768 and still call setHydrated(true); when
writing (the effect depending on isOpen and hydrated) catch errors and silently
ignore/storage-unavailable situations instead of letting the exception
propagate; update logic in the useEffect closures that reference STORAGE_KEY,
setIsOpen, setHydrated, hydrated, and isOpen accordingly.

In `@docs/components/ai-panel/ai-panel-toggle.tsx`:
- Around line 10-17: The button lacks an explicit accessible name for screen
readers; add an aria-label on the button that matches the dynamic title (use the
same isOpen ? "AI 패널 닫기" : "AI 패널 열기" text) so the accessible name remains
stable when the "AI" text is hidden on small screens; update the element that
uses toggle, isOpen and IconSparkle2 (the seed-ai-panel-toggle button) to
include aria-label with the same ternary logic as the title.

In `@docs/components/ai-panel/tool-result-renderer.tsx`:
- Around line 39-40: The code is using unsafe "as string" assertions for tool
inputs (e.g., input.name passed to ComponentPreview inside ToolResultRenderer),
which will throw at render time if the input is malformed; replace those
assertions with a runtime type guard (e.g., check typeof input?.name ===
'string') and render a small fallback UI (placeholder text or an ErrorPreview)
when validation fails. Update all occurrences that currently assert (the
ComponentPreview usage at input.name and similar spots at the other occurrences)
to perform the guard before rendering and pass only validated values to
ComponentPreview or render the fallback when validation fails.

In `@docs/lib/ai/tools.ts`:
- Around line 11-26: The input schemas for the tools (the inputSchema for the
first tool and the inputSchema inside showInstallation) are too
permissive—replace the loose z.string() descriptors with stricter validators:
for the component path schema (the first inputSchema) require a regex that
enforces the "framework/component/preview" pattern (e.g., two slash-separated
segments plus "preview") and add length checks; for showInstallation's name
field require a kebab-case regex (lowercase letters, numbers, hyphens, no
leading/trailing hyphen) and a min/max length where appropriate; implement these
constraints using z.string().regex(...).refine/.min/.max and include clear error
messages so invalid inputs fail validation early.

---

Nitpick comments:
In `@docs/components/ai-panel/ai-panel-layout.tsx`:
- Around line 14-20: AIPanelLayout currently initializes isMobile to false which
causes SSR/client hydration mismatch on mobile; change initialization to a
neutral server-safe value and set isMobile on mount: initialize isMobile via
useState(() => null or undefined) (or use a typeof window check) and then in a
useEffect run once to detect client viewport (matchMedia or window.innerWidth)
and call setIsMobile, also attach a resize/matchMedia listener to update
isMobile and clean it up on unmount; update any logic that depends on isMobile
(e.g., renderDesktopContent, isVisibilityTransitioning behavior) to handle the
initial null/undefined state until the client detection runs.
- Around line 14-133: The AIPanelLayout component should be converted to a
forwardRef wrapper and given an explicit displayName to satisfy the linting
guideline; wrap the existing function in React.forwardRef so it accepts a ref
(e.g., export const AIPanelLayout = forwardRef<HTMLDivElement, { children:
ReactNode }>((props, ref) => { ... })) forward the ref to the appropriate root
DOM/Panel element (or aiPanelRef if intended), and then set
AIPanelLayout.displayName = "AIPanelLayout". Ensure the props type still
includes children and update any internal references to use props.children.

In `@docs/components/ai-panel/ai-panel-toggle.tsx`:
- Around line 6-20: Convert the AIPanelToggle function component to a
ref-forwarding component using React.forwardRef so it follows the repo rule:
create an exported const AIPanelToggle = forwardRef<HTMLButtonElement,
React.ComponentPropsWithoutRef<'button'>>((props, ref) => { ... }), pass the ref
to the <button> element and spread any incoming props (preserve useAIPanel and
IconSparkle2 usage), and then set AIPanelToggle.displayName = 'AIPanelToggle';
also add the necessary React import for forwardRef/typing if missing.

In `@docs/components/ai-panel/chat-interface.tsx`:
- Around line 24-157: The component ChatInterface is a plain function export but
our guideline requires using forwardRef and setting displayName; refactor
ChatInterface to be a forwarded component (wrap the function with
React.forwardRef, accept a ref of type HTMLDivElement and props as needed) and
then set ChatInterface.displayName = "ChatInterface" after the assignment; also
import forwardRef/React if not already present and keep the existing export
(e.g., export const ChatInterface = forwardRef(...); ChatInterface.displayName =
"ChatInterface").

In `@docs/components/ai-panel/chat-message.tsx`:
- Around line 6-70: The ChatMessage component lacks a React displayName; add one
to follow the guideline (either set ChatMessage.displayName = "ChatMessage"
after the function or convert the exported function to a forwardRef wrapper:
const ChatMessage = forwardRef<HTMLDivElement, { message: UIMessage }>(...) and
then assign ChatMessage.displayName = "ChatMessage"). Update the export to use
the new symbol if you switch to the forwardRef form and ensure the exported name
remains ChatMessage.

In `@docs/components/ai-panel/tool-result-renderer.tsx`:
- Around line 13-87: Convert the ToolResultRenderer function to a
forwardRef-based component and set its displayName: replace the standalone
function ToolResultRenderer(props) with a const exported forwardRef component
(using React.forwardRef with the correct ref and ToolResultRendererProps types)
and then assign ToolResultRenderer.displayName = "ToolResultRenderer"; also
ensure you import forwardRef/React if not present so the component follows the
repository rule for forwardRef + displayName.

In `@docs/lib/ai/mcp-client.ts`:
- Around line 49-58: The fetch call that creates "response" currently has no
timeout; wrap the request in an AbortController and pass controller.signal to
fetch, start a setTimeout that calls controller.abort() after a sensible default
(e.g., 10s or configurable), and clear that timeout once the fetch completes;
update the logic in the function that issues the fetch (the block creating
"response" and sending the JSON-RPC body) to handle the abort (reject/throw a
timeout-specific error or translate DOMException.name === 'AbortError') so
callers get a clear timeout error instead of hanging.
- Around line 112-113: The array branch currently hardcodes z.array(z.string()),
which breaks non-string arrays; update the "array" case to inspect the
schema.items and recursively convert that item schema into a Zod type (e.g.,
call the existing schema-to-Zod helper such as convertSchemaToZod /
buildZodFromSchema or the function used elsewhere in this file) and then use
z.array(elementZod); if items is missing fall back to z.any() (or z.string() if
you prefer backward compatibility), and preserve any other modifiers
(nullable/optional) applied to the field.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e2fb929 and 5b44757.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • docs/package.json is excluded by !**/package.json
📒 Files selected for processing (15)
  • docs/.env.example
  • docs/app/api/chat/route.ts
  • docs/app/global.css
  • docs/app/layout.config.tsx
  • docs/app/layout.tsx
  • docs/components/ai-panel/ai-panel-layout.tsx
  • docs/components/ai-panel/ai-panel-provider.tsx
  • docs/components/ai-panel/ai-panel-toggle.tsx
  • docs/components/ai-panel/chat-interface.tsx
  • docs/components/ai-panel/chat-message.tsx
  • docs/components/ai-panel/tool-result-renderer.tsx
  • docs/lib/ai/mcp-client.ts
  • docs/lib/ai/system-prompt.ts
  • docs/lib/ai/tools.ts
  • docs/next.config.mjs
💤 Files with no reviewable changes (1)
  • docs/next.config.mjs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
docs/app/api/chat/route.ts (1)

18-27: ⚠️ Potential issue | 🟠 Major

요청 메시지 검증이 아직 느슨해서 역할 위조/비정상 payload가 통과됩니다.

Line [23], Line [25]에서 role을 임의 문자열로 받고 전체를 passthrough한 뒤, Line [43]에서 강제 캐스팅하고 있어 실제 streamText 입력 형태를 보장하지 못합니다. 최소한 role을 허용 목록으로 제한하고(예: user|assistant), 메시지 본문 필드(예: content/parts) 존재 조건을 스키마에서 강제해 주세요.

Also applies to: 43-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/app/api/chat/route.ts` around lines 18 - 27, chatRequestSchema currently
allows arbitrary role strings and uses .passthrough(), which lets forged/invalid
payloads through and then gets force-cast to streamText later; tighten the
schema by changing the messages item to a stricter object (remove
.passthrough()) that enforces role as an allowed enum (e.g.,
z.enum(['user','assistant'])) and requires a message body (e.g., either content:
z.string().min(1) or parts: z.array(z.string()).min(1) via z.union or
refinement), and consider using .strict() to reject extra fields; then update
code that force-casts to streamText to use the validated parsed result from
chatRequestSchema (refer to chatRequestSchema, messages, role, content, parts,
and streamText) instead of an unchecked cast.
🧹 Nitpick comments (2)
docs/lib/ai/tools.ts (1)

44-46: showCodeBlockcode 길이 상한을 추가하는 것을 권장합니다.

Line [44]는 무제한 문자열이라 큰 payload가 들어오면 토큰/렌더 비용이 급증할 수 있습니다. max(...) 제한(예: 8k~16k chars)과 language 허용 패턴(또는 enum)을 두는 편이 안전합니다.

권장 수정안
   showCodeBlock: tool({
@@
     inputSchema: z.object({
-      code: z.string().describe("The code to display"),
-      language: z.string().default("tsx").describe("Programming language"),
+      code: z
+        .string()
+        .min(1, "Code is required")
+        .max(16000, "Code is too long")
+        .describe("The code to display"),
+      language: z
+        .string()
+        .regex(/^[a-z0-9#+.-]{1,20}$/i, "Invalid language identifier")
+        .default("tsx")
+        .describe("Programming language"),
       title: z.string().optional().describe("Optional title for the code block"),
     }),
   }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/lib/ai/tools.ts` around lines 44 - 46, The zod schema for showCodeBlock
allows unbounded code and arbitrary language strings which risks huge payloads;
update the schema used by showCodeBlock (the z.object with fields code,
language, title) to add a max length constraint on code (e.g.,
z.string().max(16000) or a sensible 8k–16k limit) and restrict language to an
allowed pattern or enum (e.g., z.enum([...]) or z.string().regex(...)) while
keeping title optional, so validation prevents overly large or unexpected
language values.
docs/app/api/chat/route.ts (1)

45-45: MCP 도구 목록을 요청마다 다시 조회하지 말고 캐시를 두는 편이 안전합니다.

Line [45]의 getMCPTools()를 매 요청마다 호출하면 응답 지연과 외부 의존 장애 전파가 커집니다. 짧은 TTL 캐시(예: 30~60초)로 도구 목록을 재사용하는 방식이 좋습니다.

권장 수정안
+let cachedMcpTools: Awaited<ReturnType<typeof getMCPTools>> | null = null;
+let cachedMcpToolsAt = 0;
+const MCP_TOOLS_TTL_MS = 60_000;
@@
-  const mcpTools = await getMCPTools();
+  const now = Date.now();
+  if (!cachedMcpTools || now - cachedMcpToolsAt > MCP_TOOLS_TTL_MS) {
+    cachedMcpTools = await getMCPTools();
+    cachedMcpToolsAt = now;
+  }
+  const mcpTools = cachedMcpTools;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/app/api/chat/route.ts` at line 45, Replace the per-request direct call
to getMCPTools() with a short-lived cached value: add a module-level cache
object (e.g., cachedMcpTools and cachedMcpToolsExpiresAt) and when handling
requests return the cached value if Date.now() < expiresAt, otherwise refresh by
calling getMCPTools(), update cachedMcpTools and set expiresAt = Date.now() +
TTL (30–60s). Ensure concurrent refreshes are deduplicated (store an in-flight
Promise and await it) and that errors while refreshing fall back to the previous
cachedMcpTools if available; update usages that reference mcpTools to use the
cached value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@docs/app/api/chat/route.ts`:
- Around line 18-27: chatRequestSchema currently allows arbitrary role strings
and uses .passthrough(), which lets forged/invalid payloads through and then
gets force-cast to streamText later; tighten the schema by changing the messages
item to a stricter object (remove .passthrough()) that enforces role as an
allowed enum (e.g., z.enum(['user','assistant'])) and requires a message body
(e.g., either content: z.string().min(1) or parts: z.array(z.string()).min(1)
via z.union or refinement), and consider using .strict() to reject extra fields;
then update code that force-casts to streamText to use the validated parsed
result from chatRequestSchema (refer to chatRequestSchema, messages, role,
content, parts, and streamText) instead of an unchecked cast.

---

Nitpick comments:
In `@docs/app/api/chat/route.ts`:
- Line 45: Replace the per-request direct call to getMCPTools() with a
short-lived cached value: add a module-level cache object (e.g., cachedMcpTools
and cachedMcpToolsExpiresAt) and when handling requests return the cached value
if Date.now() < expiresAt, otherwise refresh by calling getMCPTools(), update
cachedMcpTools and set expiresAt = Date.now() + TTL (30–60s). Ensure concurrent
refreshes are deduplicated (store an in-flight Promise and await it) and that
errors while refreshing fall back to the previous cachedMcpTools if available;
update usages that reference mcpTools to use the cached value.

In `@docs/lib/ai/tools.ts`:
- Around line 44-46: The zod schema for showCodeBlock allows unbounded code and
arbitrary language strings which risks huge payloads; update the schema used by
showCodeBlock (the z.object with fields code, language, title) to add a max
length constraint on code (e.g., z.string().max(16000) or a sensible 8k–16k
limit) and restrict language to an allowed pattern or enum (e.g., z.enum([...])
or z.string().regex(...)) while keeping title optional, so validation prevents
overly large or unexpected language values.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5b44757 and fe67b01.

📒 Files selected for processing (6)
  • docs/app/api/chat/route.ts
  • docs/components/ai-panel/ai-panel-provider.tsx
  • docs/components/ai-panel/ai-panel-toggle.tsx
  • docs/components/ai-panel/chat-interface.tsx
  • docs/components/ai-panel/tool-result-renderer.tsx
  • docs/lib/ai/tools.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/components/ai-panel/tool-result-renderer.tsx
  • docs/components/ai-panel/ai-panel-provider.tsx
  • docs/components/ai-panel/ai-panel-toggle.tsx

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
docs/app/api/chat/route.ts (2)

56-62: stepCountIs(5) 매직 넘버에 대한 문서화 권장

stopWhen: stepCountIs(5)의 5라는 값이 왜 선택되었는지 명확하지 않습니다. 상수로 추출하고 주석을 추가하면 유지보수성이 향상됩니다.

♻️ 상수 추출 예시
+/** 무한 루프 방지를 위한 최대 도구 호출 단계 수 */
+const MAX_TOOL_STEPS = 5;
+
 const result = streamText({
   model: llmRouter(llmRouterModel),
   system: systemPrompt,
   messages,
   tools,
-  stopWhen: stepCountIs(5),
+  stopWhen: stepCountIs(MAX_TOOL_STEPS),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/app/api/chat/route.ts` around lines 56 - 62, The use of a magic literal
in the stop condition should be replaced with a named constant and documented:
extract the numeric literal 5 used in stopWhen: stepCountIs(5) into a clearly
named constant (e.g., MAX_STEP_COUNT or DEFAULT_STOP_STEPS) near where
streamText is invoked, add a brief comment explaining why that value was chosen,
and update the call to stopWhen: stepCountIs(MAX_STEP_COUNT); reference symbols:
stepCountIs, streamText, llmRouterModel, systemPrompt, messages, tools.

37-41: 매 요청마다 getMCPTools() 호출 - 캐싱 고려 필요

현재 모든 POST 요청마다 getMCPTools()가 호출되어 MCP 서버와 통신합니다. MCP 도구 목록이 자주 변경되지 않는다면, 일정 시간 동안 캐싱하여 응답 지연을 줄일 수 있습니다.

♻️ 간단한 캐싱 예시
// mcp-client.ts에 추가
let cachedTools: Record<string, Tool> | null = null;
let cacheExpiry = 0;
const CACHE_TTL_MS = 60000; // 1분

export async function getMCPTools(): Promise<Record<string, Tool>> {
  if (cachedTools && Date.now() < cacheExpiry) {
    return cachedTools;
  }
  // ... existing logic ...
  cachedTools = tools;
  cacheExpiry = Date.now() + CACHE_TTL_MS;
  return tools;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/app/api/chat/route.ts` around lines 37 - 41, The code calls
getMCPTools() on every request which can be expensive; modify getMCPTools (in
mcp-client.ts) to implement a simple in-memory TTL cache: add module-scoped
cachedTools and cacheExpiry values and a CACHE_TTL_MS constant, return
cachedTools when Date.now() < cacheExpiry, otherwise fetch from MCP, update
cachedTools and cacheExpiry, and return the fresh value; ensure you still
propagate errors and invalidate the cache on fetch failure.
docs/lib/ai/tools.ts (1)

46-50: showCodeBlock 입력 스키마에 길이 제한 및 언어 유효성 검사 추가 권장

code 필드에 길이 제한이 없어 매우 큰 코드 블록이 전달될 수 있고, language 필드에 유효한 언어 식별자 검증이 없습니다. 다른 도구들(showComponentExample, showInstallation)과 일관성을 유지하기 위해 제한을 추가하는 것이 좋습니다.

♻️ 권장 수정
   inputSchema: z.object({
-    code: z.string().describe("The code to display"),
-    language: z.string().default("tsx").describe("Programming language"),
+    code: z
+      .string()
+      .min(1, "Code is required")
+      .max(50000, "Code block is too large")
+      .describe("The code to display"),
+    language: z
+      .string()
+      .max(32, "Language identifier is too long")
+      .regex(/^[a-z0-9+-]+$/i, "Invalid language identifier")
+      .default("tsx")
+      .describe("Programming language"),
     title: z.string().optional().describe("Optional title for the code block"),
   }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/lib/ai/tools.ts` around lines 46 - 50, The inputSchema for showCodeBlock
allows unbounded code and accepts any string for language; add validation
similar to other tools by constraining code length (e.g., maxCharacters via
z.string().max(...)) and validate language against a whitelist/enum of supported
language identifiers (use z.enum(...) or z.string().refine(...) with a LANGS
array). Update the inputSchema object in showCodeBlock to enforce these limits
on the code field and replace the free-form language field with an enum/refine
check so invalid languages are rejected consistently.
docs/lib/ai/mcp-client.ts (2)

210-211: 배열 타입 변환이 string[]로 고정되어 있습니다

z.array(z.string())로 하드코딩되어 있어 MCP 도구가 숫자 배열이나 객체 배열을 기대하는 경우 유효성 검사가 실패할 수 있습니다.

♻️ 더 유연한 배열 처리
       case "array":
-        field = z.array(z.string());
+        // MCP 스키마의 items 타입을 확인하여 적절한 타입 사용
+        field = z.array(z.unknown());
         break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/lib/ai/mcp-client.ts` around lines 210 - 211, In the switch handling
"array" (the branch that currently sets field = z.array(z.string())), replace
the hardcoded z.string() with a dynamic element schema built from the array's
item type: inspect the schema/descriptor for the array element (e.g., an "items"
or "elementType" property used by your mapper) and call the same type-to-zod
conversion routine recursively to produce elementSchema, then set field =
z.array(elementSchema); ensure this handles primitive types
(number/boolean/string) and nested object/array schemas by reusing the existing
converter function and preserves optional/null handling.

121-125: 외부 MCP 서버 요청에 타임아웃 추가 권장

fetch 호출에 타임아웃이 없어 MCP 서버가 응답하지 않을 경우 요청이 무기한 대기할 수 있습니다. AbortController를 사용하여 타임아웃을 설정하는 것이 좋습니다.

♻️ 타임아웃 추가 예시
+const MCP_REQUEST_TIMEOUT_MS = 30000;
+
 async function mcpRequest(
   method: string,
   params: Record<string, unknown> = {},
   options: { notification?: boolean } = {},
 ): Promise<JSONRPCResponse> {
   // ...headers setup...

+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), MCP_REQUEST_TIMEOUT_MS);
+
-  const response = await fetch(url, {
-    method: "POST",
-    headers,
-    body: JSON.stringify(body),
-  });
+  let response: Response;
+  try {
+    response = await fetch(url, {
+      method: "POST",
+      headers,
+      body: JSON.stringify(body),
+      signal: controller.signal,
+    });
+  } finally {
+    clearTimeout(timeoutId);
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/lib/ai/mcp-client.ts` around lines 121 - 125, The fetch call that posts
to the MCP server currently has no timeout and can hang; wrap the request with
an AbortController: create an AbortController before calling fetch, set a
setTimeout that calls controller.abort() after a configurable timeout (e.g.,
5–30s), pass controller.signal in the fetch options (alongside
method/headers/body), and clear the timeout on success; also catch and handle
the abort error when awaiting the response so the calling code (the code that
assigns to `response`) can surface a clear timeout error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/app/api/chat/route.ts`:
- Around line 56-62: The use of a magic literal in the stop condition should be
replaced with a named constant and documented: extract the numeric literal 5
used in stopWhen: stepCountIs(5) into a clearly named constant (e.g.,
MAX_STEP_COUNT or DEFAULT_STOP_STEPS) near where streamText is invoked, add a
brief comment explaining why that value was chosen, and update the call to
stopWhen: stepCountIs(MAX_STEP_COUNT); reference symbols: stepCountIs,
streamText, llmRouterModel, systemPrompt, messages, tools.
- Around line 37-41: The code calls getMCPTools() on every request which can be
expensive; modify getMCPTools (in mcp-client.ts) to implement a simple in-memory
TTL cache: add module-scoped cachedTools and cacheExpiry values and a
CACHE_TTL_MS constant, return cachedTools when Date.now() < cacheExpiry,
otherwise fetch from MCP, update cachedTools and cacheExpiry, and return the
fresh value; ensure you still propagate errors and invalidate the cache on fetch
failure.

In `@docs/lib/ai/mcp-client.ts`:
- Around line 210-211: In the switch handling "array" (the branch that currently
sets field = z.array(z.string())), replace the hardcoded z.string() with a
dynamic element schema built from the array's item type: inspect the
schema/descriptor for the array element (e.g., an "items" or "elementType"
property used by your mapper) and call the same type-to-zod conversion routine
recursively to produce elementSchema, then set field = z.array(elementSchema);
ensure this handles primitive types (number/boolean/string) and nested
object/array schemas by reusing the existing converter function and preserves
optional/null handling.
- Around line 121-125: The fetch call that posts to the MCP server currently has
no timeout and can hang; wrap the request with an AbortController: create an
AbortController before calling fetch, set a setTimeout that calls
controller.abort() after a configurable timeout (e.g., 5–30s), pass
controller.signal in the fetch options (alongside method/headers/body), and
clear the timeout on success; also catch and handle the abort error when
awaiting the response so the calling code (the code that assigns to `response`)
can surface a clear timeout error.

In `@docs/lib/ai/tools.ts`:
- Around line 46-50: The inputSchema for showCodeBlock allows unbounded code and
accepts any string for language; add validation similar to other tools by
constraining code length (e.g., maxCharacters via z.string().max(...)) and
validate language against a whitelist/enum of supported language identifiers
(use z.enum(...) or z.string().refine(...) with a LANGS array). Update the
inputSchema object in showCodeBlock to enforce these limits on the code field
and replace the free-form language field with an enum/refine check so invalid
languages are rejected consistently.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fe67b01 and eee2398.

📒 Files selected for processing (3)
  • docs/app/api/chat/route.ts
  • docs/lib/ai/mcp-client.ts
  • docs/lib/ai/tools.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
docs/components/ai-panel/chat-message.tsx (1)

232-387: ChatMessage 컴포넌트 선언을 규칙(forwardRef/displayName)에 맞춰 주세요.

Line 232에서 일반 함수형 컴포넌트로 선언되어 있어 저장소 규칙과 불일치합니다. forwardRefdisplayName 적용을 권장합니다.

As per coding guidelines **/*.{tsx,jsx}: Use forwardRef and displayName for React components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/chat-message.tsx` around lines 232 - 387, The
ChatMessage component is declared as a plain function but must follow repo rules
by using React.forwardRef and setting a displayName; change the export to wrap
the component in forwardRef (accepting props { message }: { message: UIMessage }
and a ref) and export that forwarded component, then set ChatMessage.displayName
= "ChatMessage"; ensure internal references (like useState/useEffect and
handlers handleCopy, parseMarkdownCodeBlocks, getToolRenderContext,
getMessageCopyText, DynamicCodeBlock, ToolResultRenderer) remain unchanged and
the forwarded ref is passed to the outermost div so the ref points at the
component DOM node.
docs/components/ai-panel/tool-result-renderer.tsx (1)

29-36: 컴포넌트 선언을 forwardRef + displayName 규칙에 맞춰 주세요.

ToolLoading(Line 29)과 ToolResultRenderer(Line 84)가 모두 일반 함수 컴포넌트 선언입니다. 저장소 컴포넌트 규칙을 일관 적용해 주세요.

As per coding guidelines **/*.{tsx,jsx}: Use forwardRef and displayName for React components.

Also applies to: 84-199

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/tool-result-renderer.tsx` around lines 29 - 36,
ToolLoading and ToolResultRenderer are declared as plain function components;
update both to use React.forwardRef so they accept and forward a ref (e.g.,
forward a ref to the root div or wrapper element) and assign a static
displayName (ToolLoading.displayName = "ToolLoading",
ToolResultRenderer.displayName = "ToolResultRenderer") to satisfy the repo rule.
Ensure the forwarded ref is typed appropriately (e.g., Ref<HTMLDivElement> or
the actual DOM/component type) and preserve the existing props types (label for
ToolLoading and the props interface used by ToolResultRenderer) when converting
the functions to forwardRef wrappers.
docs/components/ai-panel/ai-panel-provider.tsx (1)

23-68: AIPanelProvider 선언을 컴포넌트 규칙과 맞춰 주세요.

Line 23의 함수형 선언은 현재 저장소 규칙(forwardRef + displayName)과 불일치합니다. Provider 계열도 적용 대상이라면 forwardRef 래핑과 displayName을 추가해 주세요.

As per coding guidelines **/*.{tsx,jsx}: Use forwardRef and displayName for React components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/ai-panel-provider.tsx` around lines 23 - 68, Wrap
the AIPanelProvider functional component with React.forwardRef so it follows the
repository component pattern (export const AIPanelProvider = forwardRef(function
AIPanelProvider(props, ref) { ... })), accept props as { children }: { children:
ReactNode } and pass ref though (even if unused), then set
AIPanelProvider.displayName = "AIPanelProvider"; keep the existing
state/useEffect logic and the AIPanelContext.Provider return unchanged but
ensure the exported symbol is the forwardRef-wrapped AIPanelProvider and
displayName is assigned after the wrap.
docs/components/ai-panel/chat-interface.tsx (1)

20-175: ChatInterfaceforwardRef/displayName 규칙을 맞춰 주세요.

Line 20의 일반 함수 컴포넌트 선언이 현재 저장소 규칙과 다릅니다. 규칙 적용 대상이면 forwardRef로 감싸고 displayName을 명시해 주세요.

As per coding guidelines **/*.{tsx,jsx}: Use forwardRef and displayName for React components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/chat-interface.tsx` around lines 20 - 175, Replace
the plain function component ChatInterface with a forwardRef-wrapped named
export (e.g., export const ChatInterface = forwardRef<HTMLDivElement,
{}>((props, ref) => { ... })) and pass the ref to the root div (replace <div
className="flex..."> with <div ref={ref} className="flex...">). After the
declaration set ChatInterface.displayName = "ChatInterface". Keep existing
internal logic/hooks unchanged and preserve the exported name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/components/ai-panel/tool-result-renderer.tsx`:
- Around line 98-117: The ComponentPreview lazy import can throw and currently
is always rendered, so update ToolResultRenderer to only render the preview when
code (derived from getToolOutputCode/output or input.code) is present and to
guard the lazy component with a Suspense fallback and an error boundary (or
local try-catch fallback UI) to prevent the whole panel from crashing;
specifically, wrap ComponentPreview with React.Suspense (provide a small
fallback like a spinner or placeholder) and surround it with an ErrorBoundary
(or render a stable error message if boundary utility is unavailable) and change
the preview Tab to conditionally render the preview block only when code exists
(use the existing code/isCodeLoading variables and components like
DynamicCodeBlock and ToolLoading to preserve current loading UI).

In `@docs/lib/ai/sitemap-links.ts`:
- Around line 1-3: The external sitemap fetch (the fetch call around line 154)
can block indefinitely—wrap it with an AbortController-based timeout: add a new
timeout constant (e.g., SITEMAP_FETCH_TIMEOUT_MS), create an AbortController,
pass controller.signal into the fetch call, start a setTimeout to call
controller.abort() after the timeout, and clear the timeout on success/failure;
update the sitemap fetching code that currently uses DEFAULT_SITEMAP_URL and
CACHE_TTL_MS to use this AbortSignal so network hangs immediately fall back to
the cached result.

---

Nitpick comments:
In `@docs/components/ai-panel/ai-panel-provider.tsx`:
- Around line 23-68: Wrap the AIPanelProvider functional component with
React.forwardRef so it follows the repository component pattern (export const
AIPanelProvider = forwardRef(function AIPanelProvider(props, ref) { ... })),
accept props as { children }: { children: ReactNode } and pass ref though (even
if unused), then set AIPanelProvider.displayName = "AIPanelProvider"; keep the
existing state/useEffect logic and the AIPanelContext.Provider return unchanged
but ensure the exported symbol is the forwardRef-wrapped AIPanelProvider and
displayName is assigned after the wrap.

In `@docs/components/ai-panel/chat-interface.tsx`:
- Around line 20-175: Replace the plain function component ChatInterface with a
forwardRef-wrapped named export (e.g., export const ChatInterface =
forwardRef<HTMLDivElement, {}>((props, ref) => { ... })) and pass the ref to the
root div (replace <div className="flex..."> with <div ref={ref}
className="flex...">). After the declaration set ChatInterface.displayName =
"ChatInterface". Keep existing internal logic/hooks unchanged and preserve the
exported name.

In `@docs/components/ai-panel/chat-message.tsx`:
- Around line 232-387: The ChatMessage component is declared as a plain function
but must follow repo rules by using React.forwardRef and setting a displayName;
change the export to wrap the component in forwardRef (accepting props { message
}: { message: UIMessage } and a ref) and export that forwarded component, then
set ChatMessage.displayName = "ChatMessage"; ensure internal references (like
useState/useEffect and handlers handleCopy, parseMarkdownCodeBlocks,
getToolRenderContext, getMessageCopyText, DynamicCodeBlock, ToolResultRenderer)
remain unchanged and the forwarded ref is passed to the outermost div so the ref
points at the component DOM node.

In `@docs/components/ai-panel/tool-result-renderer.tsx`:
- Around line 29-36: ToolLoading and ToolResultRenderer are declared as plain
function components; update both to use React.forwardRef so they accept and
forward a ref (e.g., forward a ref to the root div or wrapper element) and
assign a static displayName (ToolLoading.displayName = "ToolLoading",
ToolResultRenderer.displayName = "ToolResultRenderer") to satisfy the repo rule.
Ensure the forwarded ref is typed appropriately (e.g., Ref<HTMLDivElement> or
the actual DOM/component type) and preserve the existing props types (label for
ToolLoading and the props interface used by ToolResultRenderer) when converting
the functions to forwardRef wrappers.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between eee2398 and ecfd65d.

📒 Files selected for processing (9)
  • docs/components/ai-panel/ai-panel-provider.tsx
  • docs/components/ai-panel/chat-interface.tsx
  • docs/components/ai-panel/chat-message.tsx
  • docs/components/ai-panel/parse-markdown-code-blocks.test.ts
  • docs/components/ai-panel/parse-markdown-code-blocks.ts
  • docs/components/ai-panel/tool-result-renderer.tsx
  • docs/lib/ai/sitemap-links.ts
  • docs/lib/ai/system-prompt.ts
  • docs/lib/ai/tools.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/lib/ai/system-prompt.ts

Comment on lines +1 to +3
const DEFAULT_SITEMAP_URL = "https://seed-design.io/sitemap.xml";
const CACHE_TTL_MS = 1000 * 60 * 10;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "sitemap-links.ts" -type f

Repository: daangn/seed-design

Length of output: 92


🏁 Script executed:

git ls-files | grep -i "sitemap"

Repository: daangn/seed-design

Length of output: 110


🏁 Script executed:

cat -n docs/lib/ai/sitemap-links.ts

Repository: daangn/seed-design

Length of output: 6854


🏁 Script executed:

rg "findRelatedLinks" --type ts --type tsx

Repository: daangn/seed-design

Length of output: 89


🏁 Script executed:

rg "findRelatedLinks" -A 3 -B 3

Repository: daangn/seed-design

Length of output: 5229


🏁 Script executed:

find . -name "route.ts" -path "*/api/chat*" -type f

Repository: daangn/seed-design

Length of output: 90


🏁 Script executed:

cat -n docs/app/api/chat/route.ts | head -150

Repository: daangn/seed-design

Length of output: 2278


🏁 Script executed:

cat -n docs/lib/ai/tools.ts | grep -A 15 "findRelatedLinks"

Repository: daangn/seed-design

Length of output: 1428


🏁 Script executed:

rg "timeout|abort|signal" docs/app/api/chat/route.ts docs/lib/ai/

Repository: daangn/seed-design

Length of output: 44


🏁 Script executed:

find . -name "next.config.*" -o -name "tsconfig.json" | head -5

Repository: daangn/seed-design

Length of output: 220


🏁 Script executed:

cat -n docs/next.config.ts 2>/dev/null || echo "File not found"

Repository: daangn/seed-design

Length of output: 76


🏁 Script executed:

cat -n docs/next.config.mjs

Repository: daangn/seed-design

Length of output: 705


외부 sitemap fetch에 타임아웃이 없어 요청이 장시간 블로킹될 수 있습니다.

Line 154의 fetchAbortSignal이 없어 네트워크 이슈 시 응답이 무한정 지연될 수 있습니다. 스트리밍 응답 경로에서 외부 API 호출이 타임아웃 없이 대기하면 요청 스레드가 점유되어 성능 저하로 이어집니다. 타임아웃을 추가해 실패를 빠르게 캐시 fallback으로 넘기세요.

🔧 제안 수정
 const DEFAULT_SITEMAP_URL = "https://seed-design.io/sitemap.xml";
 const CACHE_TTL_MS = 1000 * 60 * 10;
+const FETCH_TIMEOUT_MS = 3000;
 
 async function loadSitemapEntries(): Promise<SitemapEntry[]> {
   const now = Date.now();
   if (sitemapCache && sitemapCache.expiresAt > now) {
     return sitemapCache.entries;
   }
 
   try {
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
+    let response: Response;
+    try {
-    const response = await fetch(DEFAULT_SITEMAP_URL, {
-      headers: {
-        Accept: "application/xml, text/xml;q=0.9, */*;q=0.8",
-      },
-      next: { revalidate: CACHE_TTL_MS / 1000 },
-    });
+      response = await fetch(DEFAULT_SITEMAP_URL, {
+        headers: {
+          Accept: "application/xml, text/xml;q=0.9, */*;q=0.8",
+        },
+        next: { revalidate: CACHE_TTL_MS / 1000 },
+        signal: controller.signal,
+      });
+    } finally {
+      clearTimeout(timeoutId);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/lib/ai/sitemap-links.ts` around lines 1 - 3, The external sitemap fetch
(the fetch call around line 154) can block indefinitely—wrap it with an
AbortController-based timeout: add a new timeout constant (e.g.,
SITEMAP_FETCH_TIMEOUT_MS), create an AbortController, pass controller.signal
into the fetch call, start a setTimeout to call controller.abort() after the
timeout, and clear the timeout on success/failure; update the sitemap fetching
code that currently uses DEFAULT_SITEMAP_URL and CACHE_TTL_MS to use this
AbortSignal so network hangs immediately fall back to the cached result.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
docs/components/ai-panel/chat-message.tsx (1)

117-125: 정적 tool part 파싱/캐스팅 로직을 헬퍼로 통합하는 게 좋겠습니다.

동일한 tool-* 분해/캐스팅이 복수 경로(복사 텍스트 생성, 컨텍스트 계산, 렌더링)에 반복되어 있어 추후 타입 변경 시 한쪽만 수정될 위험이 있습니다. 타입 가드 + 단일 파서 헬퍼로 묶으면 유지보수가 쉬워집니다.

Also applies to: 157-160, 347-355

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/chat-message.tsx` around lines 117 - 125,
Consolidate the repeated "tool-*" parsing/casting into a single helper and use a
type guard: implement an isToolPart(type, obj) type guard plus a
parseToolPart(toolPart) helper that extracts {toolName, input, output} and
replace the inline casts in the blocks that call getToolCopyText, the
context-calculation code, and the rendering code (currently where
part.type.startsWith("tool-") is checked and where getToolCopyText is invoked)
to call the helper and rely on the type guard; update callers to use the
returned toolName/input/output instead of duplicating the string replace and
unsafe casts so all parsing logic lives in one place (affects usages around the
getToolCopyText invocation and the similar parsing at the other noted
locations).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/components/ai-panel/chat-message.tsx`:
- Around line 236-415: Wrap the ChatMessage component with React.forwardRef so
it accepts a ref (e.g. const ChatMessage = forwardRef<HTMLDivElement, { message:
UIMessage }>((props, ref) => { ... })) and attach that ref to the outermost
container div, update the export to use this forwarded component, and set
ChatMessage.displayName = "ChatMessage"; keep the existing prop types and
internal logic unchanged.

---

Nitpick comments:
In `@docs/components/ai-panel/chat-message.tsx`:
- Around line 117-125: Consolidate the repeated "tool-*" parsing/casting into a
single helper and use a type guard: implement an isToolPart(type, obj) type
guard plus a parseToolPart(toolPart) helper that extracts {toolName, input,
output} and replace the inline casts in the blocks that call getToolCopyText,
the context-calculation code, and the rendering code (currently where
part.type.startsWith("tool-") is checked and where getToolCopyText is invoked)
to call the helper and rely on the type guard; update callers to use the
returned toolName/input/output instead of duplicating the string replace and
unsafe casts so all parsing logic lives in one place (affects usages around the
getToolCopyText invocation and the similar parsing at the other noted
locations).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ecfd65d and bd4a98b.

📒 Files selected for processing (2)
  • docs/components/ai-panel/chat-message.tsx
  • docs/components/ai-panel/tool-result-renderer.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/components/ai-panel/tool-result-renderer.tsx

Comment on lines 236 to 415
export function ChatMessage({ message }: { message: UIMessage }) {
const isUser = message.role === "user";
const [isCopied, setIsCopied] = useState(false);
const toolContext = isUser
? {
hasCodeTool: false,
hasComponentExample: false,
hasInstallation: false,
hasRelatedLinks: false,
relatedLinkUrls: [],
}
: getToolRenderContext(message);

const copyText = isUser ? "" : getMessageCopyText(message);
const canCopy = !isUser && copyText.length > 0;

useEffect(() => {
if (!isCopied) return;

const timeout = window.setTimeout(() => {
setIsCopied(false);
}, 2000);

return () => window.clearTimeout(timeout);
}, [isCopied]);

const handleCopy = async () => {
if (!canCopy) return;

try {
await navigator.clipboard.writeText(copyText);
setIsCopied(true);
} catch {
setIsCopied(false);
}
};

return (
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
<div className="max-w-[90%]">
<div
className={`${
isUser
? "bg-fd-primary text-fd-primary-foreground rounded-2xl rounded-br-sm px-3.5 py-2"
: ""
}`}
>
{message.parts.map((part, i) => {
if (part.type === "text" && part.text) {
const segments = !isUser
? parseMarkdownCodeBlocks(part.text)
: [{ type: "text" as const, text: part.text }];
return (
<div key={`text-${i}`}>
{segments.map((segment, segmentIndex) => {
if (segment.type === "code") {
if (
!isUser &&
(toolContext.hasCodeTool ||
toolContext.hasComponentExample ||
toolContext.hasInstallation)
) {
return null;
}

return (
<div key={`segment-code-${segmentIndex}`} className="my-2">
<DynamicCodeBlock lang={segment.language} code={segment.code} />
</div>
);
}

if (!segment.text) {
return null;
}

if (!isUser && isRedundantTextForTools(segment.text, toolContext)) {
return null;
}

return (
<div
key={`segment-text-${segmentIndex}`}
className={`text-sm whitespace-pre-wrap break-words ${
isUser
? ""
: "prose prose-sm dark:prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
}`}
>
{segment.text}
</div>
);
})}
</div>
);
}

// 도구 호출 파트 (dynamic-tool 포함)
if (part.type === "dynamic-tool") {
return (
<ToolResultRenderer
key={part.toolCallId}
toolName={part.toolName}
input={part.input as Record<string, unknown>}
state={part.state}
output={"output" in part ? part.output : undefined}
/>
);
}

// 정적 도구 파트 (type: "tool-{name}")
if (typeof part.type === "string" && part.type.startsWith("tool-")) {
const toolPart = part as unknown as {
type: string;
toolCallId: string;
state: string;
input: Record<string, unknown>;
output?: unknown;
};
const toolName = toolPart.type.replace("tool-", "");
return (
<ToolResultRenderer
key={toolPart.toolCallId}
toolName={toolName}
input={toolPart.input}
state={toolPart.state}
output={toolPart.output}
/>
);
}

return null;
})}
</div>

{!isUser && (
<div className="mt-1">
<ActionButton
type="button"
onClick={handleCopy}
variant="ghost"
layout="iconOnly"
size="xsmall"
bleedX="asPadding"
bleedY="asPadding"
aria-label={isCopied ? "응답 복사됨" : "응답 복사"}
disabled={!canCopy}
>
<AnimatePresence mode="wait" initial={false}>
{isCopied ? (
<m.span
key="copied"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="inline-flex"
>
<Icon svg={<IconCheckmarkCircleLine />} />
</m.span>
) : (
<m.span
key="copy"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="inline-flex"
>
<Icon svg={<IconSquare2StackedLine />} />
</m.span>
)}
</AnimatePresence>
</ActionButton>
</div>
)}
</div>
</div>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "chat-message.tsx$" -t f

Repository: daangn/seed-design

Length of output: 103


🏁 Script executed:

cat -n docs/components/ai-panel/chat-message.tsx | head -50

Repository: daangn/seed-design

Length of output: 1996


🏁 Script executed:

rg -n "export.*ChatMessage|forwardRef|displayName" docs/components/ai-panel/chat-message.tsx

Repository: daangn/seed-design

Length of output: 132


🏁 Script executed:

tail -20 docs/components/ai-panel/chat-message.tsx

Repository: daangn/seed-design

Length of output: 617


ChatMessage 컴포넌트에 forwardRefdisplayName을 적용해주세요.

저장소 코딩 가이드라인(**/*.{tsx,jsx}: React 컴포넌트에 forwardRefdisplayName 사용)에 따라 컴포넌트를 래핑하고 displayName을 설정해야 합니다.

🔧 제안 diff
-import { useEffect, useState } from "react";
+import { forwardRef, useEffect, useState } from "react";
@@
-export function ChatMessage({ message }: { message: UIMessage }) {
+export const ChatMessage = forwardRef<HTMLDivElement, { message: UIMessage }>(
+  ({ message }, ref) => {
   const isUser = message.role === "user";
   const [isCopied, setIsCopied] = useState(false);
@@
-  return (
-    <div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
+  return (
+    <div ref={ref} className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
@@
-  );
-}
+  );
+  },
+);
+
+ChatMessage.displayName = "ChatMessage";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/components/ai-panel/chat-message.tsx` around lines 236 - 415, Wrap the
ChatMessage component with React.forwardRef so it accepts a ref (e.g. const
ChatMessage = forwardRef<HTMLDivElement, { message: UIMessage }>((props, ref) =>
{ ... })) and attach that ref to the outermost container div, update the export
to use this forwarded component, and set ChatMessage.displayName =
"ChatMessage"; keep the existing prop types and internal logic unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant