Skip to content

Commit

Permalink
feat: prompt command /note #1068 (#1069)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikbry authored Jul 10, 2024
1 parent a61fc62 commit 5f1dc20
Show file tree
Hide file tree
Showing 11 changed files with 566 additions and 379 deletions.
69 changes: 67 additions & 2 deletions webapp/features/Threads/Conversation/ConversationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ function ConversationProvider({
conversationMessages: Message[],
conversation: Conversation,
previousMessage: Message | undefined,
modelName: string,
authorName: string,
) => {
let updatedConversation = conversation;
let updatedMessages: Message[] | undefined;
Expand All @@ -162,9 +162,18 @@ function ConversationProvider({
updatedConversation,
conversation.id,
));
} else if (authorName === 'Note') {
message = createMessage({ role: 'note', name: authorName }, content);
message.status = status;
({ updatedConversation, updatedMessages } = await updateMessagesAndConversation(
[message],
getConversationMessages(conversation.id),
conversation,
conversation.id,
));
} else {
const userMessage = createMessage({ role: 'user', name: 'You' }, raw, raw);
message = createMessage({ role: 'assistant', name: modelName || 'Assistant' }, content);
message = createMessage({ role: 'assistant', name: authorName || 'Assistant' }, content);
message.status = status;
userMessage.sibling = message.id;
message.sibling = userMessage.id;
Expand Down Expand Up @@ -252,6 +261,50 @@ function ConversationProvider({
],
);

const handleNote = useCallback(
async (
prompt: ParsedPrompt,
conversation: Conversation,
previousMessage?: Message | undefined,
) => {
let updatedConversation = conversation;

if (!previousMessage) {
updatedConversation =
(await clearPrompt?.(conversation, conversations))?.find(
(c) => c.id === conversation.id,
) || conversation;
}
if (updatedConversation.temp) {
updatedConversation.name = getConversationTitle(updatedConversation, t);
}

const updatedMessages = getConversationMessages(conversation.id);
const message = previousMessage;
await updateMessages(
prompt.raw,
prompt.text,
MessageStatus.Delivered,
updatedMessages,
updatedConversation,
message,
'Note',
);
if (tempConversationId) {
router.replace(`${Page.Threads}/${tempConversationId}`, undefined, { shallow: true });
}
},
[
clearPrompt,
conversations,
getConversationMessages,
router,
t,
tempConversationId,
updateMessages,
],
);

const preProcessingSendMessage = useCallback(
async (
prompt: ParsedPrompt,
Expand Down Expand Up @@ -292,6 +345,17 @@ function ConversationProvider({

return {};
}
if (result.type === 'note') {
setIsProcessing({ ...isProcessing, [conversation.id]: true });
try {
await handleNote(prompt, conversation, previousMessage);
} catch (error) {
setErrorMessage({ ...errorMessages, [conversation.id]: `${error}` });
}
setIsProcessing({ ...isProcessing, [conversation.id]: false });

return {};
}

let selectedAssistant: Assistant | undefined;
if (result.assistantId) {
Expand Down Expand Up @@ -328,6 +392,7 @@ function ConversationProvider({
getAssistant,
getConversationMessages,
handleImageGeneration,
handleNote,
isProcessing,
providers,
selectedConversation,
Expand Down
2 changes: 1 addition & 1 deletion webapp/features/Threads/Conversation/ConversationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { AvatarRef, Conversation, Message, MessageImpl } from '@/types';
import { getConversationAssets } from '@/utils/data/conversations';
import { getMessageFirstAsset } from '@/utils/data/messages';
import { Button } from '../../../components/ui/button';
import MessageView from './MessageView';
import MessageView from './Message';

type ConversationListProps = {
conversation: Conversation;
Expand Down
179 changes: 179 additions & 0 deletions webapp/features/Threads/Conversation/Message/Actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright 2024 Mik Bry
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { ChevronLeft, ChevronRight, Pencil, RotateCcw, Trash2, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { MessageImpl, MessageStatus } from '@/types';
import { shortcutAsText } from '@/utils/shortcuts';
import useTranslation from '@/hooks/useTranslation';
import CopyToClipBoard from '@/components/common/CopyToClipBoard';
import { RefObject } from 'react';
import { ShortcutIds } from '@/hooks/useShortcuts';
import { DisplayMessageState } from './types';

function DeleteButton({ onDeleteMessage }: { onDeleteMessage: () => void }) {
return (
<Button variant="ghost" size="sm" onClick={onDeleteMessage}>
<Trash2 className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
</Button>
);
}

type ActionsProps = {
state: DisplayMessageState;
message: MessageImpl;
content: string;
contentRef: RefObject<HTMLDivElement>;
disabled: boolean;
isUser: boolean;
isHover: boolean;
current: number;
setCurrent: (value: number) => void;
onEdit: () => void;
onCancelEdit: () => void;
onResendMessage: () => void;
onDeleteMessage: () => void;
onDeleteAssets: () => void;
onSave: () => void;
onCopyMessage: (messageId: string, state: boolean) => void;
onCancelSending: () => void;
};

function Actions({
state,
message,
content,
contentRef,
disabled,
isUser,
isHover,
current,
setCurrent,
onEdit,
onCancelEdit,
onResendMessage,
onDeleteMessage,
onDeleteAssets,
onSave,
onCopyMessage,
onCancelSending,
}: ActionsProps) {
const { t } = useTranslation();
return (
<>
{state === DisplayMessageState.FileAsset && isHover && (
<div className="left-34 absolute bottom-0 flex flex-row items-center">
<DeleteButton onDeleteMessage={onDeleteAssets} />
</div>
)}
{(state === DisplayMessageState.Pending || state === DisplayMessageState.Streaming) &&
isHover && (
<div className="left-34 absolute bottom-0 flex flex-row items-center">
<Button variant="ghost" size="sm" onClick={onCancelSending}>
<X className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
</Button>
</div>
)}
{(state === DisplayMessageState.Markdown ||
state === DisplayMessageState.Note ||
state === DisplayMessageState.Text) &&
isHover && (
<div className="left-34 absolute bottom-0 flex flex-row items-center">
{message.contentHistory && message.contentHistory.length > 0 && (
<div className="flex flex-row items-center pt-0 text-xs">
<Button
variant="ghost"
size="sm"
disabled={current === message.contentHistory?.length}
onClick={() => {
setCurrent(current + 1);
}}
className="h-5 w-5 p-1"
>
<ChevronLeft className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
</Button>
<span className="tabular-nums">
{' '}
{message.contentHistory.length - current + 1} /{' '}
{message.contentHistory.length + 1}{' '}
</span>
<Button
variant="ghost"
size="sm"
disabled={current === 0}
onClick={() => {
setCurrent(current - 1);
}}
className="h-5 w-5 p-1"
>
<ChevronRight className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
</Button>
</div>
)}
{!(isUser || state === DisplayMessageState.Note) && (
<Button
variant="ghost"
size="sm"
aria-label={t('Resend message')}
title={`${t('Resend message')} ${message.last ? shortcutAsText(ShortcutIds.RESEND_MESSAGE) : ''} `}
onClick={onResendMessage}
>
<RotateCcw className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
</Button>
)}
<CopyToClipBoard
copied={message.copied}
title={`${t('Copy message to clipboard')} ${message.last && !isUser ? shortcutAsText(ShortcutIds.COPY_MESSAGE) : ''} `}
message={t('Message copied to clipboard')}
text={content}
options={{ html: contentRef.current?.outerHTML ?? undefined }}
onCopy={(copied) => onCopyMessage(message.id, copied)}
/>
{message.status !== MessageStatus.Error && (
<Button
variant="ghost"
aria-label={t('Edit message')}
title={`${t('Edit message')} ${message.last && isUser ? shortcutAsText(ShortcutIds.EDIT_MESSAGE) : ''} `}
size="sm"
onClick={onEdit}
>
<Pencil className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
</Button>
)}
<Button
variant="ghost"
aria-label={t('Delete message and siblings')}
title={`${t('Delete message and siblings')} ${message.last ? shortcutAsText(ShortcutIds.DELETE_MESSAGE) : ''} `}
size="sm"
onClick={onDeleteMessage}
>
<Trash2 className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
</Button>
</div>
)}
{state === DisplayMessageState.Edit && (
<div className="left-30 absolute -bottom-2 flex flex-row gap-2">
<Button size="sm" onClick={onSave} disabled={disabled} className="my-2 h-[28px] py-2">
{isUser ? t('Save & submit') : t('Save')}
</Button>
<Button size="sm" variant="outline" onClick={onCancelEdit} className="my-2 h-[28px] py-2">
{t('Cancel')}
</Button>
</div>
)}
</>
);
}

export default Actions;
36 changes: 36 additions & 0 deletions webapp/features/Threads/Conversation/Message/AvatarIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2024 Mik Bry
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { Bot, Notebook, User } from 'lucide-react';
import AvatarView from '@/components/common/AvatarView';
import OpenAI from '@/components/icons/OpenAI';
import { Avatar } from '@/types';

function AvatarIcon({ isUser, avatar }: { isUser: boolean; avatar: Avatar }) {
let icon: React.ReactNode;
const className = 'h-4 w-4 text-muted-foreground';
if (isUser) {
icon = <User className={className} />;
} else if (avatar.name?.toLowerCase().startsWith('gpt-')) {
icon = <OpenAI className={className} />;
} else if (avatar.name === 'Note') {
icon = <Notebook className={className} />;
} else {
icon = <Bot className={className} />;
}

return <AvatarView avatar={avatar} icon={icon} className={className} />;
}

export default AvatarIcon;
Loading

0 comments on commit 5f1dc20

Please sign in to comment.