-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/madp 92 #6
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
base: main
Are you sure you want to change the base?
Feat/madp 92 #6
Changes from all commits
eaac009
0c29235
9fe18f7
d791945
96435fe
2b35468
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| 'use client'; | ||
|
|
||
| import { useState, useEffect } from 'react'; | ||
| import Image from 'next/image'; | ||
| import * as S from './style'; | ||
| import ChatSection from '@/components/ui/Agent/ChatSection/ui'; | ||
| import SearchSection from '@/components/ui/Agent/SearchSection/ui'; | ||
|
|
||
| // νμ¬ νΉλ³ν λ°±μλ κ°λ° μ§νμ΄ μμ΄ λ©λλ‘ κ³ μ λ μ μΆλ ₯μ μ¬μ©ν¨ | ||
| const USER_MESSAGE = "μ΅μ μ μ¬μΈ νλ‘μ νΈμ CPU μ¬μ©λμ μκ³ μΆμ΄"; | ||
| const AI_MESSAGE = "μ’μ΅λλ€, λ€μμ μ΅μ μ μ¬μΈ νλ‘μ νΈμ CPU μ¬μ©λ μ λλ€."; | ||
|
|
||
| export default function AgentPage() { | ||
| const [isChatStarted, setIsChatStarted] = useState(false); | ||
| const [streamedText, setStreamedText] = useState(""); | ||
| const [inputValue, setInputValue] = useState(""); | ||
|
|
||
| useEffect(() => { | ||
| if (isChatStarted) { | ||
| let currentIndex = 0; | ||
| const interval = setInterval(() => { | ||
| if (currentIndex <= AI_MESSAGE.length) { | ||
| setStreamedText(AI_MESSAGE.slice(0, currentIndex)); | ||
| currentIndex++; | ||
| } else { | ||
| clearInterval(interval); | ||
| } | ||
| }, 50); | ||
|
|
||
| return () => clearInterval(interval); | ||
| } | ||
| }, [isChatStarted]); | ||
|
|
||
| const handleSearch = (e?: React.FormEvent) => { | ||
| e?.preventDefault(); | ||
| if (!inputValue.trim()) return; | ||
| if (!isChatStarted) { | ||
| setIsChatStarted(true); | ||
| } | ||
| setInputValue(""); | ||
| }; | ||
|
|
||
| const handleKeyDown = (e: React.KeyboardEvent) => { | ||
| if (e.key === 'Enter' && !e.nativeEvent.isComposing) { | ||
| handleSearch(); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <S.Container isChat={isChatStarted}> | ||
| {!isChatStarted && ( | ||
| <S.LogoSection> | ||
| <Image | ||
| src="/assets/logo.svg" | ||
| alt="M-ADP Logo" | ||
| width={100} | ||
| height={92} | ||
| priority | ||
| /> | ||
| <S.LogoTitle>M-ADP</S.LogoTitle> | ||
| </S.LogoSection> | ||
| )} | ||
|
|
||
| {isChatStarted && ( | ||
| <ChatSection | ||
| userMessage={USER_MESSAGE} | ||
| streamedText={streamedText} | ||
| /> | ||
| )} | ||
|
|
||
| <SearchSection | ||
| inputValue={inputValue} | ||
| setInputValue={setInputValue} | ||
| handleSearch={handleSearch} | ||
| handleKeyDown={handleKeyDown} | ||
| /> | ||
| </S.Container> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import styled from '@emotion/styled'; | ||
| import { colors } from '@/styles/colors'; | ||
|
|
||
| export const Container = styled.div<{ isChat?: boolean }>` | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: ${({ isChat }) => (isChat ? 'space-between' : 'center')}; | ||
| width: 100%; | ||
| height: 100%; | ||
| background-color: #ffffff; | ||
| padding: ${({ isChat }) => (isChat ? '40px 0' : '0')}; | ||
| box-sizing: border-box; | ||
| `; | ||
|
|
||
| export const LogoSection = styled.div` | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 28px; | ||
| margin-bottom: 60px; | ||
| `; | ||
|
|
||
| export const LogoTitle = styled.span` | ||
| font-family: 'IBM Plex Sans KR', sans-serif; | ||
| font-weight: 700; | ||
| font-size: 80px; | ||
| line-height: normal; | ||
| color: ${colors.black[300]}; | ||
| margin: 0; | ||
| `; | ||
|
|
||
| // Moved to components/ui/Agent/... | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,27 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React, { useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useServerInsertedHTML } from 'next/navigation'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CacheProvider } from '@emotion/react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import createCache from '@emotion/cache'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function EmotionRegistry({ children }: { children: React.ReactNode }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [cache] = useState(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cache = createCache({ key: 'css' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cache.compat = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cache; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useServerInsertedHTML(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <style | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dangerouslySetInnerHTML={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __html: Object.values(cache.inserted).join(' '), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+24
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. Styles accumulate and get re-injected on every
Proposed fix β flush inserted cache entries useServerInsertedHTML(() => {
+ const entries = { ...cache.inserted };
+ // Flush inserted entries to avoid re-injecting on the next call
+ Object.keys(entries).forEach((key) => {
+ delete cache.inserted[key];
+ });
+ if (Object.keys(entries).length === 0) {
+ return null;
+ }
return (
<style
- data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`}
+ data-emotion={`${cache.key} ${Object.keys(entries).join(' ')}`}
dangerouslySetInnerHTML={{
- __html: Object.values(cache.inserted).join(' '),
+ __html: Object.values(entries).join(' '),
}}
/>
);
});π Committable suggestion
Suggested change
π§° Toolsπͺ ast-grep (0.40.5)[warning] 18-18: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks. (react-unsafe-html-injection) πͺ Biome (2.3.14)[error] 19-19: Avoid passing content using the dangerouslySetInnerHTML prop. Setting content using code can expose users to cross-site scripting (XSS) attacks (lint/security/noDangerouslySetInnerHtml) π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <CacheProvider value={cache}>{children}</CacheProvider>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import styled from '@emotion/styled'; | ||
| import { colors } from '@/styles/colors'; | ||
| import { typography } from '@/styles/typography'; | ||
|
|
||
| export const ChatArea = styled.div` | ||
| display: flex; | ||
| flex-direction: column; | ||
| width: 100%; | ||
| max-width: 1240px; | ||
| height: 100%; | ||
| overflow-y: auto; | ||
| padding: 20px; | ||
| gap: 20px; | ||
| flex: 1; | ||
| `; | ||
|
|
||
| export const MessageRow = styled.div` | ||
| display: flex; | ||
| gap: 20px; | ||
| align-items: flex-start; | ||
| width: 100%; | ||
| justify-content: center; | ||
| `; | ||
|
|
||
| export const Avatar = styled.div<{ color?: string }>` | ||
| width: 40px; | ||
| height: 40px; | ||
| border-radius: 50%; | ||
| background-color: ${({ color }) => color || '#e2e8f0'}; | ||
| flex-shrink: 0; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| overflow: hidden; | ||
| font-family: ${typography.text16Regular.fontFamily}; | ||
| font-size: ${typography.text16Regular.fontSize}; | ||
| font-weight: ${typography.text16Regular.fontWeight}; | ||
| line-height: ${typography.text16Regular.lineHeight}; | ||
| color: #ffffff; | ||
| `; | ||
|
|
||
| export const UserMessageCard = styled.div` | ||
| border: 1px solid #e2e8f0; | ||
| border-radius: 14px; | ||
| padding: 15px 21px; | ||
| width: 100%; | ||
| max-width: 800px; | ||
| box-sizing: border-box; | ||
| background-color: #ffffff; | ||
| color: ${colors.primary.default}; | ||
| font-family: ${typography.text18Regular.fontFamily}; | ||
| font-size: ${typography.text18Regular.fontSize}; | ||
| font-weight: ${typography.text18Regular.fontWeight}; | ||
| line-height: ${typography.text18Regular.lineHeight}; | ||
| `; | ||
|
|
||
| export const AIMessageCard = styled.div` | ||
| background: #ffffff; | ||
| border-radius: 14px; | ||
| padding: 22px; | ||
| width: 100%; | ||
| max-width: 800px; | ||
| box-sizing: border-box; | ||
| box-shadow: 14px 27px 45px 4px rgba(112, 144, 176, 0.2); | ||
| color: #1b2559; | ||
| font-family: ${typography.text18Regular.fontFamily}; | ||
| font-size: ${typography.text18Regular.fontSize}; | ||
| font-weight: ${typography.text18Regular.fontWeight}; | ||
| line-height: ${typography.text18Regular.lineHeight}; | ||
| white-space: pre-wrap; | ||
| min-height: 100px; | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import Image from 'next/image'; | ||
| import { colors } from '@/styles/colors'; | ||
| import * as S from './style'; | ||
|
|
||
| interface ChatSectionProps { | ||
| userMessage: string; | ||
| streamedText: string; | ||
| } | ||
|
|
||
| export default function ChatSection({ userMessage, streamedText }: ChatSectionProps) { | ||
| return ( | ||
| <S.ChatArea> | ||
| <S.MessageRow> | ||
| <S.Avatar color={colors.primary.default}> | ||
| N | ||
| </S.Avatar> | ||
| <S.UserMessageCard> | ||
| {userMessage} | ||
| </S.UserMessageCard> | ||
| </S.MessageRow> | ||
|
|
||
| <S.MessageRow> | ||
| <S.Avatar> | ||
| <Image | ||
| src="/assets/logo.svg" | ||
| alt="AI Avatar" | ||
| width={24} | ||
| height={24} | ||
| /> | ||
| </S.Avatar> | ||
| <S.AIMessageCard> | ||
| {streamedText} | ||
| </S.AIMessageCard> | ||
| </S.MessageRow> | ||
| </S.ChatArea> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import styled from '@emotion/styled'; | ||
| import { colors } from '@/styles/colors'; | ||
| import { typography } from '@/styles/typography'; | ||
|
|
||
| export const SearchContainer = styled.div` | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| width: 100%; | ||
| max-width: 800px; | ||
| height: 52px; | ||
| padding: 0 20px; | ||
| background: #ffffff; | ||
| border: 1px solid ${colors.black[50]}; | ||
| border-radius: 26px; | ||
| box-sizing: border-box; | ||
| transition: border-color 0.2s ease; | ||
| margin-bottom: 20px; | ||
|
|
||
| &:focus-within { | ||
| border-color: ${colors.primary.default}; | ||
| } | ||
| `; | ||
|
|
||
| export const InputWrapper = styled.div` | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 12px; | ||
| flex: 1; | ||
| `; | ||
|
|
||
| export const IconCircle = styled.div` | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 28px; | ||
| height: 28px; | ||
| border-radius: 50%; | ||
| overflow: hidden; | ||
| flex-shrink: 0; | ||
|
|
||
| &.send-button { | ||
| background: ${colors.primary.default}; | ||
| cursor: pointer; | ||
| border: 1px solid ${colors.primary.default}; | ||
| } | ||
| `; | ||
|
|
||
| export const SearchInput = styled.input` | ||
| border: none; | ||
| background: transparent; | ||
| width: 100%; | ||
| padding: 0; | ||
| font-family: ${typography.text16Medium.fontFamily}; | ||
| font-size: ${typography.text16Medium.fontSize}; | ||
| font-weight: ${typography.text16Medium.fontWeight}; | ||
| line-height: ${typography.text16Medium.lineHeight}; | ||
| color: ${colors.black[75]}; | ||
| outline: none; | ||
|
|
||
| &::placeholder { | ||
| color: ${colors.black[75]}; | ||
| } | ||
| `; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||||||||||||||
| import * as S from './style'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| interface SearchSectionProps { | ||||||||||||||||||||||
| inputValue: string; | ||||||||||||||||||||||
| setInputValue: (value: string) => void; | ||||||||||||||||||||||
| handleSearch: (e?: React.FormEvent) => void; | ||||||||||||||||||||||
| handleKeyDown: (e: React.KeyboardEvent) => void; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export default function SearchSection({ inputValue, setInputValue, handleSearch, handleKeyDown }: SearchSectionProps) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <S.SearchContainer> | ||||||||||||||||||||||
| <S.InputWrapper> | ||||||||||||||||||||||
| <S.IconCircle> | ||||||||||||||||||||||
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||||||||||||||||||
| <path d="M12 4L14.4 9.6L20 12L14.4 14.4L12 20L9.6 14.4L4 12L9.6 9.6L12 4Z" fill="#969696" /> | ||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||
| </S.IconCircle> | ||||||||||||||||||||||
| <S.SearchInput | ||||||||||||||||||||||
| placeholder="무μμ μνμλμ?" | ||||||||||||||||||||||
| value={inputValue} | ||||||||||||||||||||||
| onChange={(e) => setInputValue(e.target.value)} | ||||||||||||||||||||||
| onKeyDown={handleKeyDown} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| </S.InputWrapper> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| <S.IconCircle className="send-button" onClick={handleSearch}> | ||||||||||||||||||||||
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||||||||||||||||||
| <path d="M12 19V5M12 5L5 12M12 5L19 12" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> | ||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||
| </S.IconCircle> | ||||||||||||||||||||||
|
Comment on lines
+27
to
+31
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. Send button The send button is a Proposed fix β use a styled button insteadIn - <S.IconCircle className="send-button" onClick={handleSearch}>
+ <S.IconCircle as="button" className="send-button" type="button" onClick={handleSearch} aria-label="Send">π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||
| </S.SearchContainer> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
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.
π§© Analysis chain
π Script executed:
# First, let's check the current state of style.ts cat -n src/app/agent/style.tsRepository: M-ADP/M-ADP-CLIENT
Length of output: 1065
π Script executed:
Repository: M-ADP/M-ADP-CLIENT
Length of output: 89
π Script executed:
Repository: M-ADP/M-ADP-CLIENT
Length of output: 89
π Script executed:
Repository: M-ADP/M-ADP-CLIENT
Length of output: 713
π Script executed:
Repository: M-ADP/M-ADP-CLIENT
Length of output: 86
π Script executed:
Repository: M-ADP/M-ADP-CLIENT
Length of output: 734
Use transient prop syntax for
isChatto prevent React warnings about unknown DOM attributes.Emotion's
styledpasses non-transient props to underlying DOM elements. SinceisChatis not a valid HTML attribute, prefix it with$to mark it as transient (consumed only by the styled component, not forwarded to the DOM).Required changes
Also update
src/app/agent/page.tsxto pass$isChatinstead ofisChat:π Committable suggestion
π€ Prompt for AI Agents