-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(design-system): 팝업 공통 컴포넌트 #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
fe6dc65
ba77031
caaaa45
21380a7
545e6ce
c982724
942b4b1
525ed2b
aec1ac0
879c1cc
526699d
d0131c9
d6927fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. story 중앙배치~~! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import Popup from './Popup'; | ||
|
|
||
| const meta: Meta<typeof Popup> = { | ||
| title: 'Components/Popup', | ||
| component: Popup, | ||
| tags: ['autodocs'], | ||
| argTypes: { | ||
| type: { | ||
| control: 'radio', | ||
| options: ['input', 'subtext', 'default'], | ||
| description: '팝업의 내용 타입', | ||
|
Comment on lines
+4
to
+15
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 팝업들 중앙 배치하기~~ |
||
| }, | ||
| title: { | ||
| control: 'text', | ||
| description: '팝업 제목', | ||
| }, | ||
| subtext: { | ||
| control: 'text', | ||
| description: 'subtext 타입일 때 보여줄 보조 문구', | ||
| }, | ||
| placeholder: { | ||
| control: 'text', | ||
| description: 'input 타입일 때 placeholder', | ||
| }, | ||
| left: { | ||
| control: 'text', | ||
| description: '왼쪽 버튼 텍스트', | ||
| }, | ||
| right: { | ||
| control: 'text', | ||
| description: '오른쪽 버튼 텍스트', | ||
| }, | ||
| isError: { | ||
| control: 'boolean', | ||
| description: 'input 타입일 때 에러 상태 여부', | ||
| }, | ||
| helperText: { | ||
| control: 'text', | ||
| description: '에러 상태일 때 표시할 도움말', | ||
| }, | ||
| }, | ||
| }; | ||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof Popup>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| type: 'default', | ||
| title: '기본 팝업', | ||
| left: '취소', | ||
| right: '확인', | ||
| }, | ||
| }; | ||
|
|
||
| export const WithInput: Story = { | ||
| args: { | ||
| type: 'input', | ||
| title: '카테고리 입력', | ||
| placeholder: '카테고리 제목을 입력해주세요', | ||
| left: '취소', | ||
| right: '저장', | ||
| }, | ||
| }; | ||
|
|
||
| export const WithErrorInput: Story = { | ||
| args: { | ||
| type: 'input', | ||
| title: '카테고리 입력', | ||
| placeholder: '카테고리 제목을 입력해주세요', | ||
| isError: true, | ||
| helperText: '10자 이내로 입력해주세요', | ||
| left: '취소', | ||
| right: '저장', | ||
| }, | ||
| }; | ||
|
|
||
| export const WithSubtext: Story = { | ||
| args: { | ||
| type: 'subtext', | ||
| title: '알림', | ||
| subtext: '카테고리가 정상적으로 저장되었습니다.', | ||
| left: '닫기', | ||
| right: '확인', | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,52 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import Input from '../input/Input'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BasePopupProps } from './types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const Popup = ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| subtext, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| title, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| left, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| right, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| helperText, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| isError, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onLeftClick, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onRightClick, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }: BasePopupProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+17
to
+28
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 디자인을 보니 설계하기가 어려우셨을 것 같습니다! 꽤 많은 타입의
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추후 계속 리팩토링 요소 보이는대로 작업해보겠습니다! 감사합니다 |
||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="bg-white-bg flex w-[26rem] cursor-pointer flex-col items-center justify-center rounded-[1.2rem] bg-white px-[1.6rem] py-[2.4rem] shadow-[0_0_32px_0_rgba(0,0,0,0.10)]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="sub2-sb text-font-black-1 pb-[0.8rem]">{title}</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {type === 'input' && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-full py-[0.8rem]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <Input | ||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder={placeholder} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| helperText={helperText} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| isError={isError} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {type === 'subtext' && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="body3-r text-font-gray-2 w-full py-[0.8rem] text-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {subtext} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* type===default일 떄는 아무것도 없음 */} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-row items-center justify-center gap-[1.2rem] pt-[0.8rem]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className="border-gray200 sub5-sb bg-white-bg text-font-black-1 w-[10.8rem] rounded-[0.4rem] border py-[0.85rem]" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={onLeftClick} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {left} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className="sub5-sb bg-gray900 text-white-bg w-[10.8rem] rounded-[0.4rem] py-[0.85rem]" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={onRightClick} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {right} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 버튼: onClick 노출 및 type="button" 지정 폼 내부에서 의도치 않은 submit을 막고, 액션을 외부에 전달하세요. (가능하면 디자인 시스템의 Button 컴포넌트 사용도 고려해 주세요.) - <button className="border-gray200 sub5-sb bg-white-bg text-font-black-1 w-[10.8rem] rounded-[0.4rem] border py-[0.85rem]">
+ <button
+ type="button"
+ onClick={onLeftClick}
+ className="border-gray200 sub5-sb bg-white-bg text-font-black-1 w-[10.8rem] rounded-[0.4rem] border py-[0.85rem]"
+ >
{left}
</button>
- <button className="sub5-sb bg-gray900 text-white-bg w-[10.8rem] rounded-[0.4rem] py-[0.85rem]">
+ <button
+ type="button"
+ onClick={onRightClick}
+ className="sub5-sb bg-gray900 text-white-bg w-[10.8rem] rounded-[0.4rem] py-[0.85rem]"
+ >
{right}
</button>추가로, 내부에서 직접 스타일링하는 대신 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export default Popup; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||||||
| import { useState } from 'react'; | ||||||
| import PopupContainer from './PopupContainer'; | ||||||
|
|
||||||
| const PopupContainerWrapper = ( | ||||||
| props: React.ComponentProps<typeof PopupContainer> | ||||||
| ) => <PopupContainer {...props} />; | ||||||
|
|
||||||
| const meta: Meta<typeof PopupContainerWrapper> = { | ||||||
| title: 'Components/PopupContainer', | ||||||
| component: PopupContainerWrapper, | ||||||
| tags: ['autodocs'], | ||||||
| parameters: { | ||||||
| layout: 'centered', | ||||||
| }, | ||||||
| }; | ||||||
| export default meta; | ||||||
|
|
||||||
| type Story = StoryObj<typeof PopupContainer>; | ||||||
|
|
||||||
|
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Story 제네릭이 meta의 component와 불일치
-type Story = StoryObj<typeof PopupContainer>;
+type Story = StoryObj<typeof PopupContainerWrapper>;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| // 인터랙션 가능한 예시 | ||||||
| const WithTriggerButtonComponent = () => { | ||||||
| const [open, setOpen] = useState(false); | ||||||
|
|
||||||
| return ( | ||||||
| <div className="flex flex-col items-center gap-4"> | ||||||
| <button | ||||||
| className="rounded bg-blue-500 px-4 py-2 text-white" | ||||||
| onClick={() => setOpen(true)} | ||||||
| > | ||||||
| 팝업 열기 | ||||||
| </button> | ||||||
|
|
||||||
| <PopupContainer | ||||||
| isOpen={open} | ||||||
| onClose={() => setOpen(false)} | ||||||
| type="input" | ||||||
| title="카테고리 입력" | ||||||
| left="취소" | ||||||
| right="저장" | ||||||
| placeholder="카테고리 제목을 입력해주세요" | ||||||
| /> | ||||||
| </div> | ||||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| export const WithTriggerButton: Story = { | ||||||
| render: () => <WithTriggerButtonComponent />, | ||||||
| }; | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createPortal } from 'react-dom'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Popup from './Popup'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { PopupContainerProps } from './types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const PopupContainer = ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpen, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...popupProps | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: PopupContainerProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ESC 키로 닫는 것 정도 (외부 클릭은 안되게! : 어차피 x박스나 취소버튼이 있음) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그렇다면 모달/팝업은 요소 외부 클릭 시 닫히는 경우가 없나요? 기/디 분들과 상의는 필요 없는지 궁금합니다!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넹! 확인해보겠습니다!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확인해보니! 닫기나 x버튼 이외의 외부요소 클릭 제어는 필요없다고 하십니다! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isOpen) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleEsc = (e: KeyboardEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key === 'Escape') onClose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.addEventListener('keydown', handleEsc); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => window.removeEventListener('keydown', handleEsc); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [isOpen, onClose]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+35
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 재사용이 다른 곳에서도 된다면 Hook으로 분리해도 좋겠네요 👍 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isOpen) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return createPortal( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="fixed inset-0 z-10 flex items-center justify-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute inset-0 bg-[#00000099]" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="relative"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Popup {...popupProps} onLeftClick={onClose} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.body // body 위에서 렌더링 되게 함! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion SSR 안전성 보강 및 포털 대상 가드 필요
또한 현재 -import { useEffect } from 'react';
+import { useEffect, useRef, useState } from 'react';
...
-const PopupContainer = ({
+const PopupContainer = ({
isOpen,
onClose,
...popupProps
}: PopupContainerProps) => {
+ const [mounted, setMounted] = useState(false);
+ const dialogRef = useRef<HTMLDivElement>(null);
+ const { onLeftClick, ...restPopupProps } = popupProps;
+
+ // 클라이언트 마운트 확인
+ useEffect(() => {
+ setMounted(true);
+ return () => setMounted(false);
+ }, []);
// ESC 키로 닫는 것 정도 (외부 클릭은 안되게! : 어차피 x박스나 취소버튼이 있음)
useEffect(() => {
if (!isOpen) return;
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [isOpen, onClose]);
- if (!isOpen) return null;
+ if (!isOpen || !mounted) return null;
- return createPortal(
+ const portalTarget =
+ typeof document !== 'undefined' ? document.body : null;
+
+ return portalTarget ? createPortal(
<div className="fixed inset-0 z-10 flex items-center justify-center">
- <div className="absolute inset-0 bg-[#00000099]" />
- <div className="relative">
- <Popup {...popupProps} onLeftClick={onClose} />
+ <div className="absolute inset-0 bg-black/60" />
+ <div
+ className="relative outline-none"
+ ref={dialogRef}
+ role="dialog"
+ aria-modal="true"
+ aria-label={popupProps.title}
+ tabIndex={-1}
+ >
+ <Popup
+ {...restPopupProps}
+ onLeftClick={() => {
+ onLeftClick?.();
+ onClose();
+ }}
+ />
</div>
</div>,
- document.body // body 위에서 렌더링 되게 함!
- );
+ portalTarget // body 위에서 렌더링 되게 함!
+ ) : null;
추가로, 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default PopupContainer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| export type PopupType = 'input' | 'subtext' | 'default'; | ||
|
|
||
| export interface BasePopupProps { | ||
|
||
| type: PopupType; | ||
| title: string; | ||
| left: string; | ||
| right: string; | ||
| subtext?: string; | ||
| placeholder?: string; | ||
| isError?: boolean; | ||
| helperText?: string; | ||
| onLeftClick?: () => void; | ||
| onRightClick?: () => void; | ||
| } | ||
|
|
||
| export interface PopupContainerProps extends BasePopupProps { | ||
| isOpen: boolean; | ||
| onClose: () => void; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
ref를 선택적(prop)으로 전환 — React 19 채택 전제에서는 합리적
이 변경은 React 19의 “함수 컴포넌트가 ref를 일반 prop으로 받는다”는 팀 컨벤션/러닝과 일치합니다. 하위 호환성(React 18 이하)에서는 깨질 수 있으므로, 워크스페이스 전반이 React 19.x 타입을 사용 중인지 한 번 확인을 부탁드립니다.
다음 스크립트로 워크스페이스의 React/타입 버전을 점검해 주세요.
🏁 Script executed:
Length of output: 3971
React 19 도입 기반 검증 — @types/react 버전 일관성 확보 필요
이 PR에서
ref를 선택적 prop으로 전환한 것은 React 19의 컨벤션에 부합하나, 워크스페이스 전반의 타입 일관성을 위해 다음 위치의@types/react버전을 React 19.x와 맞춰주세요.• apps/extension/package.json
– 현재:
@types/react: ^18.3.5– 수정: React 19.x 타입(
^19.1.10등)으로 업데이트• 루트 package.json
– 현재:
@types/react: 19– 수정: 명확한 SemVer(
^19.1.x) 형식으로 변경• 필요 시, 다른 패키지(특히 devDependencies)에
@types/react가 있다면 동일하게 점검 및 업데이트위 조치 후, React 19 기준의 ref prop 사용이 전체 코드베이스에서 안전하게 작동함을 보장할 수 있습니다.
🤖 Prompt for AI Agents