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

unread message indicator, message previews, user app metadata on conversations, message pinning #192

Merged
merged 10 commits into from
Oct 31, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ def _error_check(self) -> None:
# should this throw an exception?

def reset(self) -> None:
self = Step() # not sure what to do about this squiggly.
# TODO: consider if this is the right way to reset a step, fix the # noqa: F841
self = Step() # noqa: F841

def set_name(self, name: StepName) -> None:
if name is StepName.UNDEFINED: # need to reset step
Expand Down Expand Up @@ -113,7 +114,8 @@ def _error_check(self) -> None:
# should this throw an exception?

def reset(self) -> None:
self = Mode() # not sure what to do about this squiggly.
# TODO: consider if this is the right way to reset a mode, fix the # noqa: F841
self = Mode() # noqa: F841

def set_name(self, name: ModeName) -> None:
if name is ModeName.UNDEFINED: # need to reset mode
Expand Down
3 changes: 2 additions & 1 deletion workbench-app/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@
"workbenchservice",
"workflowdefinition",
"workflowrun",
"workflowuserparticipant"
"workflowuserparticipant",
"YYYYMMDDH"
]
}
7 changes: 4 additions & 3 deletions workbench-app/src/components/App/PresenceMotionList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

import { makeStyles, tokens } from '@fluentui/react-components';
import { makeStyles, mergeClasses, tokens } from '@fluentui/react-components';
import { PresenceGroup, createPresenceComponent, motionTokens } from '@fluentui/react-motions-preview';
import React from 'react';

Expand Down Expand Up @@ -28,15 +28,16 @@ const ItemMotion = createPresenceComponent({
});

interface PresenceMotionListProps {
className?: string;
items?: React.ReactNode[];
}

