Skip to content

Commit

Permalink
Meeting postback (#187)
Browse files Browse the repository at this point in the history
* Meeting postback

* Spelling

* Fix post selection for non-ai posts

* Add help text for meeting summary
  • Loading branch information
crspeller authored May 29, 2024
1 parent 1ac40f7 commit 798c326
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 10 deletions.
1 change: 1 addition & 0 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
postRouter.POST("/summarize_transcription", p.handleSummarizeTranscription)
postRouter.POST("/stop", p.handleStop)
postRouter.POST("/regenerate", p.handleRegenerate)
postRouter.POST("/postback_summary", p.handlePostbackSummary)

channelRouter := botRequriedRouter.Group("/channel/:channelid")
channelRouter.Use(p.channelAuthorizationRequired)
Expand Down
73 changes: 67 additions & 6 deletions server/api_post.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func (p *Plugin) regeneratePost(bot *Bot, post *model.Post, user *model.User, ch
defer p.finishPostStreaming(post.Id)

summaryPostIDProp := post.GetProp(ThreadIDProp)
refrencedRecordingFileIDProp := post.GetProp(ReferencedRecordingFileID)
referenceRecordingFileIDProp := post.GetProp(ReferencedRecordingFileID)
referencedTranscriptPostProp := post.GetProp(ReferencedTranscriptPostID)
var result *ai.TextStreamResult
switch {
Expand All @@ -288,11 +288,11 @@ func (p *Plugin) regeneratePost(bot *Bot, post *model.Post, user *model.User, ch
if err != nil {
return fmt.Errorf("could not summarize post on regen: %w", err)
}
case refrencedRecordingFileIDProp != nil:
case referenceRecordingFileIDProp != nil:
post.Message = ""
refrencedRecordingFileID := refrencedRecordingFileIDProp.(string)
referencedRecordingFileID := referenceRecordingFileIDProp.(string)

fileInfo, err := p.pluginAPI.File.GetInfo(refrencedRecordingFileID)
fileInfo, err := p.pluginAPI.File.GetInfo(referencedRecordingFileID)
if err != nil {
return fmt.Errorf("could not get transcription file on regen: %w", err)
}
Expand Down Expand Up @@ -322,8 +322,8 @@ func (p *Plugin) regeneratePost(bot *Bot, post *model.Post, user *model.User, ch
}
case referencedTranscriptPostProp != nil:
post.Message = ""
refrencedTranscriptionPostID := referencedTranscriptPostProp.(string)
referencedTranscriptionPost, err := p.pluginAPI.Post.GetPost(refrencedTranscriptionPostID)
referencedTranscriptionPostID := referencedTranscriptPostProp.(string)
referencedTranscriptionPost, err := p.pluginAPI.Post.GetPost(referencedTranscriptionPostID)
if err != nil {
return fmt.Errorf("could not get transcription post on regen: %w", err)
}
Expand Down Expand Up @@ -373,3 +373,64 @@ func (p *Plugin) regeneratePost(bot *Bot, post *model.Post, user *model.User, ch

return nil
}

func (p *Plugin) handlePostbackSummary(c *gin.Context) {
userID := c.GetHeader("Mattermost-User-Id")
post := c.MustGet(ContextPostKey).(*model.Post)

bot := p.GetBotByID(post.UserId)
if bot == nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("unable to get bot"))
return
}

if post.GetProp(LLMRequesterUserID) != userID {
c.AbortWithError(http.StatusForbidden, errors.New("only the original requester can post back"))
return
}

transcriptThreadRootPost, err := p.pluginAPI.Post.GetPost(post.RootId)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("unable to get transcript thread root post: %w", err))
return
}

originalTranscriptPostID, ok := transcriptThreadRootPost.GetProp(ReferencedTranscriptPostID).(string)
if !ok || originalTranscriptPostID == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("post missing reference to transcription post ID"))
return
}

transcriptionPost, err := p.pluginAPI.Post.GetPost(originalTranscriptPostID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("unable to get transcription post: %w", err))
return
}

if !p.pluginAPI.User.HasPermissionToChannel(userID, transcriptionPost.ChannelId, model.PermissionCreatePost) {
c.AbortWithError(http.StatusForbidden, errors.New("user doesn't have permission to create a post in the transcript channel"))
return
}

postedSummary := &model.Post{
UserId: bot.mmBot.UserId,
ChannelId: transcriptionPost.ChannelId,
RootId: transcriptionPost.RootId,
Message: post.Message,
Type: "custom_llm_postback",
}
postedSummary.AddProp("userid", userID)
if err := p.pluginAPI.Post.CreatePost(postedSummary); err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("unable to post back summary: %w", err))
return
}

