Skip to content
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

FS-93 Changes for the File Upload UI #36

Merged
merged 7 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions frontend/src/components/fileUpload.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
.uploadButton_container {
position: relative;
display: flex;
align-items: flex-start;
justify-content: center;
background-color: transparent;
border-radius: 50%;
height: 36px;
width: 36px;
margin-right: 16px;
margin-left: 10px;
margin-top: 5px;
}

.uploadButton {
background: transparent;
border: none;
height: 36px;
width: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}

.uploadButton img {
width: 24px;
height: 24px;
}

.uploadButton:not(:disabled):hover {
cursor: pointer;
}

.uploadButton:active {
background-color: var(--grey-400);
border-radius: 50%;
}

.uploadButton_container:has(.uploadButton:disabled) {
color: var(--grey-900);
opacity: 0.5;
}

.uploadButton_container:has(.uploadButton:not(:disabled)):hover {
background-color: var(--grey-300);
}

.sendButtonContainer {
display: flex;
align-items: flex-start;
justify-content: center;
height: 100%;
}

.tooltip {
position: absolute;
bottom: 130%;
left: 50%;
transform: translateX(-50%);
width: 220px;
background-color: #333333;
color: #f2f2f2;
padding: 8px;
border-radius: 8px;
font-family: 'Roboto', sans-serif;
font-size: 14px;
line-height: 16px;
text-align: left;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
z-index: 10;
}

.tooltip p {
margin: 0 0 8px 0;
}

.tooltip p:last-child {
margin-bottom: 0;
}

.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 8px;
border-style: solid;
border-color: #333333 transparent transparent transparent;
}
62 changes: 62 additions & 0 deletions frontend/src/components/fileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { ChangeEvent, useState } from 'react';
import styles from './fileUpload.module.css';
import UploadIcon from '../icons/upload.svg';
import UploadInProgressIcon from '../icons/upload-in-progress.svg';
import CheckCircleIcon from '../icons/check-circle.svg';

interface FileUploaderProps {
onFileUpload: (file: File) => Promise<void>;
uploadInProgress: boolean;
disabled: boolean;
}

export const FileUpload = ({
onFileUpload,
uploadInProgress,
disabled,
}: FileUploaderProps) => {
const [showTooltip, setShowTooltip] = useState<boolean>(false);

const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
onFileUpload(file);
}
};

const tooltipContent = disabled ? (
<>
<p>You already uploaded one file.</p>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches design, but should probably be You've instead of You.

<p>You can upload a different file by starting a new chat.</p>
<p>Starting a new chat will reset your existing conversation history.</p>
</>
) : (
<p>You can only upload one .csv, .pdf or .txt file to this chat.</p>
);

return (
<div
className={styles.uploadButton_container}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<label className={styles.uploadButton}>
{uploadInProgress ? (
<img src={UploadInProgressIcon} alt="Uploading..." />
) : disabled ? (
<img src={CheckCircleIcon} alt="Upload Complete" />
) : (
<img src={UploadIcon} alt="Upload" />
)}
<input
type="file"
accept=".csv, .pdf, .txt"
onChange={handleFileChange}
style={{ display: 'none' }}
disabled={disabled}
/>
</label>
{showTooltip && <div className={styles.tooltip}>{tooltipContent}</div>}
</div>
);
};
51 changes: 5 additions & 46 deletions frontend/src/components/input.module.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
.inputContainer {
box-sizing: border-box;
display: flex;
margin: 1rem 0 0 0;
position: relative;
width: 100%;
}
Expand Down Expand Up @@ -76,56 +75,16 @@ textarea::placeholder {
font-family: 'Roboto', sans-serif;
}

