Gemini
feat: add detailed logging
01d5a5d
'use client';
import { useState } from 'react';
import type { UploadProps } from 'antd';
import { Upload, Input, Button, message } from 'antd';
import {
CheckCircleFilled,
InboxOutlined,
FileOutlined,
FolderOutlined,
DesktopOutlined
} from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
import { uploadMemory } from '@/service/memory';
import { EVENT } from '@/utils/event';
interface UploadMemoriesProps {
onFileUpload: (files: any[]) => void;
}
const { TextArea } = Input;
// Use regular components instead of styled-components
const GlobalStyle: React.FC<{ children?: React.ReactNode }> = ({
children
}: {
children?: React.ReactNode;
}) => (
<div className="[&_.custom-message-success_.ant-message-notice-content]:bg-[#f6ffed] [&_.custom-message-success_.ant-message-notice-content]:border [&_.custom-message-success_.ant-message-notice-content]:border-[#b7eb8f] [&_.custom-message-success_.ant-message-notice-content]:rounded [&_.custom-message-success_.ant-message-notice-content]:p-2 [&_.custom-message-success_.ant-message-notice-content]:shadow-md">
{children}
</div>
);
const UploadTypeContainer: React.FC<{ children: React.ReactNode }> = ({
children
}: {
children: React.ReactNode;
}) => <div className="flex w-full gap-2 mb-4">{children}</div>;
interface UploadTypeBoxProps {
active?: boolean;
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
const UploadTypeBox: React.FC<UploadTypeBoxProps> = ({
active,
disabled,
children,
onClick
}: UploadTypeBoxProps) => (
<div
className={`
flex-1 p-4 rounded-lg cursor-pointer flex flex-col items-center gap-2 transition-all duration-300
${disabled ? 'opacity-50 pointer-events-none' : 'opacity-100'}
${
active
? 'bg-gradient-to-b from-[#F8FAFF] to-[#EEF3FF] border border-[#4080FF] shadow-[0_2px_8px_rgba(64,128,255,0.15)]'
: 'bg-white border border-[#E5E6EB] hover:border-[#4080FF] hover:bg-[#F8F9FC] hover:shadow-[0_2px_6px_rgba(0,0,0,0.05)]'
}
[&_.icon]:text-2xl [&_.icon]:${active ? 'text-[#4080FF]' : 'text-[#86909C]'}
[&_.text]:text-sm [&_.text]:font-medium [&_.text]:${active ? 'text-[#4080FF]' : 'text-[#4E5969]'}
`}
onClick={onClick}
>
{children}
</div>
);
interface TabContentProps {
isTextArea?: boolean;
children: React.ReactNode;
}
const TabContent: React.FC<TabContentProps> = ({ isTextArea, children }: TabContentProps) => (
<div
className={`
bg-white rounded-lg p-4 shadow-[0_4px_16px_rgba(0,0,0,0.08)]
${isTextArea ? 'min-h-[240px] items-start' : 'min-h-[200px] items-center'}
flex justify-center border border-[#F0F0F0]
`}
>
{children}
</div>
);
const UploadArea: React.FC<{ children: React.ReactNode }> = ({
children
}: {
children: React.ReactNode;
}) => (
<div
className="
border-2 border-dashed border-[#E5E6EB] rounded-lg bg-[#FAFBFC] p-6 text-center cursor-pointer transition-all duration-300 w-full
hover:border-[#4080FF] hover:bg-[#F5F8FF] hover:shadow-[0_0_0_4px_rgba(64,128,255,0.08)]
[&_.upload-icon]:text-2xl [&_.upload-icon]:text-[#4080FF] [&_.upload-icon]:mb-2
[&_.upload-text]:text-[#4E5969] [&_.upload-text]:mb-1 [&_.upload-text]:font-medium
[&_.browse-link]:text-[#4080FF] [&_.browse-link]:font-medium [&_.browse-link]:no-underline hover:[&_.browse-link]:underline
[&_.file-types]:text-[#86909C] [&_.file-types]:text-xs [&_.file-types]:mt-2
"
>
{children}
</div>
);
const TextContainer: React.FC<{ children: React.ReactNode }> = ({
children
}: {
children: React.ReactNode;
}) => <div className="flex flex-col gap-3 w-full h-full">{children}</div>;
const SaveButton: React.FC<React.ComponentProps<typeof Button>> = (props) => (
<Button
{...props}
className="self-start px-4 h-8 flex items-center justify-center rounded-lg shadow-sm hover:shadow-md transition-all duration-300 font-medium bg-gradient-to-r from-[#4080FF] to-[#3A75E6] hover:from-[#3A75E6] hover:to-[#3369D3]"
/>
);
export default function UploadMemories({ onFileUpload }: UploadMemoriesProps) {
const [text, setText] = useState('');
const [activeTab, setActiveTab] = useState('text');
const [uploadedFiles] = useState(new Set<string>());
const showSuccessMessage = () => {
message.success({
content: 'Successfully added text content',
icon: <CheckCircleFilled style={{ color: '#52c41a' }} />,
className: 'custom-message-success'
});
};
const handleFileUpload = async (file: File | UploadFile) => {
// Check file type
const extension = (file instanceof File ? file.name : file.name)
.split('.')
.pop()
?.toLowerCase();
if (!extension || !['pdf', 'txt', 'md'].includes(extension)) {
message.info('Only .pdf, .txt and .md files are supported');
return false;
}
// Get filename without path
const fullName = file instanceof File ? file.name : file.name;
const fileName = fullName.split('/').pop() || fullName;
const fileSize = file instanceof File ? file.size : file.size;
const fileKey = `${fileName}_${fileSize}`;
try {
const formData = new FormData();
if (file instanceof File) {
// Create a new File object, using only the filename
const newFile = new File([file], fileName, { type: file.type });
formData.append('file', newFile);
} else {
// Ensure we get the original File object from UploadFile
const originalFile = file.originFileObj;
if (!originalFile) {
throw new Error('Cannot get file content');
}
// Create a new File object, using only the filename
const newFile = new File([originalFile], fileName, {
type: originalFile.type
});
formData.append('file', newFile);
}
const res = await uploadMemory(formData);
if (res.data.code !== 0) {
throw new Error(res.data.message);
}
uploadedFiles.add(fileKey);
onFileUpload([res.data.data]);
showSuccessMessage();
return true;
} catch (error: any) {
message.error(`${fileName} upload failed: ${error.message}`);
return false;
}
};
const handleTextSubmit = async () => {
if (text.trim()) {
const formData = new FormData();
const blob = new Blob([text], { type: 'text/plain' });
const file = new File([blob], `note_${new Date().getTime()}.txt`, {
type: 'text/plain'
});
formData.append('file', file);
try {
const res = await uploadMemory(formData);
if (res.data.code !== 0) {
throw new Error(res.data.message);
}
dispatchEvent(new Event(EVENT.REFRESH_MEMORIES));
onFileUpload([res.data.data]);
showSuccessMessage();
setText('');
} catch (error: any) {
message.error(`Upload failed: ${error.message}`);
}
}
};
// Single file upload configuration
const fileProps: UploadProps = {
name: 'file',
multiple: false,
showUploadList: false,
customRequest: async ({ file, onSuccess, onError }) => {
try {
const fileToUpload = file instanceof File ? file : (file as any).originFileObj;
if (!fileToUpload) {
message.error('Unable to get file content');
return;
}
// Check file type
const extension = fileToUpload.name.split('.').pop()?.toLowerCase();
if (!extension || !['pdf', 'txt', 'md'].includes(extension)) {
message.info('Only .pdf, .txt and .md files are supported');
return;
}
const success = await handleFileUpload(fileToUpload);
if (success) {
onSuccess?.(file);
dispatchEvent(new Event(EVENT.REFRESH_MEMORIES));
} else {
onError?.(new Error('Upload failed'));
}
} catch (error: any) {
message.error(error.message || 'Upload failed');
onError?.(new Error('Upload failed'));
}
},
onChange: (info) => {
if (info.file.status === 'done') {
// showSuccessMessage();
} else if (info.file.status === 'error') {
message.error(`${info.file.name} upload failed`);
}
}
};
// Configuration for folder upload
const folderProps: UploadProps = {
showUploadList: false,
directory: true,
customRequest: async ({ file, onSuccess }) => {
try {
const fileToUpload = file instanceof File ? file : (file as any).originFileObj;
if (!fileToUpload) {
message.error('Unable to get file content');
return;
}
// Check file type
const extension = fileToUpload.name.split('.').pop()?.toLowerCase();
if (!extension || !['pdf', 'txt', 'md'].includes(extension)) {
// Skip unsupported file types without error
onSuccess?.(file);
return;
}
const success = await handleFileUpload(fileToUpload);
if (success) {
onSuccess?.(file);
dispatchEvent(new Event(EVENT.REFRESH_MEMORIES));
}
} catch (error: any) {
message.error(error.message || 'Upload failed');
}
},
onChange: (_info) => {
// Display total number of files
// if (info.fileList.length > 0) {
// const validFiles = info.fileList.filter((file) => {
// const extension = file.name.split('.').pop()?.toLowerCase();
// return extension === 'pdf' || extension === 'txt' || extension === 'md';
// });
// }
}
};
const uploadTypes = [
{ key: 'text', icon: <FileOutlined className="icon" />, text: 'Text', disabled: false },
{ key: 'file', icon: <InboxOutlined className="icon" />, text: 'File', disabled: false },
{ key: 'folder', icon: <FolderOutlined className="icon" />, text: 'Folder', disabled: false },
{
key: 'software',
icon: <DesktopOutlined className="icon" />,
text: 'Software Integration',
disabled: true
},
{
key: 'wearable',
icon: (
<svg
aria-hidden="true"
className="icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
>
<path d="M6 10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm12-6c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm-6-5.99h-2v2h2v-2z" />
</svg>
),
text: 'Wearable Integration',
disabled: true
}
];
const renderContent = () => {
switch (activeTab) {
case 'text':
return (
<TabContent isTextArea>
<TextContainer>
<TextArea
className="hover:border-[#4080FF] focus:border-[#4080FF] focus:shadow-[0_0_0_2px_rgba(64,128,255,0.2),inset_0_2px_4px_rgba(0,0,0,0.03)] flex-1"
onChange={(e) => setText(e.target.value)}
placeholder="Enter your text here..."
style={{
resize: 'none',
minHeight: '180px',
padding: '16px',
fontSize: '14px',
lineHeight: '1.6',
border: '1px solid #E5E6EB',
borderRadius: '12px',
backgroundColor: '#FAFAFA',
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.03)',
transition: 'all 0.3s ease'
}}
value={text}
/>
<SaveButton onClick={handleTextSubmit} size="large" type="primary">
Save Text
</SaveButton>
</TextContainer>
</TabContent>
);
case 'file':
return (
<TabContent>
<Upload {...fileProps}>
<UploadArea>
<div className="upload-icon">
<InboxOutlined style={{ filter: 'drop-shadow(0 2px 3px rgba(0,0,0,0.1))' }} />
</div>
<div className="upload-text">
Drag and drop file, or <span className="browse-link">Browse</span>
</div>
<div className="file-types">Supports PDF, TXT, MARKDOWN, Max 15MB each.</div>
</UploadArea>
</Upload>
</TabContent>
);
case 'folder':
return (
<TabContent>
<Upload {...folderProps}>
<UploadArea>
<div className="upload-icon">
<FolderOutlined style={{ filter: 'drop-shadow(0 2px 3px rgba(0,0,0,0.1))' }} />
</div>
<div className="upload-text">
Drag and drop folder, or <span className="browse-link">Browse</span>
</div>
<div className="file-types">Supports TXT, MARKDOWN, PDF. Max 15MB each.</div>
</UploadArea>
</Upload>
</TabContent>
);
case 'software':
case 'wearable':
return (
<TabContent>
<div style={{ textAlign: 'center', color: '#86909C' }}>
{activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} upload coming soon
</div>
</TabContent>
);
default:
return null;
}
};
return (
<>
<GlobalStyle />
<div className="p-1">
<div className="mb-2 text-[15px] font-medium text-gray-700">Upload Method</div>
<UploadTypeContainer>
{uploadTypes.map((type) => (
<UploadTypeBox
key={type.key}
active={activeTab === type.key}
disabled={type.disabled}
onClick={() => !type.disabled && setActiveTab(type.key)}
>
{type.icon}
<span className="text">{type.text}</span>
</UploadTypeBox>
))}
</UploadTypeContainer>
{renderContent()}
</div>
</>
);
}