Skip to content

Commit f330ce1

Browse files
authored
Merge pull request #17 from CleanEngine/develop
Develop
2 parents 35c0c59 + ab7c2c1 commit f330ce1

File tree

18 files changed

+530
-16
lines changed

18 files changed

+530
-16
lines changed

.github/workflows/deploy.yml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Deploy remix app
22

3-
run-name: Deploy remix app to docker hub by ${{github.actor}}
3+
run-name: Deploy remix app to ECR by ${{github.actor}}
44

55
on:
66
push:
@@ -9,6 +9,9 @@ on:
99
jobs:
1010
deploy:
1111
runs-on: ubuntu-latest
12+
permissions:
13+
id-token: write
14+
contents: read
1215
env:
1316
HUSKY: 0
1417
steps:
@@ -21,12 +24,15 @@ jobs:
2124
echo "VITE_STOMP_URL=${{ secrets.VITE_STOMP_URL }}" >> .env
2225
- name: Build image
2326
run: |
24-
docker build -t ${{secrets.DOCKER_USERNAME}}/if-fe:latest .
25-
- name: Login to Docker Hub
26-
uses: docker/[email protected]
27+
docker build -t ${{secrets.AWS_ORGANIZATION}}/frontend .
28+
docker tag ${{secrets.AWS_ORGANIZATION}}/frontend:latest ${{ secrets.AWS_ECR_FRONTEND_REPO }}:latest
29+
- name: Configure AWS credentials
30+
uses: aws-actions/configure-aws-credentials@v4
2731
with:
28-
username: ${{secrets.DOCKER_USERNAME}}
29-
password: ${{secrets.DOCKER_TOKEN}}
30-
- name: Push image to Docker hub
32+
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
33+
aws-region: ${{ secrets.AWS_REGION }}
34+
- name: Login to Amazon ECR
35+
uses: aws-actions/amazon-ecr-login@v2
36+
- name: Push image to ECR
3137
run: |
32-
docker push ${{secrets.DOCKER_USERNAME}}/if-fe:latest
38+
docker push ${{ secrets.AWS_ECR_FRONTEND_REPO }}:latest

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"cookie": "^1.0.2",
2626
"isbot": "^5",
2727
"ky": "^1.8.1",
28+
"motion": "^12.12.1",
2829
"react": "^19.1.0",
2930
"react-dom": "^19.1.0",
3031
"react-router": "^7.5.3",
@@ -52,6 +53,8 @@
5253
},
5354
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
5455
"msw": {
55-
"workerDirectory": ["public"]
56+
"workerDirectory": [
57+
"public"
58+
]
5659
}
5760
}

src/app/routes/trade.$ticker.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import * as cookie from 'cookie';
22
import { Outlet, redirect } from 'react-router';
3-
import type { Route } from './+types/trade.$ticker';
43

5-
import { api as coinApi } from '~/entities/coin';
6-
import CoinPriceWithName from '~/entities/coin/ui/CoinPriceWithName';
4+
import { CoinPriceWithName, api as coinApi } from '~/entities/coin';
75
import { api } from '~/entities/session';
6+
import { AIChatBot } from '~/features/chat';
87
import { CoinListWithSearchBar } from '~/features/coin-search-list';
98
import { OrderForm, OrderFormFallback } from '~/features/order';
109
import { ExecutionList } from '~/features/order-execution-list';
1110
import { Orderbook, StockChart } from '~/features/tradeview';
1211
import Container from '~/shared/ui/Container';
1312
import ContainerTitle from '~/shared/ui/ContainerTitle';
1413
import { NavBar } from '~/widgets/navbar';
14+
import type { Route } from './+types/trade.$ticker';
1515

1616
export async function loader({ request, params }: Route.LoaderArgs) {
1717
const rawCookie = request.headers.get('Cookie');
@@ -49,7 +49,7 @@ export default function TradeRouteComponent({
4949
}));
5050

