Skip to content

Commit

Permalink
feat(chat): add files cards list
Browse files Browse the repository at this point in the history
  • Loading branch information
Mati365 committed Dec 25, 2024
1 parent 503111d commit 0ee8f02
Show file tree
Hide file tree
Showing 20 changed files with 249 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import isValidUTF8 from 'utf-8-validate';

import type { SdkJwtTokenT } from '@llm/sdk';

import { isNil } from '@llm/commons';
import {
isImageMimetype,
isLegacyExcelMimetype,
isLegacyWordMimetype,
isNil,
isPDFMimeType,
isXmlOfficeMimetype,
} from '@llm/commons';

import type { WithAuthFirewall } from '../auth';
import type { TableId, TableRowWithUuid } from '../database';
Expand All @@ -28,13 +35,6 @@ import {
XlsAIEmbeddingGenerator,
} from './generators';
import { createRelevantEmbeddingsPrompt, formatVector } from './helpers';
import {
isImageMimetype,
isLegacyExcelMimetype,
isLegacyWordMimetype,
isPDFMimeType,
isXmlOfficeMimetype,
} from './mimetypes';
import { ProjectsEmbeddingsFirewall } from './projects-embeddings.firewall';
import { ProjectsEmbeddingsRepo } from './projects-embeddings.repo';
import { ProjectEmbeddingsInsertTableRow } from './projects-embeddings.tables';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,11 @@ export const ChatConversationPanel = memo(({ ref, chat, initialMessages, classNa
ref={messagesContainerRef}
className={clsx(
'relative z-10 flex-1 [&::-webkit-scrollbar]:hidden p-4 [-ms-overflow-style:none] overflow-y-scroll [scrollbar-width:none]',
flickeringIndicator.visible ? 'opacity-100' : 'opacity-0', // Avoid scroll flickering on first render

// Avoid scroll flickering on first render
flickeringIndicator.visible
? 'opacity-100'
: 'opacity-0',
)}
onLoad={scrollConversation}
>
Expand Down
122 changes: 122 additions & 0 deletions apps/chat/src/modules/chats/conversation/files/file-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import clsx from 'clsx';
import {
FileAxis3DIcon,
FileIcon,
FileSpreadsheetIcon,
FileTextIcon,
ImageIcon,
XIcon,
} from 'lucide-react';

import {
isImageFileUrl,
isLegacyExcelMimetype,
isLegacyWordMimetype,
isPDFFileUrl,
isPDFMimeType,
isSpreadsheetFileUrl,
isWordFileUrl,
} from '@llm/commons';
import { useI18n } from '~/i18n';

type Props = {
file: File;
onRemove: () => void;
};

export function FileCard({ file, onRemove }: Props) {
const { pack } = useI18n();
const { type, bgColor, icon: IconComponent } = getFileTypeAndColor(file);

const isImage = isImageFileUrl(file.name);
const imageUrl = isImage ? URL.createObjectURL(file) : null;

return (
<div
className={clsx(
'relative gap-2 p-3 border rounded-lg h-[57px] group',
isImage
? 'p-0 w-[128px]'
: 'bg-gray-50 border-gray-200 w-[200px]',
)}
>
<button
title={pack.buttons.delete}
type="button"
onClick={onRemove}
className="-top-2 -right-2 absolute bg-black p-1 rounded-full"
>
<XIcon className="w-3 h-3 text-white" />
</button>

{isImage && imageUrl
? (
<img
src={imageUrl}
alt={file.name}
className="rounded w-full h-full aspect-square object-contain"
/>
)
: (
<div className="gap-2 grid grid-cols-[auto,1fr] min-w-0 overflow-hidden">
<div className={clsx('flex items-center p-1 rounded', bgColor)}>
<IconComponent className="w-5 h-5 text-white" />
</div>

<div className="flex flex-col min-w-0">
<span
className="font-semibold text-gray-700 text-xs truncate"
title={file.name}
>
{file.name}
</span>

<span className="text-[10px] text-gray-500">
{type}
</span>
</div>
</div>
)}
</div>
);
}

function getFileTypeAndColor(file: File) {
if (isSpreadsheetFileUrl(file.name) || isLegacyExcelMimetype(file.type)) {
return {
type: 'Excel',
bgColor: 'bg-[#217346]',
icon: FileSpreadsheetIcon,
};
}

if (isLegacyWordMimetype(file.type) || isWordFileUrl(file.name)) {
return {
type: 'Word',
bgColor: 'bg-[#2B579A]',
icon: FileTextIcon,
};
}

if (isPDFMimeType(file.type) || isPDFFileUrl(file.name)) {
return {
type: 'PDF',
bgColor: 'bg-[#D93F3F]',
icon: FileAxis3DIcon,
};
}

if (isImageFileUrl(file.name)) {
return {
type: 'Image',
bgColor: 'bg-purple-500',
icon: ImageIcon,
};
}

return {
type: 'TXT',
bgColor: 'bg-gray-400',
icon: FileIcon,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { controlled } from '@under-control/forms';
import clsx from 'clsx';
import { useMemo } from 'react';
import { v4 } from 'uuid';

import { without } from '@llm/commons';

import { FileCard } from './file-card';

type Props = {
className?: string;
};

export const FilesCardsList = controlled<File[], Props>(({ className, control: { value, setValue } }) => {
const mappedFiles = useMemo(() => {
const fileNames = new Set<string>();
const duplicateNames = new Set<string>();

value.forEach((file) => {
if (fileNames.has(file.name)) {
duplicateNames.add(file.name);
}

fileNames.add(file.name);
});

return value.map(file => ({
id: duplicateNames.has(file.name) ? v4() : file.name,
file,
}));
}, [value]);

if (!value?.length) {
return null;
}

const handleRemove = (file: File) => {
setValue({
value: without([file])(value),
});
};

return (
<div
className={clsx(
'flex flex-row gap-5',
className,
)}
>
{mappedFiles.map(({ file, id }) => (
<FileCard
key={id}
file={file}
onRemove={() => handleRemove(file)}
/>
))}
</div>
);
});
3 changes: 3 additions & 0 deletions apps/chat/src/modules/chats/conversation/files/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './file-card';
export * from './files-cards-list';
export * from './select-chat-file';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { selectFile } from '@llm/commons';

export const selectChatFile = selectFile('.pdf,.csv,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.bmp');
9 changes: 9 additions & 0 deletions apps/chat/src/modules/chats/conversation/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
export * from './chat-attached-app';
export * from './chat-background';
export * from './chat-conversation-panel';
export * from './chat-conversation-with-sidebar';
export * from './config-panel';
export * from './content-parse';
export * from './files';
export * from './hooks';
export * from './input-toolbar';
export * from './messages';
40 changes: 28 additions & 12 deletions apps/chat/src/modules/chats/start-chat/start-chat-form.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import type { KeyboardEventHandler } from 'react';

import clsx from 'clsx';
import { pipe } from 'fp-ts/function';
import { PaperclipIcon, SendIcon } from 'lucide-react';

import type { SdkTableRowWithIdNameT } from '@llm/sdk';

import { StrictBooleanV } from '@llm/commons';
import { StrictBooleanV, tapTaskOption } from '@llm/commons';
import { useFocusAfterMount, useLocalStorageObject } from '@llm/commons-front';
import { Checkbox, FormSpinnerCTA } from '@llm/ui';
import { useI18n } from '~/i18n';
import { AIModelsSearchSelect } from '~/modules/ai-models';
import { ProjectsSearchSelect } from '~/modules/projects';

import { FilesCardsList, selectChatFile } from '../conversation';
import { useStartChatForm } from './use-start-chat-form';

type Props = {
Expand Down Expand Up @@ -42,6 +44,13 @@ export function StartChatForm({ forceProject, className }: Props) {
}
};

const onAttachFile = pipe(
selectChatFile,
tapTaskOption((file) => {
bind.path('files').onChange([...value.files ?? [], file]);
}),
);

return (
<form
className={clsx(
Expand All @@ -57,15 +66,22 @@ export function StartChatForm({ forceProject, className }: Props) {
'transition-border duration-100',
)}
>
<textarea
ref={focusInputRef}
name="message"
className="mb-[50px] p-4 pb-0 w-full h-[120px] focus:outline-none resize-none"
placeholder={t.placeholder}
required
{...bind.path('content')}
onKeyDown={handleKeyDown}
/>
<div className="mb-[65px]">
<textarea
ref={focusInputRef}
name="message"
className="p-4 pb-0 w-full h-[80px] focus:outline-none resize-none"
placeholder={t.placeholder}
required
{...bind.path('content')}
onKeyDown={handleKeyDown}
/>

<FilesCardsList
{...bind.path('files')}
className="mt-4 px-4"
/>
</div>

<div className="bottom-4 left-3 absolute flex flex-row gap-4">
<AIModelsSearchSelect
Expand Down Expand Up @@ -94,8 +110,8 @@ export function StartChatForm({ forceProject, className }: Props) {
<div className="flex flex-wrap items-center gap-4">
<button
type="button"
className="border-gray-300 border pointer-events-none uk-button uk-button-default"
disabled
className="border-gray-300 border uk-button uk-button-default"
onClick={onAttachFile}
>
<PaperclipIcon size={16} className="mr-2" />
{t.addFile}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function useStartChatForm({ project }: FormProps) {
content: '',
aiModel: null as any,
public: false,
files: [],
},
onSubmit,
validation: {
Expand Down
4 changes: 2 additions & 2 deletions apps/chat/src/modules/projects/files/use-file-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import * as TE from 'fp-ts/TaskEither';

import type { SdkTableRowIdT } from '@llm/sdk';

import { selectFile } from '@llm/commons';
import { useAsyncCallback } from '@llm/commons-front';
import { useSdkForLoggedIn } from '@llm/sdk';
import { selectChatFile } from '~/modules/chats/conversation/files/select-chat-file';

export function useFileUpload(projectId: SdkTableRowIdT) {
const { sdks } = useSdkForLoggedIn();

return useAsyncCallback(
pipe(
selectFile('*/*'),
selectChatFile,
TE.fromTaskOption(() => new Error('No file selected')),
TE.chainW(file => sdks.dashboard.projectsFiles.upload({
projectId,
Expand Down
1 change: 1 addition & 0 deletions packages/commons/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './is-object-with-fake-id';
export * from './is-object-with-id';
export * from './is-ssr';
export * from './iterators';
export * from './mimetypes';
export * from './observable';
export * from './panic-error';
export * from './pluck';
Expand Down
2 changes: 2 additions & 0 deletions packages/commons/src/helpers/urls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ export * from './is-absolute-url';
export * from './is-document-file-url';
export * from './is-image-file-url';
export * from './is-markdown-file-url';
export * from './is-pdf-file-url';
export * from './is-presentation-file-url';
export * from './is-spreadsheet-file-url';
export * from './is-word-file-url';
export * from './parameterize-path';
export * from './parameterize-strict-path';
export * from './with-hash';
Expand Down
3 changes: 3 additions & 0 deletions packages/commons/src/helpers/urls/is-pdf-file-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isPDFFileUrl(url: string): boolean {
return url.endsWith('.pdf');
}
3 changes: 3 additions & 0 deletions packages/commons/src/helpers/urls/is-word-file-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isWordFileUrl(url: string): boolean {
return /\.(?:doc|docx)$/i.test(url);
}

0 comments on commit 0ee8f02

Please sign in to comment.