data := struct {
PostID string `json:"rootid"`
ChannelID string `json:"channelid"`
}{
PostID: postedSummary.RootId,
ChannelID: postedSummary.ChannelId,
}
c.Render(http.StatusOK, render.JSON{Data: data})
}
1 change: 1 addition & 0 deletions server/meeting_summarization.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func (p *Plugin) newCallTranscriptionSummaryThread(bot *Bot, requestingUser *mod
Message: fmt.Sprintf("Sure, I will summarize this transcription: %s/_redirect/pl/%s\n", *siteURL, transcriptionPost.Id),
}
surePost.AddProp(NoRegen, "true")
surePost.AddProp(ReferencedTranscriptPostID, transcriptionPost.Id)
if err := p.botDM(bot.mmBot.UserId, requestingUser.Id, surePost); err != nil {
return nil, err
}
Expand Down
17 changes: 17 additions & 0 deletions webapp/src/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ export async function doRegenerate(postid: string) {
});
}

export async function doPostbackSummary(postid: string) {
const url = `${postRoute(postid)}/postback_summary`;
const response = await fetch(url, Client4.getOptions({
method: 'POST',
}));

if (response.ok) {
return response.json();
}

throw new ClientError(Client4.url, {
message: '',
status_code: response.status,
url,
});
}

