From f243bfaaec19fd43950428a594e400627bba1eb5 Mon Sep 17 00:00:00 2001 From: junyong Date: Sat, 15 Aug 2026 02:10:08 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat(web):=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=ED=83=AD=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/decisions/DecisionDetailPage.tsx | 47 +-- .../pages/decisions/DecisionsRoutePage.tsx | 2 +- .../components/ApplicationDetailTabs.tsx | 37 ++ .../components/CodeApplicationTab.tsx | 365 ++++++++++++++++++ .../pages/decisions/components/CommitCard.tsx | 94 +++++ .../components/CommitDetailPanel.tsx | 186 +++++++++ .../decisions/components/DecisionHeader.tsx | 18 +- web/src/styles/base/base.css | 30 ++ 8 files changed, 729 insertions(+), 50 deletions(-) create mode 100644 web/src/pages/decisions/components/ApplicationDetailTabs.tsx create mode 100644 web/src/pages/decisions/components/CodeApplicationTab.tsx create mode 100644 web/src/pages/decisions/components/CommitCard.tsx create mode 100644 web/src/pages/decisions/components/CommitDetailPanel.tsx diff --git a/web/src/pages/decisions/DecisionDetailPage.tsx b/web/src/pages/decisions/DecisionDetailPage.tsx index 83567b4..374d64c 100644 --- a/web/src/pages/decisions/DecisionDetailPage.tsx +++ b/web/src/pages/decisions/DecisionDetailPage.tsx @@ -1,49 +1,22 @@ import type { DecisionDetailViewModel } from "@/types/decision"; -import ApplicationStatusCard from "./components/ApplicationStatusCard"; -import CommitTableCard from "./components/CommitTableCard"; -import ContextCard from "./components/ContextCard"; +import ApplicationDetailTabs from "./components/ApplicationDetailTabs"; +import CodeApplicationTab from "./components/CodeApplicationTab"; import DecisionHeader from "./components/DecisionHeader"; -import ReasonsCard from "./components/ReasonsCard"; -import TimelineCard from "./components/TimelineCard"; interface DecisionDetailPageProps { vm: DecisionDetailViewModel; - decisionId: number; } -const DecisionDetailPage = ({ vm, decisionId }: DecisionDetailPageProps) => { +const DecisionDetailPage = ({ vm }: DecisionDetailPageProps) => { return ( -
- + + + - -
-
- - - -
- -
- - -
-
); }; diff --git a/web/src/pages/decisions/DecisionsRoutePage.tsx b/web/src/pages/decisions/DecisionsRoutePage.tsx index 22abde7..84cbcaf 100644 --- a/web/src/pages/decisions/DecisionsRoutePage.tsx +++ b/web/src/pages/decisions/DecisionsRoutePage.tsx @@ -81,7 +81,7 @@ const DecisionsRoutePage = () => { linked_commits: connectedQuery.data?.commits ?? [], }; - return ; + return ; }; export default DecisionsRoutePage; diff --git a/web/src/pages/decisions/components/ApplicationDetailTabs.tsx b/web/src/pages/decisions/components/ApplicationDetailTabs.tsx new file mode 100644 index 0000000..735d32e --- /dev/null +++ b/web/src/pages/decisions/components/ApplicationDetailTabs.tsx @@ -0,0 +1,37 @@ +export type ApplicationDetailTab = "overview" | "relation" | "context" | "code"; + +const TABS: { id: ApplicationDetailTab; label: string }[] = [ + { id: "overview", label: "개요" }, + { id: "relation", label: "관계도" }, + { id: "context", label: "원문 맥락" }, + { id: "code", label: "코드 반영" }, +]; + +interface ApplicationDetailTabsProps { + activeTab: ApplicationDetailTab; +} + +const ApplicationDetailTabs = ({ activeTab }: ApplicationDetailTabsProps) => ( + +); + +export default ApplicationDetailTabs; diff --git a/web/src/pages/decisions/components/CodeApplicationTab.tsx b/web/src/pages/decisions/components/CodeApplicationTab.tsx new file mode 100644 index 0000000..f380f68 --- /dev/null +++ b/web/src/pages/decisions/components/CodeApplicationTab.tsx @@ -0,0 +1,365 @@ +import { useEffect, useRef, useState } from "react"; +import { useCurrentTeam } from "@/hooks/useCurrentTeam"; +import type { + ApplicationConnectedCommit, + ApplicationRecommendedCommit, +} from "@/types/application"; +import type { RepositoryCommitItem } from "@/types/git"; +import { useLinkCommit } from "../hooks/useLinkCommit"; +import { useRepositories } from "../hooks/useRepositories"; +import { useRepositoryCommitsInfinite } from "../hooks/useRepositoryCommitsInfinite"; +import CommitCard from "./CommitCard"; +import CommitDetailPanel, { + type CommitDetailCommit, +} from "./CommitDetailPanel"; + +interface CodeApplicationTabProps { + applicationId: number; + recommendedCommits: ApplicationRecommendedCommit[]; + linkedCommits: ApplicationConnectedCommit[]; +} + +interface DirectCommitListProps { + repositories: { repository_id: number; name: string }[]; + selectedRepositoryId: number | null; + onSelectRepository: (repositoryId: number) => void; + commits: RepositoryCommitItem[]; + onSelectCommit: (commit: RepositoryCommitItem) => void; + onDragStart: (commitId: number) => void; + onDragEnd: () => void; +} + +const DirectCommitList = ({ + repositories, + selectedRepositoryId, + onSelectRepository, + commits, + onSelectCommit, + onDragStart, + onDragEnd, +}: DirectCommitListProps) => ( + <> + +
+ {commits.map((commit) => ( + repository.repository_id === selectedRepositoryId, + )?.name ?? "" + } + variant="direct" + authorName={commit.author_name} + committedDate={commit.date_time} + addedLines={commit.added_lines} + removedLines={commit.deleted_lines} + onClick={() => onSelectCommit(commit)} + onDragStart={() => onDragStart(commit.commit_id)} + onDragEnd={onDragEnd} + /> + ))} +
+ +); + +const RepositoryTabs = ({ + repositories, + selectedRepositoryId, + onSelectRepository, +}: Pick< + DirectCommitListProps, + "repositories" | "selectedRepositoryId" | "onSelectRepository" +>) => { + const scrollRef = useRef(null); + const dragStartRef = useRef<{ x: number; scrollLeft: number } | null>(null); + return ( +
{ + dragStartRef.current = { + x: event.clientX, + scrollLeft: scrollRef.current?.scrollLeft ?? 0, + }; + }} + onMouseMove={(event) => { + if (dragStartRef.current && scrollRef.current) + scrollRef.current.scrollLeft = + dragStartRef.current.scrollLeft - + (event.clientX - dragStartRef.current.x); + }} + onMouseUp={() => { + dragStartRef.current = null; + }} + onMouseLeave={() => { + dragStartRef.current = null; + }} + className="flex min-h-10 gap-1 overflow-x-auto px-0 py-1" + > + {repositories.map((repository) => ( + + ))} +
+ ); +}; + +const CodeApplicationTab = ({ + applicationId, + recommendedCommits, + linkedCommits, +}: CodeApplicationTabProps) => { + const [sourceTab, setSourceTab] = useState<"recommended" | "direct">( + "recommended", + ); + const [selectedCommit, setSelectedCommit] = + useState(null); + const [detailCollapsed, setDetailCollapsed] = useState(true); + const [isDraggingCommit, setIsDraggingCommit] = useState(false); + const [isScrolling, setIsScrolling] = useState(false); + const scrollTimerRef = useRef | null>(null); + const [draggedCommitId, setDraggedCommitId] = useState(null); + const [selectedRepositoryId, setSelectedRepositoryId] = useState< + number | null + >(null); + const { teamId } = useCurrentTeam(); + const { data: repositories = [] } = useRepositories(teamId); + const { commits: directCommits } = useRepositoryCommitsInfinite( + selectedRepositoryId, + { enabled: selectedRepositoryId != null }, + ); + const directCommitsByHash = new Map( + directCommits.map((commit) => [commit.hash, commit]), + ); + const { linkCommits, errorMessage: linkErrorMessage } = + useLinkCommit(applicationId); + + useEffect(() => { + if (selectedRepositoryId == null && repositories[0]) { + setSelectedRepositoryId(repositories[0].repository_id); + } + }, [repositories, selectedRepositoryId]); + + const showDetail = (commit: CommitDetailCommit) => { + setSelectedCommit(commit); + setDetailCollapsed(false); + }; + const startDragging = (commitId: string | number) => { + const numericId = Number(commitId); + setDraggedCommitId(Number.isInteger(numericId) ? numericId : null); + setIsDraggingCommit(true); + }; + const finishDragging = () => { + setDraggedCommitId(null); + setIsDraggingCommit(false); + }; + const handleDrop = () => { + if (draggedCommitId != null) linkCommits([draggedCommitId]); + finishDragging(); + }; + const handleScroll = () => { + setIsScrolling(true); + if (scrollTimerRef.current) clearTimeout(scrollTimerRef.current); + scrollTimerRef.current = setTimeout(() => setIsScrolling(false), 700); + }; + return ( +
+
+ + + 추천{" "} + + {recommendedCommits.length} + + + + + + 연결됨{" "} + + {linkedCommits.length} + + + + 전체 {recommendedCommits.length + linkedCommits.length} + +
+
+
+
+
+
+ +

+ 커밋 가져오기 +

+
+
+ + +
+
+
+ {sourceTab === "recommended" ? ( + recommendedCommits.map((commit) => + (() => { + const matchedCommit = directCommitsByHash.get( + commit.commit_hash, + ); + return ( + + showDetail({ + repositoryName: commit.repository_name, + hash: commit.commit_hash, + message: commit.message, + reason: commit.reason, + repositoryId: repositories.find( + (repository) => + repository.name === commit.repository_name, + )?.repository_id, + authorName: matchedCommit?.author_name, + committedDate: matchedCommit?.date_time, + addedLines: matchedCommit?.added_lines, + removedLines: matchedCommit?.deleted_lines, + }) + } + onDragStart={() => startDragging(commit.commit_id)} + onDragEnd={finishDragging} + /> + ); + })(), + ) + ) : ( + + showDetail({ + repositoryName: + repositories.find( + (repository) => + repository.repository_id === selectedRepositoryId, + )?.name ?? "", + hash: commit.hash, + message: commit.message, + repositoryId: selectedRepositoryId ?? undefined, + authorName: commit.author_name, + committedDate: commit.date_time, + addedLines: commit.added_lines, + removedLines: commit.deleted_lines, + }) + } + onDragStart={startDragging} + onDragEnd={finishDragging} + /> + )} +
+
+
+
+
+ +