export const PresenceMotionList: React.FC<PresenceMotionListProps> = (props) => {
const { items } = props;
const { className, items } = props;
const classes = useClasses();

return (
<div className={classes.root}>
<div className={mergeClasses(classes.root, className)}>
<PresenceGroup>
{items?.map((item, index) => (
<ItemMotion key={index}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Form from '@rjsf/fluentui-rc';
import { RegistryWidgetsType } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';
import React from 'react';
import { WorkbenchEventSource } from '../../../libs/WorkbenchEventSource';
import { WorkbenchEventSource, WorkbenchEventSourceType } from '../../../libs/WorkbenchEventSource';
import { useEnvironment } from '../../../libs/useEnvironment';
import { AssistantStateDescription } from '../../../models/AssistantStateDescription';
import { useGetConversationStateQuery, useUpdateConversationStateMutation } from '../../../services/workbench';
Expand Down Expand Up @@ -92,7 +92,11 @@ export const AssistantInspector: React.FC<AssistantInspectorProps> = (props) =>
};

(async () => {
workbenchEventSource = await WorkbenchEventSource.createOrUpdate(environment.url, conversationId);
workbenchEventSource = await WorkbenchEventSource.createOrUpdate(
environment.url,
WorkbenchEventSourceType.Conversation,
conversationId,
);
workbenchEventSource.addEventListener('assistant.state.updated', handleEvent);
})();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { EventSourceMessage } from '@microsoft/fetch-event-source';
import React from 'react';
import { useEnvironment } from '../../../libs/useEnvironment';
import { useInteractCanvasController } from '../../../libs/useInteractCanvasController';
import { WorkbenchEventSource } from '../../../libs/WorkbenchEventSource';
import { WorkbenchEventSource, WorkbenchEventSourceType } from '../../../libs/WorkbenchEventSource';
import { useAppSelector } from '../../../redux/app/hooks';

const useClasses = makeStyles({
Expand Down Expand Up @@ -49,7 +49,11 @@ export const CanvasControls: React.FC<CanvasControlsProps> = (props) => {
};

(async () => {
workbenchEventSource = await WorkbenchEventSource.createOrUpdate(environment.url, conversationId);
workbenchEventSource = await WorkbenchEventSource.createOrUpdate(
environment.url,
WorkbenchEventSourceType.Conversation,
conversationId,
);
workbenchEventSource.addEventListener('assistant.state.focus', handleFocusEvent);
})();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.

import { ArrowDownload24Regular } from '@fluentui/react-icons';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import React from 'react';
import { Utility } from '../../libs/Utility';
import { Conversation } from '../../models/Conversation';
import { ConversationParticipant } from '../../models/ConversationParticipant';
import { useGetConversationMessagesQuery } from '../../services/workbench';
import { CommandButton } from '../App/CommandButton';

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.guess();

interface ConversationTranscriptProps {
conversation: Conversation;
participants: ConversationParticipant[];
Expand All @@ -39,14 +33,14 @@ export const ConversationTranscript: React.FC<ConversationTranscriptProps> = (pr
return;
}

const currentDateTime = dayjs.utc().tz(dayjs.tz.guess()).format('YYYYMMDDHHmmss');
const filename = `transcript_${conversation.title.replaceAll(' ', '_')}_${currentDateTime}.md`;
const timestampForFilename = Utility.getTimestampForFilename();
const filename = `transcript_${conversation.title.replaceAll(' ', '_')}_${timestampForFilename}.md`;

const markdown = messages
.filter((message) => message.messageType !== 'log')
.map((message) => {
const date = dayjs.utc(message.timestamp).tz(dayjs.tz.guess()).format('dddd, MMMM D');
const time = dayjs.utc(message.timestamp).tz(dayjs.tz.guess()).format('h:mm A');
const date = Utility.toFormattedDateString(message.timestamp, 'dddd, MMMM D');
const time = Utility.toFormattedDateString(message.timestamp, 'h:mm A');
const participant = participants.find(
(possible_participant) => possible_participant.id === message.sender.participantId,
);
Expand Down
10 changes: 2 additions & 8 deletions workbench-app/src/components/Conversations/FileItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,16 @@ import {
Tooltip,
} from '@fluentui/react-components';
import { ArrowDownloadRegular, Delete16Regular } from '@fluentui/react-icons';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import React from 'react';
import * as StreamSaver from 'streamsaver';
import { useWorkbenchService } from '../../libs/useWorkbenchService';
import { Utility } from '../../libs/Utility';
import { Conversation } from '../../models/Conversation';
import { ConversationFile } from '../../models/ConversationFile';
import { useDeleteConversationFileMutation } from '../../services/workbench';
import { CommandButton } from '../App/CommandButton';
import { ConversationFileIcon } from './ConversationFileIcon';

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.guess();

const useClasses = makeStyles({
cardHeader: {
'> .fui-CardHeader__header': {
Expand All @@ -53,7 +47,7 @@ export const FileItem: React.FC<FileItemProps> = (props) => {
const workbenchService = useWorkbenchService();
const [deleteConversationFile] = useDeleteConversationFileMutation();

const time = dayjs.utc(conversationFile.updated).tz(dayjs.tz.guess()).format('M/D/YYYY h:mm A');
const time = Utility.toFormattedDateString(conversationFile.updated, 'M/D/YYYY h:mm A');

const sizeToDisplay = (size: number) => {
if (size < 1024) {
Expand Down
28 changes: 23 additions & 5 deletions workbench-app/src/components/Conversations/InteractHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { useLocation } from 'react-router-dom';
import AutoSizer from 'react-virtualized-auto-sizer';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Constants } from '../../Constants';
import { WorkbenchEventSource } from '../../libs/WorkbenchEventSource';
import { Utility } from '../../libs/Utility';
import { WorkbenchEventSource, WorkbenchEventSourceType } from '../../libs/WorkbenchEventSource';
import { useConversationUtility } from '../../libs/useConversationUtility';
import { useEnvironment } from '../../libs/useEnvironment';
import { Conversation } from '../../models/Conversation';
import { conversationMessageFromJSON } from '../../models/ConversationMessage';
import { ConversationMessage, conversationMessageFromJSON } from '../../models/ConversationMessage';
import { ConversationParticipant } from '../../models/ConversationParticipant';
import { useAppDispatch } from '../../redux/app/hooks';
import {
Expand Down Expand Up @@ -62,6 +64,7 @@ export const InteractHistory: React.FC<InteractHistoryProps> = (props) => {
const { hash } = useLocation();
const [items, setItems] = React.useState<React.ReactNode[]>([]);
const [hashItemIndex, setHashItemIndex] = React.useState<number>();
const { setLastRead } = useConversationUtility();
const environment = useEnvironment();
const dispatch = useAppDispatch();

Expand Down Expand Up @@ -110,6 +113,13 @@ export const InteractHistory: React.FC<InteractHistoryProps> = (props) => {
performScrollToBottom();
}, [performScrollToBottom]);

const handleOnRead = React.useCallback(
(message: ConversationMessage) => {
setLastRead(conversation, message.timestamp);
},
[setLastRead, conversation],
);

React.useEffect(() => {
if (isLoadingMessages || !messages) {
setItems([]);
Expand Down Expand Up @@ -150,7 +160,7 @@ export const InteractHistory: React.FC<InteractHistoryProps> = (props) => {
</>
);
}
const date = dayjs.utc(message.timestamp).tz(dayjs.tz.guess()).format('M/D/YY');
const date = Utility.toFormattedDateString(message.timestamp, 'M/D/YY');
let displayDate = false;
if (date !== lastDate) {
displayDate = true;
Expand Down Expand Up @@ -186,6 +196,7 @@ export const InteractHistory: React.FC<InteractHistoryProps> = (props) => {
participant={senderParticipant}
hideParticipant={hideParticipant}
displayDate={displayDate}
onRead={handleOnRead}
/>
</div>
);
Expand All @@ -210,6 +221,7 @@ export const InteractHistory: React.FC<InteractHistoryProps> = (props) => {
classes.item,
classes.status,
conversation,
handleOnRead,
handleParticipantStatusChange,
hash,
hashItemIndex,
Expand Down Expand Up @@ -268,7 +280,11 @@ export const InteractHistory: React.FC<InteractHistoryProps> = (props) => {

(async () => {
// create or update the event source
const workbenchEventSource = await WorkbenchEventSource.createOrUpdate(environment.url, conversation.id);
const workbenchEventSource = await WorkbenchEventSource.createOrUpdate(
environment.url,
WorkbenchEventSourceType.Conversation,
conversation.id,
);
workbenchEventSource.addEventListener('message.created', messageHandler);
workbenchEventSource.addEventListener('message.deleted', messageHandler);
workbenchEventSource.addEventListener('participant.created', participantCreatedHandler);
Expand All @@ -277,7 +293,9 @@ export const InteractHistory: React.FC<InteractHistoryProps> = (props) => {

return () => {
(async () => {
const workbenchEventSource = await WorkbenchEventSource.getInstance();
const workbenchEventSource = await WorkbenchEventSource.getInstance(
WorkbenchEventSourceType.Conversation,
);
workbenchEventSource.removeEventListener('message.created', messageHandler);
workbenchEventSource.removeEventListener('message.deleted', messageHandler);
workbenchEventSource.removeEventListener('participant.created', participantCreatedHandler);
Expand Down
4 changes: 2 additions & 2 deletions workbench-app/src/components/Conversations/InteractInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ export const InteractInput: React.FC<InteractInputProps> = (props) => {
const editorRef = React.useRef<LexicalEditor | null>();
const attachmentInputRef = React.useRef<HTMLInputElement>(null);
const dispatch = useAppDispatch();
const localUserAccount = useLocalUserAccount();
const userId = localUserAccount.getUserId();
const { getUserId } = useLocalUserAccount();
const userId = getUserId();

const {
data: conversationMessages,
Expand Down
44 changes: 32 additions & 12 deletions workbench-app/src/components/Conversations/InteractMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ import {
PersonRegular,
TextBulletListSquareSparkleRegular,
} from '@fluentui/react-icons';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import React from 'react';
import { useConversationUtility } from '../../libs/useConversationUtility';
import { Utility } from '../../libs/Utility';
import { Conversation } from '../../models/Conversation';
import { ConversationMessage } from '../../models/ConversationMessage';
import { ConversationParticipant } from '../../models/ConversationParticipant';
Expand All @@ -48,10 +47,6 @@ import { MessageDelete } from './MessageDelete';
import { MessageLink } from './MessageLink';
import { RewindConversation } from './RewindConversation';

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.guess();

const useClasses = makeStyles({
root: {
display: 'flex',
Expand Down Expand Up @@ -151,17 +146,19 @@ interface InteractMessageProps {
hideParticipant?: boolean;
displayDate?: boolean;
readOnly: boolean;
onRead?: (message: ConversationMessage) => void;
}

export const InteractMessage: React.FC<InteractMessageProps> = (props) => {
const { conversation, message, participant, hideParticipant, displayDate, readOnly } = props;
const { conversation, message, participant, hideParticipant, displayDate, readOnly, onRead } = props;
const classes = useClasses();
const [createConversationMessage] = useCreateConversationMessageMutation();
const { isMessageVisibleRef, isMessageVisible, isUnread } = useConversationUtility();

const isUser = participant.role === 'user';

const date = dayjs.utc(message.timestamp).tz(dayjs.tz.guess()).format('dddd, MMMM D');
const time = dayjs.utc(message.timestamp).tz(dayjs.tz.guess()).format('h:mm A');
const date = Utility.toFormattedDateString(message.timestamp, 'dddd, MMMM D');
const time = Utility.toFormattedDateString(message.timestamp, 'h:mm A');

const attribution = React.useMemo(() => {
if (message.metadata?.attribution) {
Expand All @@ -180,6 +177,13 @@ export const InteractMessage: React.FC<InteractMessageProps> = (props) => {
contentClassName = mergeClasses(contentClassName, classes.userContent);
}

React.useEffect(() => {
// if the message is visible, mark it as read
if (isMessageVisible && isUnread(conversation, message.timestamp)) {
onRead?.(message);
}
}, [isMessageVisible, isUnread, message.timestamp, onRead, conversation, message]);

const content = React.useMemo(() => {
const onSubmit = async (data: string) => {
if (message.metadata?.command) {
Expand Down Expand Up @@ -322,7 +326,7 @@ export const InteractMessage: React.FC<InteractMessageProps> = (props) => {
);
}
const footerContent = (
<div className={classes.footer}>
<div ref={isMessageVisibleRef} className={classes.footer}>
{aiGeneratedDisclaimer}
{footerItems}
</div>
Expand All @@ -338,7 +342,23 @@ export const InteractMessage: React.FC<InteractMessageProps> = (props) => {
{attachmentList}
</>
);
}, [actions, classes, content, contentClassName, isUser, message.filenames, message.messageType, message.metadata]);
}, [
actions,
classes.actions,
classes.attachments,
classes.footer,
classes.generated,
classes.innerContent,
classes.noteContent,
classes.noticeContent,
content,
contentClassName,
isUser,
isMessageVisibleRef,
message.filenames,
message.messageType,
message.metadata,
]);

const renderedContent = getRenderedMessage();

Expand Down
7 changes: 0 additions & 7 deletions workbench-app/src/components/FrontDoor/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

import { makeStyles, mergeClasses, shorthands, tokens } from '@fluentui/react-components';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import React from 'react';
import { useGetAssistantCapabilitiesSet } from '../../../libs/useAssistantCapabilities';
import { Assistant } from '../../../models/Assistant';
Expand All @@ -21,10 +18,6 @@ import { InteractInput } from '../../Conversations/InteractInput';
import { ChatCanvas } from './ChatCanvas';
import { ChatControls } from './ChatControls';

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.guess();

const useClasses = makeStyles({
// centered content, two rows
// first is for chat history, fills the space and scrolls
Expand Down
Loading