Skip to content

Commit

Permalink
feat(chat): add prorotype pinning section
Browse files Browse the repository at this point in the history
  • Loading branch information
Mati365 committed Feb 22, 2025
1 parent 4893d60 commit e22e5ae
Show file tree
Hide file tree
Showing 12 changed files with 267 additions and 23 deletions.
8 changes: 8 additions & 0 deletions apps/chat/src/i18n/packs/i18n-lang-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,14 @@ export const I18N_PACK_EN = {
emptyPlaceholder: 'No files yet. Feel free to upload one!',
},
},
pinnedMessages: {
grid: {
placeholder: 'No pinned messages yet. Pin a message to keep it here!',
},
buttons: {
goToChat: 'Go to chat',
},
},
experts: {
favorites: {
add: 'Add to favorites',
Expand Down
8 changes: 8 additions & 0 deletions apps/chat/src/i18n/packs/i18n-lang-pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,14 @@ export const I18N_PACK_PL: I18nLangPack = {
emptyPlaceholder: 'Brak plików! Dodaj nowy plik, aby zacząć.',
},
},
pinnedMessages: {
grid: {
placeholder: 'Brak przypiętych wiadomości! Dodaj nową wiadomość, aby zacząć',
},
buttons: {
goToChat: 'Przejdź do czatu',
},
},
experts: {
favorites: {
add: 'Dodaj do ulubionych',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export function ToolbarSmallActionButton({ children, icon, title, disabled, dark
type="button"
className={clsx(
'flex items-center gap-1.5 bg-gray-100/50 px-2 py-1.5 rounded-md text-gray-600 text-xs transition-all',
'border border-gray-200',
'hover:scale-105 active:scale-95',
disabled
? 'opacity-50 cursor-not-allowed'
Expand Down
69 changes: 49 additions & 20 deletions apps/chat/src/modules/chats/conversation/messages/chat-message.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ReactNode } from 'react';

import { useControl } from '@under-control/forms';
import clsx from 'clsx';
import { takeRight } from 'fp-ts/lib/Array';
Expand Down Expand Up @@ -31,16 +33,34 @@ export type SdkRepeatedMessageItemT = SdkRepeatedMessageLike<
>;

type Props = {
className?: string;
message: SdkRepeatedMessageItemT;
isLast: boolean;
actionsToolbar?: ReactNode;
showAnim?: boolean;
isLast?: boolean;
readOnly?: boolean;
archived?: boolean;
onRefreshResponse: (message: Omit<SdkRepeatedMessageItemT, 'content'>) => void;
onReply: (message: SdkRepeatedMessageItemT) => void;
onAction: (action: string) => void;
showToolbars?: boolean;
onRefreshResponse?: (message: Omit<SdkRepeatedMessageItemT, 'content'>) => void;
onReply?: (message: SdkRepeatedMessageItemT) => void;
onAction?: (action: string) => void;
};

export function ChatMessage({ archived, message, isLast, readOnly, onRefreshResponse, onReply, onAction }: Props) {
export function ChatMessage(
{
className,
showAnim = true,
archived,
message,
actionsToolbar,
isLast,
readOnly,
showToolbars = true,
onRefreshResponse,
onReply,
onAction,
}: Props,
) {
const t = useI18n().pack.chat;
const { session } = useSdkForLoggedIn();

Expand All @@ -67,15 +87,16 @@ export function ChatMessage({ archived, message, isLast, readOnly, onRefreshResp
<div
className={clsx(
'flex items-start gap-4',
'animate-messageSlideIn',
showAnim && 'animate-messageSlideIn',
{
'mb-5': !repeats.length,
'mb-10': repeats.length,
'opacity-75': readOnly && archived,
'opacity-0': !readOnly || !archived,
'flex-row-reverse': !isAI && isYou,
'opacity-0': showAnim && (!readOnly || !archived),
'flex-row-reverse': showAnim && (!isAI && isYou),
'bg-purple-50/50 p-6 border border-purple-100 rounded-lg': isPinned,
},
className,
)}
>
{(!isYou || isAI) && (
Expand Down Expand Up @@ -151,7 +172,7 @@ export function ChatMessage({ archived, message, isLast, readOnly, onRefreshResp
'items-end': !isAI && isYou,
})}
>
{!readOnly && isYou && !isAI && (
{!readOnly && isYou && !isAI && onReply && (
<button
type="button"
onClick={() => onReply(message)}
Expand All @@ -168,11 +189,12 @@ export function ChatMessage({ archived, message, isLast, readOnly, onRefreshResp
{!isAI && message.repliedMessage && (
<ChatMessageRepliedMessage message={message.repliedMessage} />
)}

<ChatMessageContent
key={typeof content}
content={content}
disabled={!isLast || readOnly}
showToolbars={isAI}
showToolbars={showToolbars && isAI}
searchResults={message.webSearch.results ?? []}
onAction={onAction}
/>
Expand Down Expand Up @@ -200,21 +222,28 @@ export function ChatMessage({ archived, message, isLast, readOnly, onRefreshResp
{!readOnly && !isYou && (
<div className="flex justify-between items-center">
<div className="flex flex-wrap items-center gap-4 w-full">
<ToolbarSmallActionButton
title={t.actions.reply}
icon={<ReplyIcon size={14} className="text-gray-500" />}
onClick={() => onReply(message)}
/>
{onReply && (
<ToolbarSmallActionButton
title={t.actions.reply}
icon={<ReplyIcon size={14} className="text-gray-500" />}
onClick={() => onReply(message)}
/>
)}

<ChatMessagePinAction messageId={message.id} />

{actionsToolbar}

{isAI && (
<>
<ChatMessageAIActions
isLast={isLast}
message={message}
onRefreshResponse={() => onRefreshResponse(message)}
/>
{onRefreshResponse && (
<ChatMessageAIActions
isLast={!!isLast}
message={message}
onRefreshResponse={() => onRefreshResponse(message)}
/>
)}

{repeats.length > 0 && (
<ChatMessageVariants
{...currentVariant.bind.entire()}
Expand Down
2 changes: 2 additions & 0 deletions apps/chat/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ export * from './chats';
export * from './experts';
export * from './organizations';
export * from './permissions';
export * from './pinned-messages';
export * from './projects';
export * from './s3-buckets';
export * from './search-bar';
export * from './search-engines';
export * from './shared';
export * from './users';
export * from './users-groups';
Expand Down
3 changes: 3 additions & 0 deletions apps/chat/src/modules/pinned-messages/grid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './pinned-message-card';
export * from './pinned-messages-container';
export * from './pinned-messages-placeholder';
43 changes: 43 additions & 0 deletions apps/chat/src/modules/pinned-messages/grid/pinned-message-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { LinkIcon } from 'lucide-react';
import { useLocation } from 'wouter';

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

import { useI18n } from '~/i18n';
import { ChatMessage } from '~/modules/chats';
import { ToolbarSmallActionButton } from '~/modules/chats/conversation/messages/buttons';
import { useSitemap } from '~/routes/use-sitemap';

type Props = {
pinnedMessage: SdkSearchPinnedMessageItemT;
};

export function PinnedMessageCard({ pinnedMessage }: Props) {
const { message } = pinnedMessage;

const sitemap = useSitemap();
const t = useI18n().pack.pinnedMessages.buttons;
const [, navigate] = useLocation();

const onNavigate = () => {
const link = sitemap.chat.generate({ pathParams: { id: message.chat.id } });

navigate(link);
};

return (
<ChatMessage
className="mx-auto mb-0 max-w-[750px]"
message={{ ...message, repeats: [] }}
showToolbars={false}
showAnim={false}
actionsToolbar={(
<ToolbarSmallActionButton
title={t.goToChat}
icon={<LinkIcon size={14} className="text-gray-500" />}
onClick={onNavigate}
/>
)}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { flow } from 'fp-ts/lib/function';
import { useMemo } from 'react';

import {
SdkSearchProjectsInputV,
useSdkForLoggedIn,
useSdkSubscribePinnedMessagesOrThrow,
} from '@llm/sdk';
import { useWorkspaceOrganizationOrThrow } from '~/modules/workspace';
import {
PaginatedList,
PaginationSearchToolbarItem,
PaginationToolbar,
ResetFiltersButton,
useDebouncedPaginatedSearch,
} from '~/ui';

import { PinnedMessagesPlaceholder } from './pinned-messages-placeholder';
import { PinnedMessagesTimeline } from './pinned-messages-timeline';

type Props = {
storeDataInUrl?: boolean;
};

export function PinnedMessagesContainer({ storeDataInUrl = false }: Props) {
const { assignWorkspaceToFilters } = useWorkspaceOrganizationOrThrow();

const { sdks } = useSdkForLoggedIn();
const { loading, pagination, result, reset } = useDebouncedPaginatedSearch({
storeDataInUrl,
schema: SdkSearchProjectsInputV,
fallbackSearchParams: {
limit: 100,
},
fetchResultsTask: flow(assignWorkspaceToFilters, sdks.dashboard.pinnedMessages.search),
});

const allPinnedItems = useSdkSubscribePinnedMessagesOrThrow();
const mappedResult = useMemo(() => {
if (!result || allPinnedItems.loading) {
return null;
}

const mappedItems = result.items.filter(item => allPinnedItems.items.some(pinnedItem => pinnedItem.id === item.id));

return {
...result,
items: mappedItems,
...!mappedItems.length && {
total: 0,
},
};
}, [result, allPinnedItems]);

return (
<section>
<PaginationToolbar className="mb-6">
<PaginationSearchToolbarItem
{...pagination.bind.path('phrase', {
relatedInputs: ({ newGlobalValue, newControlValue }) => ({
...newGlobalValue,
sort: newControlValue ? 'score:desc' : 'createdAt:asc',
}),
})}
/>

<ResetFiltersButton onClick={reset} />
</PaginationToolbar>

<PaginatedList
result={mappedResult}
loading={loading || allPinnedItems.loading}
pagination={pagination.bind.entire()}
withEmptyPlaceholder={false}
>
{({ items, total }) => {
if (!total || allPinnedItems.loading) {
return <PinnedMessagesPlaceholder />;
}

return <PinnedMessagesTimeline items={items} />;
}}
</PaginatedList>
</section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useI18n } from '~/i18n';
import { GhostPlaceholder } from '~/modules/shared';

export function PinnedMessagesPlaceholder() {
const t = useI18n().pack.pinnedMessages.grid;

return (
<GhostPlaceholder>{t.placeholder}</GhostPlaceholder>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { clsx } from 'clsx';

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

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

import { PinnedMessageCard } from './pinned-message-card';

type TimelineProps = {
items: SdkSearchPinnedMessageItemT[];
};

export function PinnedMessagesTimeline({ items }: TimelineProps) {
return (
<div className="relative">
<div className="top-0 bottom-0 left-1/2 absolute bg-gray-200 w-0.5 -translate-x-1/2" />

<div className="relative space-y-12">
{items.map((item, index) => {
const isEven = index % 2 === 0;

return (
<div key={item.id} className="relative">
<div className="top-7 left-1/2 absolute bg-white border-2 border-gray-300 rounded-full w-3 h-3 -translate-x-1/2" />

<div className={clsx('flex items-start', isEven ? 'justify-end pr-[52%]' : 'justify-start pl-[52%]')}>
<div
className="top-1/2 left-1/2 absolute bg-gray-200 w-4 h-0.5"
style={{
transform: `translateX(${isEven ? '-100%' : '0'})`,
}}
/>

<div className="w-full max-w-xl">
<div className="clear-both">
<PinnedMessageCard pinnedMessage={item} />
</div>

<div
className={clsx(
'inline-block bg-gray-50 px-3 pt-4 rounded-full text-gray-600 text-sm',
isEven ? 'float-right' : 'float-left',
)}
>
{formatDate(item.createdAt)}
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
1 change: 1 addition & 0 deletions apps/chat/src/modules/pinned-messages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './grid';
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useI18n } from '~/i18n';
import { LayoutHeader, PageWithNavigationLayout } from '~/layouts';
import { AppsContainer } from '~/modules';
import { PinnedMessagesContainer } from '~/modules';
import { RouteMetaTags } from '~/routes';

export function PinnedMessagesRoute() {
Expand All @@ -14,7 +14,7 @@ export function PinnedMessagesRoute() {
{t.title}
</LayoutHeader>

<AppsContainer storeDataInUrl />
<PinnedMessagesContainer storeDataInUrl />
</PageWithNavigationLayout>
);
}

0 comments on commit e22e5ae

Please sign in to comment.