+ 연결된 커밋 ({linkedCommits.length}) +

+
+
+ {linkErrorMessage ? ( +

+ {linkErrorMessage} +

+ ) : null} +
{ + event.preventDefault(); + setIsDraggingCommit(true); + }} + onDrop={handleDrop} + onScroll={handleScroll} + className={`application-scroll relative flex flex-1 flex-col overflow-y-auto ${isScrolling ? "is-scrolling" : ""}`} + > + {linkedCommits.map((commit) => { + const matchedCommit = directCommitsByHash.get( + commit.commit_hash, + ); + return ( + + ); + })} + {isDraggingCommit ? ( +
+

+ 여기에 드래그하여 연결 +

+

+ 커밋을 이 영역에 드롭하면 연결됩니다 +

+
+ ) : null} +
+
+
+ setDetailCollapsed((value) => !value)} + /> +
+
+ ); +}; + +export default CodeApplicationTab; diff --git a/web/src/pages/decisions/components/CommitCard.tsx b/web/src/pages/decisions/components/CommitCard.tsx new file mode 100644 index 0000000..47515bf --- /dev/null +++ b/web/src/pages/decisions/components/CommitCard.tsx @@ -0,0 +1,94 @@ +import { formatCommittedDate } from "@/utils/date"; +import CommitHashBadge from "./CommitHashBadge"; + +export type CommitCardVariant = "recommended" | "direct" | "linked"; + +interface CommitCardProps { + hash: string; + message: string; + repositoryName: string; + variant: CommitCardVariant; + reason?: string; + committedDate?: string; + authorName?: string; + addedLines?: number; + removedLines?: number; + selected?: boolean; + onClick?: () => void; + onDragStart?: () => void; + onDragEnd?: () => void; +} + +const CommitCard = ({ + hash, + message, + repositoryName, + variant, + reason, + committedDate, + authorName, + addedLines, + removedLines, + selected = false, + onClick, + onDragStart, + onDragEnd, +}: CommitCardProps) => ( + +); + +export default CommitCard; diff --git a/web/src/pages/decisions/components/CommitDetailPanel.tsx b/web/src/pages/decisions/components/CommitDetailPanel.tsx new file mode 100644 index 0000000..3efaae5 --- /dev/null +++ b/web/src/pages/decisions/components/CommitDetailPanel.tsx @@ -0,0 +1,186 @@ +import IconArrowRight from "@/assets/icons/arrow/ic_arrow_right_md.svg?react"; +import { Icon } from "@/components/common/Icon"; +import { useGetCommitDetail } from "@/pages/git/hooks/useGetCommitDetail"; +import { formatCommittedDate } from "@/utils/date"; +import CommitHashBadge from "./CommitHashBadge"; + +export interface CommitDetailCommit { + repositoryName: string; + hash: string; + message: string; + reason?: string; + repositoryId?: number; + authorName?: string; + committedDate?: string; + addedLines?: number; + removedLines?: number; +} + +interface CommitDetailPanelProps { + commit: CommitDetailCommit | null; + collapsed: boolean; + onToggle: () => void; +} + +const CommitDetailPanel = ({ + commit, + collapsed, + onToggle, +}: CommitDetailPanelProps) => { + const detailQuery = useGetCommitDetail( + collapsed ? null : (commit?.repositoryId ?? null), + commit?.hash, + ); + const detail = detailQuery.data; + + return ( + + ); +}; + +export default CommitDetailPanel; diff --git a/web/src/pages/decisions/components/DecisionHeader.tsx b/web/src/pages/decisions/components/DecisionHeader.tsx index 2e092b1..9fcec9d 100644 --- a/web/src/pages/decisions/components/DecisionHeader.tsx +++ b/web/src/pages/decisions/components/DecisionHeader.tsx @@ -1,11 +1,8 @@ import IconCircleUser from "@/assets/icons/user/ic_circle_user.svg?react"; import { Icon } from "@/components/common/Icon"; -import type { DecisionConfidence, DecisionMeetingMeta } from "@/types/decision"; -import ConfidenceBadge from "./ConfidenceBadge"; +import type { DecisionMeetingMeta } from "@/types/decision"; interface DecisionHeaderProps { - name: string; - confidence: DecisionConfidence; meta: DecisionMeetingMeta; } @@ -13,18 +10,15 @@ const Dot = () => ( · ); -const DecisionHeader = ({ name, confidence, meta }: DecisionHeaderProps) => { +const DecisionHeader = ({ meta }: DecisionHeaderProps) => { const visibleAvatars = meta.participants.slice(0, 5); return ( -
-
-

{name}

- -
- +
- {meta.meeting_name} + + {meta.meeting_name} + {meta.meeting_date} diff --git a/web/src/styles/base/base.css b/web/src/styles/base/base.css index 56f5f20..85548a2 100644 --- a/web/src/styles/base/base.css +++ b/web/src/styles/base/base.css @@ -68,3 +68,33 @@ body { box-shadow: none; -webkit-box-shadow: none; } + +.application-scroll::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.application-scroll::-webkit-scrollbar-track { + background-color: transparent; + box-shadow: none; +} + +.application-scroll::-webkit-scrollbar-thumb { + background-color: transparent; + box-shadow: none; + -webkit-box-shadow: none; +} + +.application-scroll.is-scrolling::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.application-scroll.is-scrolling::-webkit-scrollbar-track { + background-color: transparent; + box-shadow: none; +} + +.application-scroll.is-scrolling::-webkit-scrollbar-thumb { + background-color: var(--color-gray-400); +} From 98680c42f9d49d480ecaeefdc3a456cfc210ab55 Mon Sep 17 00:00:00 2001 From: junyong Date: Sat, 15 Aug 2026 04:11:34 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat(web):=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=ED=83=AD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/decisions/DecisionDetailPage.tsx | 2 + .../components/CodeApplicationTab.tsx | 69 +++++- .../pages/decisions/components/CommitCard.tsx | 28 +-- .../components/CommitDetailPanel.tsx | 211 ++++++++++-------- web/src/styles/base/base.css | 17 +- 5 files changed, 202 insertions(+), 125 deletions(-) diff --git a/web/src/pages/decisions/DecisionDetailPage.tsx b/web/src/pages/decisions/DecisionDetailPage.tsx index 374d64c..78ed2c3 100644 --- a/web/src/pages/decisions/DecisionDetailPage.tsx +++ b/web/src/pages/decisions/DecisionDetailPage.tsx @@ -14,6 +14,8 @@ const DecisionDetailPage = ({ vm }: DecisionDetailPageProps) => { reason.title)} recommendedCommits={vm.recommended_commits} linkedCommits={vm.linked_commits} /> diff --git a/web/src/pages/decisions/components/CodeApplicationTab.tsx b/web/src/pages/decisions/components/CodeApplicationTab.tsx index f380f68..5bf75fc 100644 --- a/web/src/pages/decisions/components/CodeApplicationTab.tsx +++ b/web/src/pages/decisions/components/CodeApplicationTab.tsx @@ -8,6 +8,7 @@ import type { RepositoryCommitItem } from "@/types/git"; import { useLinkCommit } from "../hooks/useLinkCommit"; import { useRepositories } from "../hooks/useRepositories"; import { useRepositoryCommitsInfinite } from "../hooks/useRepositoryCommitsInfinite"; +import { useUnlinkCommit } from "../hooks/useUnlinkCommit"; import CommitCard from "./CommitCard"; import CommitDetailPanel, { type CommitDetailCommit, @@ -15,6 +16,8 @@ import CommitDetailPanel, { interface CodeApplicationTabProps { applicationId: number; + applicationName: string; + keywords: string[]; recommendedCommits: ApplicationRecommendedCommit[]; linkedCommits: ApplicationConnectedCommit[]; } @@ -119,6 +122,8 @@ const RepositoryTabs = ({ const CodeApplicationTab = ({ applicationId, + applicationName, + keywords, recommendedCommits, linkedCommits, }: CodeApplicationTabProps) => { @@ -144,8 +149,25 @@ const CodeApplicationTab = ({ const directCommitsByHash = new Map( directCommits.map((commit) => [commit.hash, commit]), ); - const { linkCommits, errorMessage: linkErrorMessage } = - useLinkCommit(applicationId); + const { + linkCommits, + errorMessage: linkErrorMessage, + isPending: isLinkPending, + } = useLinkCommit(applicationId, { + onSuccess: () => + setSelectedCommit((commit) => + commit ? { ...commit, isConnected: true } : null, + ), + }); + const { unlinkCommit, isPending: isUnlinkPending } = useUnlinkCommit( + applicationId, + { + onSuccess: () => + setSelectedCommit((commit) => + commit ? { ...commit, isConnected: false } : null, + ), + }, + ); useEffect(() => { if (selectedRepositoryId == null && repositories[0]) { @@ -199,8 +221,10 @@ const CodeApplicationTab = ({
-
-
+
+

@@ -252,6 +276,11 @@ const CodeApplicationTab = ({ repositoryName: commit.repository_name, hash: commit.commit_hash, message: commit.message, + commitId: Number(commit.commit_id), + isConnected: linkedCommits.some( + (linkedCommit) => + linkedCommit.commit_hash === commit.commit_hash, + ), reason: commit.reason, repositoryId: repositories.find( (repository) => @@ -259,8 +288,6 @@ const CodeApplicationTab = ({ )?.repository_id, authorName: matchedCommit?.author_name, committedDate: matchedCommit?.date_time, - addedLines: matchedCommit?.added_lines, - removedLines: matchedCommit?.deleted_lines, }) } onDragStart={() => startDragging(commit.commit_id)} @@ -284,11 +311,14 @@ const CodeApplicationTab = ({ )?.name ?? "", hash: commit.hash, message: commit.message, + commitId: commit.commit_id, + isConnected: linkedCommits.some( + (linkedCommit) => + linkedCommit.commit_hash === commit.hash, + ), repositoryId: selectedRepositoryId ?? undefined, authorName: commit.author_name, committedDate: commit.date_time, - addedLines: commit.added_lines, - removedLines: commit.deleted_lines, }) } onDragStart={startDragging} @@ -297,7 +327,7 @@ const CodeApplicationTab = ({ )}

-
+
@@ -336,6 +366,22 @@ const CodeApplicationTab = ({ addedLines={matchedCommit?.added_lines} removedLines={matchedCommit?.deleted_lines} variant="linked" + selected={selectedCommit?.hash === commit.commit_hash} + onClick={() => + showDetail({ + repositoryName: commit.repository_name, + hash: commit.commit_hash, + message: commit.message, + commitId: commit.commit_id, + isConnected: true, + repositoryId: repositories.find( + (repository) => + repository.name === commit.repository_name, + )?.repository_id, + authorName: matchedCommit?.author_name, + committedDate: commit.committed_date, + }) + } /> ); })} @@ -353,9 +399,14 @@ const CodeApplicationTab = ({
setDetailCollapsed((value) => !value)} + onConnect={(commitId) => linkCommits([commitId])} + onUnlink={unlinkCommit} + isPending={isLinkPending || isUnlinkPending} />
diff --git a/web/src/pages/decisions/components/CommitCard.tsx b/web/src/pages/decisions/components/CommitCard.tsx index 47515bf..80cbef1 100644 --- a/web/src/pages/decisions/components/CommitCard.tsx +++ b/web/src/pages/decisions/components/CommitCard.tsx @@ -42,49 +42,51 @@ const CommitCard = ({ onDragEnd={onDragEnd} className={`flex w-full flex-col gap-1 border-b px-2 py-2.5 text-left transition-colors ${ selected - ? "border-(--color-border-brand) bg-(--color-bg-brand-subtle)" + ? "mx-1 w-auto rounded-lg border-b-0 bg-(--color-bg-brand-subtle)" : "border-(--color-border-default) bg-(--color-bg-surface) hover:bg-(--color-action-hover)" }`} > -
+
{variant !== "linked" ? (
- + {repositoryName} {authorName ? ( - + {authorName} ) : null} {committedDate ? ( - + {formatCommittedDate(committedDate)} ) : null} {addedLines != null || removedLines != null ? ( - - +{addedLines ?? 0} - - -{removedLines ?? 0} - + + {addedLines != null ? ( + +{addedLines} + ) : null} + {removedLines != null ? ( + -{removedLines} + ) : null} ) : null}
{variant === "recommended" && reason ? ( -

+

{reason}

) : null} diff --git a/web/src/pages/decisions/components/CommitDetailPanel.tsx b/web/src/pages/decisions/components/CommitDetailPanel.tsx index 3efaae5..bd1dd6f 100644 --- a/web/src/pages/decisions/components/CommitDetailPanel.tsx +++ b/web/src/pages/decisions/components/CommitDetailPanel.tsx @@ -8,24 +8,34 @@ export interface CommitDetailCommit { repositoryName: string; hash: string; message: string; + commitId?: number; + isConnected: boolean; reason?: string; repositoryId?: number; authorName?: string; committedDate?: string; - addedLines?: number; - removedLines?: number; } interface CommitDetailPanelProps { + applicationName: string; + keywords: string[]; commit: CommitDetailCommit | null; collapsed: boolean; onToggle: () => void; + onConnect: (commitId: number) => void; + onUnlink: (commitId: number) => void; + isPending: boolean; } const CommitDetailPanel = ({ + applicationName, + keywords, commit, collapsed, onToggle, + onConnect, + onUnlink, + isPending, }: CommitDetailPanelProps) => { const detailQuery = useGetCommitDetail( collapsed ? null : (commit?.repositoryId ?? null), @@ -35,7 +45,7 @@ const CommitDetailPanel = ({ return (