Skip to content

Commit 188b6f0

Browse files
committed
[refactor] add Chat Message component with File Text parsing API
1 parent 1e101dc commit 188b6f0

File tree

6 files changed

+126
-80
lines changed

6 files changed

+126
-80
lines changed

components/Project/ChatMessage.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { ConsultMessage, UserRole } from '@idea2app/data-server';
2+
import { Avatar, LinearProgress, Paper, Typography } from '@mui/material';
3+
import { marked } from 'marked';
4+
import { observer } from 'mobx-react';
5+
import { ObservedComponent } from 'mobx-react-helper';
6+
7+
import { FileModel } from '../../models/File';
8+
import { i18n } from '../../models/Translation';
9+
import { FilePreview } from '../FilePreview';
10+
import { EvaluationDisplay } from './EvaluationDisplay';
11+
12+
export interface ChatMessageProps extends ConsultMessage {
13+
onFileParse?: (messageId: number, text: string) => any;
14+
}
15+
16+
@observer
17+
export class ChatMessage extends ObservedComponent<ChatMessageProps, typeof i18n> {
18+
fileStore = new FileModel();
19+
20+
async componentDidMount() {
21+
super.componentDidMount();
22+
23+
const { content, file } = this.props;
24+
25+
if (!file || content) return;
26+
27+
const text = await this.fileStore.getText(file);
28+
29+
this.props.onFileParse?.(this.props.id, text);
30+
}
31+
32+
render() {
33+
const { t } = this.observedContext,
34+
{ project, id, content, file, evaluation, prototypes, createdAt, createdBy } = this.props;
35+
const isBot = createdBy.roles.includes(3 as UserRole.Robot);
36+
const avatarSrc = isBot ? '/robot-avatar.png' : createdBy?.avatar || '/default-avatar.png';
37+
const name = isBot ? `${t('ai_assistant')} 🤖` : createdBy?.name || 'User';
38+
39+
return (
40+
<div
41+
className={`flex max-w-[95%] items-start gap-1 sm:max-w-[80%] ${isBot ? 'flex-row justify-self-start' : 'flex-row-reverse justify-self-end'}`}
42+
>
43+
<Avatar src={avatarSrc} alt={name} className="h-7 w-7 sm:h-8 sm:w-8" />
44+
<Paper
45+
elevation={1}
46+
className="bg-primary-light text-primary-contrast rounded-[16px_16px_4px_16px] p-1.5 sm:p-2"
47+
sx={{
48+
backgroundColor: 'primary.light',
49+
color: 'primary.contrastText',
50+
}}
51+
>
52+
<Typography
53+
variant="caption"
54+
display="block"
55+
className="mb-0.5 text-[0.7rem] opacity-80 sm:text-[0.75rem]"
56+
>
57+
{name}
58+
</Typography>
59+
60+
{file ? (
61+
<div className="mb-1">
62+
<FilePreview path={file} />
63+
64+
{this.fileStore.downloading > 0 && (
65+
<div className="mt-1.5">
66+
<Typography variant="caption" className="mb-1 block text-[0.7rem] opacity-80">
67+
{t('parsing_file_text')}
68+
</Typography>
69+
<LinearProgress color="inherit" />
70+
</div>
71+
)}
72+
</div>
73+
) : (
74+
content && (
75+
<Typography
76+
className="prose mb-1 text-[0.875rem] sm:text-base"
77+
variant="body2"
78+
dangerouslySetInnerHTML={{ __html: marked(content) }}
79+
/>
80+
)
81+
)}
82+
{evaluation && (
83+
<EvaluationDisplay
84+
{...evaluation}
85+
projectId={project!.id}
86+
messageId={id}
87+
prototypes={prototypes}
88+
/>
89+
)}
90+
{createdAt && (
91+
<Typography variant="caption" className="text-[0.65rem] opacity-60 sm:text-[0.75rem]">
92+
{new Date(createdAt).toLocaleTimeString()}
93+
</Typography>
94+
)}
95+
</Paper>
96+
</div>
97+
);
98+
}
99+
}

models/File.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ export class FileModel extends BaseModel {
1717

1818
return body!.getLink;
1919
}
20+
21+
@toggle('downloading')
22+
async getText(URI: string) {
23+
const { pathname } = new URL(URI);
24+
25+
const { body } = await this.client.get<string>(`${this.baseURI}/${pathname}`, {
26+
Accept: 'text/*',
27+
});
28+
return body!;
29+
}
2030
}
2131

2232
export default new FileModel();

pages/dashboard/project/[id].tsx

Lines changed: 14 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
1-
import { ConsultMessage, User, UserRole } from '@idea2app/data-server';
2-
import {
3-
Avatar,
4-
Button,
5-
Container,
6-
IconButton,
7-
Paper,
8-
TextField,
9-
Tooltip,
10-
Typography,
11-
} from '@mui/material';
12-
import { marked } from 'marked';
1+
import { User } from '@idea2app/data-server';
2+
import { Button, Container, IconButton, Paper, TextField, Tooltip } from '@mui/material';
133
import { observer } from 'mobx-react';
144
import { ObservedComponent, reaction } from 'mobx-react-helper';
155
import { compose, JWTProps, jwtVerifier, RouteProps, router } from 'next-ssr-middleware';
166
import { ChangeEvent, KeyboardEventHandler, type SubmitEvent } from 'react';
177
import { formToJSON, scrollTo, sleep } from 'web-utility';
188