.uploadButton_container {
.sendButtonContainer {
display: flex;
align-items: flex-start;
justify-content: center;
background-color: transparent;
border-radius: 50%;
height: 36px;
width: 36px;
margin-right: 16px;
margin-left: 10px;
margin-top: 5px;
}

.uploadButton {
background: transparent;
color: var(--grey-900);
border: none;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}

.uploadButton img {
width: 24px;
height: 24px;
}

.uploadButton:not(:disabled):hover {
cursor: pointer;
}

.uploadButton:active {
background-color: var(--grey-400);
}

.uploadButton_container:has(.uploadButton:disabled) {
color: var(--grey-900);
opacity: 0.5;
}

.uploadButton_container:has(.uploadButton:not(:disabled)):hover {
background-color: var(--grey-300);
}

.sendButtonContainer {
.inputRow {
display: flex;
align-items: flex-start;
justify-content: center;
height: 100%;
justify-content: space-between;
align-items: center;
width: 100%;
}
55 changes: 38 additions & 17 deletions frontend/src/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import React, {
} from 'react';
import styles from './input.module.css';
import RightArrowIcon from '../icons/send.svg';
import UploadIcon from '../icons/upload.svg';
import { FileUpload } from './fileUpload';
import { UploadedFileDisplay } from './uploadedFileDisplay';
import { Suggestions } from './suggestions';
import { Button } from './button';
import { uploadFileToServer } from '../server';

export interface InputProps {
sendMessage: (message: string) => void;
Expand All @@ -20,6 +22,8 @@ export interface InputProps {

export const Input = ({ sendMessage, waiting, suggestions }: InputProps) => {
const [userInput, setUserInput] = useState<string>('');
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [uploadInProgress, setUploadInProgress] = useState<boolean>(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);

const onChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
Expand Down Expand Up @@ -52,26 +56,43 @@ export const Input = ({ sendMessage, waiting, suggestions }: InputProps) => {
[sendMessage, userInput, waiting],
);

const uploadFile = async (file: File) => {
setUploadInProgress(true);

try {
const { filename, id } = await uploadFileToServer(file);
console.log(`File uploaded successfully: ${filename} with id ${id}`);
setUploadedFile(file);
} catch (error) {
console.error(error);
} finally {
setUploadInProgress(false);
}
};

return (
<>
{uploadedFile && <UploadedFileDisplay fileName={uploadedFile.name} />}
<form onSubmit={onSend} className={styles.inputContainer}>
<div className={styles.parentDiv}>
<textarea
className={styles.textarea}
ref={textareaRef}
placeholder="Send a Message..."
value={userInput}
onChange={onChange}
rows={1}
/>
<div className={styles.uploadButton_container}>
<button className={styles.uploadButton} disabled>
<img src={UploadIcon} />
</button>
<div className={styles.inputRow}>
<div className={styles.parentDiv}>
<textarea
className={styles.textarea}
ref={textareaRef}
placeholder="Send a Message..."
value={userInput}
onChange={onChange}
rows={1}
/>
<FileUpload
onFileUpload={uploadFile}
uploadInProgress={uploadInProgress}
disabled={!!uploadedFile}
/>
</div>
<div className={styles.sendButtonContainer}>
<Button icon={RightArrowIcon} disabled={waiting} />
</div>
</div>
<div className={styles.sendButtonContainer}>
<Button icon={RightArrowIcon} disabled={waiting} />
</div>
</form>
<Suggestions loadPrompt={setUserInput} suggestions={suggestions} />
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/components/uploadedFileDisplay.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.uploadedFileContainer {
display: flex;
justify-content: flex-end;
align-items: center;
height: 36px;
margin-bottom: 4px;
box-sizing: border-box;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed some extra spacing between the inputContainer and the uploadedFileContainer which I think can be easily fixed by removing the margin from the inputContainer

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done :)

.uploadedFile {
font-size: 14px;
color: var(--grey-900);
background-color: var(--grey-200);
padding: 8px 12px;
border-radius: 8px;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 65px;
}

.attachmentIcon {
width: 16px;
height: 16px;
margin-right: 8px;
margin-bottom: -3px;
}
20 changes: 20 additions & 0 deletions frontend/src/components/uploadedFileDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import styles from './uploadedFileDisplay.module.css';
import AttachmentIcon from '../icons/attachment.svg';

interface UploadedFileDisplayProps {
fileName: string;
}

export const UploadedFileDisplay = ({ fileName }: UploadedFileDisplayProps) => (
<div className={styles.uploadedFileContainer}>
<span className={styles.uploadedFile}>
<img
src={AttachmentIcon}
alt="Attachment"
className={styles.attachmentIcon}
/>
{fileName}
</span>
</div>
);
23 changes: 23 additions & 0 deletions frontend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,26 @@ export const resetChat = async (): Promise<Response> => {
method: 'DELETE',
});
};

export const uploadFileToServer = async (
file: File,
): Promise<{ filename: string; id: string }> => {
const formData = new FormData();
formData.append('file', file);

return await fetch(`${process.env.BACKEND_URL}/report`, {
method: 'POST',
body: formData,
credentials: 'include',
})
.then((response) => {
if (!response.ok) {
throw new Error(`Upload failed with status : ${response.status}`);
}
return response.json();
})
.catch((error) => {
console.error('Error uploading file:', error);
throw error;
});
};