export async function summarizeChannelSince(channelID: string, since: number, prompt: string, botUsername: string) {
const url = `${channelRoute(channelID)}/since?botUsername=${botUsername}`;
const response = await fetch(url, Client4.getOptions({
Expand Down
63 changes: 60 additions & 3 deletions webapp/src/components/llmbot_post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import styled, {css, createGlobalStyle} from 'styled-components';
import {WebSocketMessage} from '@mattermost/client';
import {GlobalState} from '@mattermost/types/store';

import {doRegenerate, doStopGenerating} from '@/client';
import {SendIcon} from '@mattermost/compass-icons/components';

import {doPostbackSummary, doRegenerate, doStopGenerating} from '@/client';

import {useSelectNotAIPost, useSelectPost} from '@/hooks';

import PostText from './post_text';
import IconRegenerate from './assets/icon_regenerate';
Expand Down Expand Up @@ -41,9 +45,10 @@ const PostBody = styled.div<{disableHover?: boolean}>`
const ControlsBar = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
justify-content: left;
height: 28px;
margin-top: 8px;
gap: 4px;
`;

const GenerationButton = styled.button`
Expand Down Expand Up @@ -72,6 +77,20 @@ const GenerationButton = styled.button`
}
`;

const PostSummaryButton = styled(GenerationButton)`
background: var(--button-bg);
color: var(--button-color);
:hover {
background: rgba(var(--button-bg-rgb), 0.88);
color: var(--button-color);
}
:active {
background: rgba(var(--button-bg-rgb), 0.92);
}
`;

const StopGeneratingButton = styled.button`
display: flex;
padding: 5px 12px;
Expand All @@ -95,6 +114,18 @@ const StopGeneratingButton = styled.button`
font-weight: 600;
`;

const PostSummaryHelpMessage = styled.div`
font-size: 14px;
font-style: italic;
font-weight: 400;
line-height: 20px;
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.12);
padding-top: 8px;
padding-bottom: 8px;
margin-top: 16px;
`;

export interface PostUpdateWebsocketMessage {
next: string
post_id: string
Expand All @@ -108,6 +139,7 @@ interface Props {
}

export const LLMBotPost = (props: Props) => {
const selectPost = useSelectNotAIPost();
const [message, setMessage] = useState(props.post.message);

// Generating is true while we are reciving new content from the websocket
Expand All @@ -120,6 +152,7 @@ export const LLMBotPost = (props: Props) => {
stoppedRef.current = stopped;

const currentUserId = useSelector<GlobalState, string>((state) => state.entities.users.currentUserId);
const rootPost = useSelector<GlobalState, any>((state) => state.entities.posts.posts[props.post.root_id]);

useEffect(() => {
props.websocketRegister(props.post.id, (msg: WebSocketMessage<PostUpdateWebsocketMessage>) => {
Expand Down Expand Up @@ -159,9 +192,15 @@ export const LLMBotPost = (props: Props) => {
}
};

const postSummary = async () => {
const result = await doPostbackSummary(props.post.id);
selectPost(result.rootid, result.channelid);
};

const requesterIsCurrentUser = (props.post.props?.llm_requester_user_id === currentUserId);
const isThreadSummaryPost = (props.post.props?.referenced_thread && props.post.props?.referenced_thread !== '');
const isNoShowRegen = (props.post.props?.no_regen && props.post.props?.no_regen !== '');
const isTranscriptionResult = rootPost?.props?.referenced_transcript_post_id && rootPost?.props?.referenced_transcript_post_id !== '';

let permalinkView = null;
if (PostMessagePreview) { // Ignore permalink if version does not exporrt PostMessagePreview
Expand All @@ -177,6 +216,8 @@ export const LLMBotPost = (props: Props) => {
}

const showRegenerate = !generating && requesterIsCurrentUser && !isNoShowRegen;
const showPostbackButton = !generating && requesterIsCurrentUser && isTranscriptionResult;
const showControlsBar = (showRegenerate || showPostbackButton) && message !== '';

return (
<PostBody
Expand Down Expand Up @@ -207,15 +248,31 @@ export const LLMBotPost = (props: Props) => {
{'Stop Generating'}
</StopGeneratingButton>
}
{ showRegenerate &&
{ showPostbackButton &&
<PostSummaryHelpMessage>
{'Would you like to post this summary to the original call thread? You can also ask Copilot to make changes.'}
</PostSummaryHelpMessage>
}
{ showControlsBar &&
<ControlsBar>
{showPostbackButton &&
<PostSummaryButton
data-testid='llm-bot-post-summary'
onClick={postSummary}
>
<SendIcon/>
{'Post summary'}
</PostSummaryButton>
}
{ showRegenerate &&
<GenerationButton
data-testid='regenerate-button'
onClick={regnerate}
>
<IconRegenerate/>
{'Regenerate'}
</GenerationButton>
}
</ControlsBar>
}
</PostBody>
Expand Down
27 changes: 27 additions & 0 deletions webapp/src/components/postback_post.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import {useSelector} from 'react-redux';

import {GlobalState} from '@mattermost/types/store';

import PostText from './post_text';

interface Props {
post: any;
}

export const PostbackPost = (props: Props) => {
const editorUsername = useSelector<GlobalState, string>((state) => state.entities.users.profiles[props.post.props.userid]?.username);
const botUsername = useSelector<GlobalState, string>((state) => state.entities.users.profiles[props.post.user_id]?.username);
const userMotificationMessage = 'This summary was created by ' + botUsername + ' then edited and posted by @' + editorUsername;
return (
<>
<PostText
message={props.post.message}
channelID={props.post.channel_id}
postID={props.post.id}
/>
<br/>
<i>{userMotificationMessage}</i>
</>
);
};
14 changes: 13 additions & 1 deletion webapp/src/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useDispatch} from 'react-redux';

import {selectPost, openRHS} from 'src/redux_actions';
import {selectPost, openRHS, selectRegularPost} from 'src/redux_actions';

import {viewMyChannel} from 'src/client';

Expand Down Expand Up @@ -32,3 +32,15 @@ export const useSelectPost = () => {
};
};

export const doSelectNotAIPost = (postid: string, channelid: string, dispatch: any) => {
dispatch(selectRegularPost(postid, channelid));
viewMyChannel(channelid);
};

export const useSelectNotAIPost = () => {
const dispatch = useDispatch();

return (postid: string, channelid: string) => {
doSelectNotAIPost(postid, channelid, dispatch);
};
};
2 changes: 2 additions & 0 deletions webapp/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import PostEventListener from './websocket';
import {setupRedux} from './redux';
import UnreadsSumarize from './components/unreads_summarize';
import {Pill} from './components/pill';
import {PostbackPost} from './components/postback_post';

type WebappStore = Store<GlobalState, Action<Record<string, unknown>>>

Expand Down Expand Up @@ -130,6 +131,7 @@ export default class Plugin {
};

registry.registerPostTypeComponent('custom_llmbot', LLMBotPostWithWebsockets);
registry.registerPostTypeComponent('custom_llm_postback', PostbackPost);
if (registry.registerPostActionComponent) {
registry.registerPostActionComponent(PostMenu);
} else {
Expand Down
8 changes: 8 additions & 0 deletions webapp/src/redux_actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ export const openRHS = () => {
export const setOpenRHSAction = (action: any) => {
openRHSAction = action;
};

export const selectRegularPost = (postid: string, channelid: string) => {
return {
type: 'SELECT_POST',
postId: postid,
channelId: channelid,
};
};

0 comments on commit 798c326

Please sign in to comment.