199
import { SymbolIcon } from '../../../components/Icon';
20-
import { FilePreview } from '../../../components/FilePreview';
2110
import { PageHead } from '../../../components/PageHead';
2211
import { PasteDropBox, PasteDropEvent } from '../../../components/PasteDropBox';
23-
import { EvaluationDisplay } from '../../../components/Project/EvaluationDisplay';
12+
import { ChatMessage } from '../../../components/Project/ChatMessage';
2413
import { ScrollList } from '../../../components/ScrollList';
2514
import { SessionBox } from '../../../components/User/SessionBox';
2615
import fileStore from '../../../models/File';
@@ -103,71 +92,8 @@ export default class ProjectEvaluationPage extends ObservedComponent<
10392
for (const file of currentTarget.files || []) await this.uploadFile(file);
10493
};
10594

106-
renderChatMessage = (
107-
{ id, content, file, evaluation, prototypes, createdAt, createdBy }: ConsultMessage,
108-
index = 0,
109-
{ length }: ConsultMessage[],
110-
) => {
111-
const { t } = this.observedContext;
112-
const isBot = createdBy.roles.includes(3 as UserRole.Robot);
113-
const avatarSrc = isBot ? '/robot-avatar.png' : createdBy?.avatar || '/default-avatar.png';
114-
const name = isBot ? `${t('ai_assistant')} 🤖` : createdBy?.name || 'User';
115-
116-
return (
117-
<div
118-
key={id}
119-
id={index + 1 === length ? 'last-message' : undefined}
120-
className={`mb-2 flex w-full ${isBot ? 'justify-start' : 'justify-end'}`}
121-
>
122-
<div
123-
className={`flex max-w-[95%] items-start gap-1 sm:max-w-[80%] ${isBot ? 'flex-row' : 'flex-row-reverse'}`}
124-
>
125-
<Avatar src={avatarSrc} alt={name} className="h-7 w-7 sm:h-8 sm:w-8" />
126-
<Paper
127-
elevation={1}
128-
className="bg-primary-light text-primary-contrast rounded-[16px_16px_4px_16px] p-1.5 sm:p-2"
129-
sx={{
130-
backgroundColor: 'primary.light',
131-
color: 'primary.contrastText',
132-
}}
133-
>
134-
<Typography
135-
variant="caption"
136-
display="block"
137-
className="mb-0.5 text-[0.7rem] opacity-80 sm:text-[0.75rem]"
138-
>
139-
{name}
140-
</Typography>
141-
142-
{file ? (
143-
<FilePreview className="mb-1" path={file} />
144-
) : (
145-
content && (
146-
<Typography
147-
className="prose mb-1 text-[0.875rem] sm:text-base"
148-
variant="body2"
149-
dangerouslySetInnerHTML={{ __html: marked(content) }}
150-
/>
151-
)
152-
)}
153-
{evaluation && (
154-
<EvaluationDisplay
155-
{...evaluation}
156-
projectId={this.projectId}
157-
messageId={id}
158-
prototypes={prototypes}
159-
/>
160-
)}
161-
{createdAt && (
162-
<Typography variant="caption" className="text-[0.65rem] opacity-60 sm:text-[0.75rem]">
163-
{new Date(createdAt).toLocaleTimeString()}
164-
</Typography>
165-
)}
166-
</Paper>
167-
</div>
168-
</div>
169-
);
170-
};
95+
handleParseFile = (messageId: number, text: string) =>
96+
this.messageStore.updateOne({ content: text }, messageId);
17197

17298
render() {
17399
const { jwtPayload } = this.props,
@@ -194,7 +120,15 @@ export default class ProjectEvaluationPage extends ObservedComponent<
194120
filter={{ project: projectId }}
195121
renderList={allItems => (
196122
<div className="h-full overflow-y-auto p-1 sm:p-2">
197-
{allItems.map(this.renderChatMessage)}
123+
{allItems.map((message, index, { length }) => (
124+
<div
125+
key={message.id}
126+
id={index + 1 === length ? 'last-message' : undefined}
127+
className="mb-2 flex w-full"
128+
>
129+
<ChatMessage {...message} onFileParse={this.handleParseFile} />
130+
</div>
131+
))}
198132
</div>
199133
)}
200134
/>

translation/en-US.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export default {
151151
send_message: 'Send Message',
152152
send: 'Send',
153153
attach_files: 'Attach Files',
154+
parsing_file_text: 'Parsing file text, please wait...',
154155

155156
// Prototype Generator
156157
generate_prototype: 'Generate Prototype',

translation/zh-CN.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export default {
147147
send_message: '发送消息',
148148
send: '发送',
149149
attach_files: '上传文件',
150+
parsing_file_text: '正在解析文件文本,请稍候……',
150151

151152
// Prototype Generator
152153
generate_prototype: '生成原型',

translation/zh-TW.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export default {
146146
send_message: '發送訊息',
147147
send: '發送',
148148
attach_files: '上傳檔案',
149+
parsing_file_text: '正在解析檔案文字,請稍候……',
149150

150151
// Prototype Generator
151152
generate_prototype: '生成原型',

0 commit comments

Comments
 (0)