5151
return (
52-
<div className="h-full bg-gray-100">
52+
<div className="relative h-full bg-gray-100">
5353
<NavBar
5454
to="/"
5555
serviceName="IF"
@@ -99,6 +99,7 @@ export default function TradeRouteComponent({
9999
</div>
100100
</div>
101101
<Outlet />
102+
<AIChatBot />
102103
</div>
103104
);
104105
}

src/assets/svgs/headset-solid.svg

Lines changed: 1 addition & 0 deletions
Loading

src/assets/svgs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { ReactComponent as IconCoinBitcoin } from './coin-bitcoin.svg';
33
export { ReactComponent as IconMagnifying } from './magnifying.svg';
44
export { ReactComponent as IconPlus } from './plus-solid.svg';
55
export { ReactComponent as IconMinus } from './minus-solid.svg';
6+
export { ReactComponent as IconHeadset } from './headset-solid.svg';

src/entities/coin/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { default as CoinWithIconAndName } from './ui/CoinWithIconAndName';
22
export type { CoinWithIconAndNameProps } from './ui/CoinWithIconAndName';
33
export { default as useCurrentPrice } from './hooks/useCurrentPrice';
4+
export { default as CoinPriceWithName } from './ui/CoinPriceWithName';
45
export { default as api } from './api/coin.endpoint';
56
export type {
67
CoinInfo,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import ky, { type KyResponse } from 'ky';
2+
import type { Message } from '../types/chat.type';
3+
4+
export default {
5+
ask: async (question: Message): Promise<KyResponse<string>> => {
6+
return ky.get(
7+
`${import.meta.env.VITE_AI_URL}/async/chat?question=${question}`,
8+
{
9+
credentials: 'omit',
10+
},
11+
);
12+
},
13+
};

src/features/chat/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as AIChatBot } from './ui/AIChatBot';
2+
export { default as api } from './api/chat.endpoint';
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { assertEvent, assign, fromPromise, setup } from 'xstate';
2+
import api from '../api/chat.endpoint';
3+
import type { Message, MessageObj } from '../types/chat.type';
4+
5+
export const chatMachine = setup({
6+
types: {
7+
context: {} as {
8+
question: Message;
9+
answer: Message;
10+
messageList: MessageObj[];
11+
state: 'idle' | 'processing' | 'complete';
12+
},
13+
events: {} as
14+
| {
15+
type: 'SUBMIT_EVENT';
16+
}
17+
| {
18+
type: 'RECEIVE_ANSWER';
19+
answer: string;
20+
}
21+
| {
22+
type: 'TYPING_QUESTION';
23+
question: string;
24+
},
25+
},
26+
actors: {
27+
submitQuestion: fromPromise(
28+
async ({ input }: { input: { question: string } }) => {
29+
const response = await api.ask(input.question);
30+
return await response.json();
31+
},
32+
),
33+
},
34+
actions: {
35+
switchStateToIdle: assign({
36+
state: 'idle',
37+
}),
38+
switchStateToProcessing: assign({
39+
state: 'processing',
40+
}),
41+
switchStateToComplete: assign({
42+
state: 'complete',
43+
}),
44+
assignQuestion: assign({
45+
question: ({ event }) => {
46+
assertEvent(event, 'TYPING_QUESTION');
47+
return event.question;
48+
},
49+
}),
50+
assignQuestionToMessageList: assign({
51+
messageList: ({ event, context }) => {
52+
assertEvent(event, 'SUBMIT_EVENT');
53+
return [
54+
...context.messageList,
55+
{
56+
message: context.question,
57+
isMine: true,
58+
},
59+
];
60+
},
61+
}),
62+
resetQuestion: assign({
63+
question: '',
64+
}),
65+
assignErrorMessage: assign({
66+
answer: ({ event }) => {
67+
return '답변을 받아오는데 실패했습니다.';
68+
},
69+
}),
70+
},
71+
}).createMachine({
72+
id: 'chatMachine',
73+
context: {
74+
question: '',
75+
answer: '',
76+
messageList: [],
77+
state: 'idle',
78+
},
79+
initial: 'EMPTY QUESTION FIELD',
80+
states: {
81+
'EMPTY QUESTION FIELD': {
82+
on: {
83+
TYPING_QUESTION: {
84+
target: 'FILLING QUESTION FIELD',
85+
actions: 'assignQuestion',
86+
},
87+
},
88+
},
89+
'FILLING QUESTION FIELD': {
90+
on: {
91+
TYPING_QUESTION: {
92+
actions: 'assignQuestion',
93+
},
94+
SUBMIT_EVENT: {
95+
target: 'SENDING QUESTION',
96+
actions: ['assignQuestionToMessageList', 'switchStateToProcessing'],
97+
},
98+
},
99+
},
100+
'SENDING QUESTION': {
101+
invoke: {
102+
src: 'submitQuestion',
103+
input: ({ context }) => ({ question: context.question }),
104+
onDone: {
105+
target: 'RECEIVED ANSWER',
106+
actions: [
107+
'switchStateToComplete',
108+
'resetQuestion',
109+
assign({
110+
messageList: ({ event, context }) => {
111+
return [
112+
...context.messageList,
113+
{ message: event.output, isMine: false },
114+
];
115+
},
116+
}),
117+
],
118+
},
119+
onError: {
120+
target: 'RECEIVED ANSWER',
121+
actions: ['assignErrorMessage'],
122+
},
123+
},
124+
},
125+
'RECEIVED ANSWER': {
126+
always: {
127+
target: 'EMPTY QUESTION FIELD',
128+
actions: ['switchStateToIdle'],
129+
},
130+
},
131+
},
132+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type Message = string;
2+
export type isMine = boolean;
3+
export type MessageObj = {
4+
message: Message;
5+
isMine: isMine;
6+
};

0 commit comments

Comments
 (0)