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

Bot message rendering #7114

Merged
merged 5 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions frontend/app/src/components/bots/BotMessageContext.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import type { BotMessageContext } from "openchat-client";
import Markdown from "../home/Markdown.svelte";

interface Props {
botContext: BotMessageContext;
}

let { botContext }: Props = $props();

let text = $derived(
`@UserId(${botContext.initiator}) executed command ${botContext.commandText}`,
);
</script>

<div class="bot-context">
<Markdown {text} />
</div>

<style lang="scss">
.bot-context {
@include font(light, normal, fs-80);
margin-bottom: $sp4;
}
</style>
1 change: 1 addition & 0 deletions frontend/app/src/components/home/ChatEvent.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
{supportsEdit}
{supportsReply}
{collapsed}
botContext={event.event.botContext}
on:chatWith
on:goToMessageIndex
on:replyPrivatelyTo
Expand Down
60 changes: 36 additions & 24 deletions frontend/app/src/components/home/ChatMessage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
currentCommunityMembers as communityMembers,
currentChatMembersMap as chatMembersMap,
currentChatBlockedUsers,
type BotMessageContext as BotMessageContextType,
} from "openchat-client";
import EmojiPicker from "./EmojiPicker.svelte";
import Avatar from "../Avatar.svelte";
Expand Down Expand Up @@ -63,6 +64,7 @@
import WithRole from "./profile/WithRole.svelte";
import RoleIcon from "./profile/RoleIcon.svelte";
import Badges from "./profile/Badges.svelte";
import BotMessageContext from "../bots/BotMessageContext.svelte";

const client = getContext<OpenChat>("client");
const dispatch = createEventDispatcher();
Expand Down Expand Up @@ -99,6 +101,7 @@
export let dateFormatter: (date: Date) => string = (date) => client.toShortTimeString(date);
export let collapsed: boolean = false;
export let threadRootMessage: Message | undefined;
export let botContext: BotMessageContextType | undefined;

// this is not to do with permission - some messages (namely thread root messages) will simply not support replying or editing inside a thread
export let supportsEdit: boolean;
Expand Down Expand Up @@ -158,6 +161,7 @@
msg.content.kind === "deleted_content" &&
Number(msg.content.timestamp) < $now - 5 * 60 * 1000;
$: canRevealDeleted = deletedByMe && !undeleting && !permanentlyDeleted;
$: edited = msg.edited && !botContext?.finalised;

onMount(() => {
if (!readByMe) {
Expand Down Expand Up @@ -478,29 +482,36 @@
class:rtl={$rtlStore}>
{#if first && !isProposal && !isPrize}
<div class="sender" class:fill class:rtl={$rtlStore}>
<Link underline={"never"} on:click={openUserProfile}>
<h4 class="username" class:fill class:crypto>
{senderDisplayName}
</h4>
<Badges
uniquePerson={sender?.isUniquePerson}
diamondStatus={sender?.diamondStatus}
streak={client.getStreak(sender?.userId)} />
{#if sender !== undefined && multiUserChat}
<WithRole
userId={sender.userId}
chatMembers={$chatMembersMap}
communityMembers={$communityMembers}
let:chatRole
let:communityRole>
<RoleIcon level="community" popup role={communityRole} />
<RoleIcon
level={chatType === "channel" ? "channel" : "group"}
popup
role={chatRole} />
</WithRole>
{/if}
</Link>
{#if botContext !== undefined}
<BotMessageContext {botContext} />
{:else}
<Link underline={"never"} on:click={openUserProfile}>
<h4 class="username" class:fill class:crypto>
{senderDisplayName}
</h4>
<Badges
uniquePerson={sender?.isUniquePerson}
diamondStatus={sender?.diamondStatus}
streak={client.getStreak(sender?.userId)} />
{#if sender !== undefined && multiUserChat}
<WithRole
userId={sender.userId}
chatMembers={$chatMembersMap}
communityMembers={$communityMembers}
let:chatRole
let:communityRole>
<RoleIcon
level="community"
popup
role={communityRole} />
<RoleIcon
level={chatType === "channel" ? "channel" : "group"}
popup
role={chatRole} />
</WithRole>
{/if}
</Link>
{/if}
{#if senderTyping}
<span class="typing">
<Typing />
Expand Down Expand Up @@ -548,7 +559,7 @@
messageId={msg.messageId}
myUserId={user.userId}
content={msg.content}
edited={msg.edited}
{edited}
height={mediaCalculatedHeight}
blockLevelMarkdown={msg.blockLevelMarkdown}
on:removePreview
Expand Down Expand Up @@ -591,6 +602,7 @@
<pre>timestamp: {timestamp}</pre>
<pre>expiresAt: {expiresAt}</pre>
<pre>thread: {JSON.stringify(msg.thread, null, 4)}</pre>
<pre>botContext: {JSON.stringify(botContext, null, 4)}</pre>
{/if}

{#if showChatMenu && intersecting}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@
timestamp={thread.rootMessage.timestamp}
expiresAt={thread.rootMessage.expiresAt}
dateFormatter={(date) => client.toDatetimeString(date)}
msg={thread.rootMessage.event} />
msg={thread.rootMessage.event}
botContext={thread.rootMessage.event.botContext} />
</div>
{#if missingMessages > 0}
<div class="separator">
Expand Down Expand Up @@ -222,7 +223,8 @@
timestamp={evt.timestamp}
expiresAt={evt.expiresAt}
dateFormatter={(date) => client.toDatetimeString(date)}
msg={evt.event} />
msg={evt.event}
botContext={evt.event.botContext} />
{/each}
{/each}
<LinkButton underline="hover" on:click={selectThread}
Expand Down
11 changes: 11 additions & 0 deletions frontend/openchat-agent/src/services/common/chatMappersV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ import type {
SlashCommandSchema,
SlashCommandParamType,
SlashCommandParam,
BotMessageContext,
} from "openchat-shared";
import {
ProposalDecisionStatus,
Expand Down Expand Up @@ -237,6 +238,7 @@ import type {
ImageContent as TImageContent,
LocalUserIndexJoinGroupResponse,
Message as TMessage,
BotMessageContext as TBotMessageContext,
MessageContent as TMessageContent,
MessageContentInitial as TMessageContentInitial,
MessageMatch as TMessageMatch,
Expand Down Expand Up @@ -556,6 +558,15 @@ export function message(value: TMessage): Message {
deleted: content.kind === "deleted_content",
thread: mapOptional(value.thread_summary, threadSummary),
blockLevelMarkdown: value.block_level_markdown,
botContext: mapOptional(value.bot_context, botMessageContext),
};
}

export function botMessageContext(value: TBotMessageContext): BotMessageContext {
return {
initiator: principalBytesToString(value.initiator),
commandText: value.command_text,
finalised: value.finalised,
};
}

Expand Down
86 changes: 83 additions & 3 deletions frontend/openchat-agent/src/services/externalBot/externalBot.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
import { type BotDefinitionResponse } from "openchat-shared";
import { type BotCommandResponse, type BotDefinitionResponse } from "openchat-shared";
import { Value, AssertError } from "@sinclair/typebox/value";
import { Type, type Static } from "@sinclair/typebox";
import { SlashCommandSchema } from "../../typebox";
import { externalBotDefinition } from "../common/chatMappersV2";
import { MessageContent, SlashCommandSchema } from "../../typebox";
import { externalBotDefinition, messageContent } from "../common/chatMappersV2";
import { mapOptional } from "../../utils/mapping";

type ApiBotDefinition = Static<typeof ApiBotDefinition>;
const ApiBotDefinition = Type.Object({
description: Type.String(),
commands: Type.Array(SlashCommandSchema),
});

type ApiBotResponse = Static<typeof ApiBotResponse>;
const ApiBotResponse = Type.Union([
Type.Object({
Success: Type.Object({
message: Type.Optional(
Type.Object({
message_id: Type.String(),
message_content: MessageContent,
}),
),
}),
}),
Type.Object({
BadRequest: Type.Union([
Type.Literal("AccessTokenNotFound"),
Type.Literal("AccessTokenInvalid"),
Type.Literal("AccessTokenExpired"),
Type.Literal("CommandNotFound"),
Type.Literal("ArgsInvalid"),
]),
}),
Type.Object({
InternalError: Type.Any(),
}),
]);

export function getBotDefinition(endpoint: string): Promise<BotDefinitionResponse> {
return fetch(`${endpoint}`)
.then((res) => {
Expand Down Expand Up @@ -47,3 +74,56 @@ function formatError(err: unknown) {
}
return err;
}

function validateBotResponse(json: unknown): BotCommandResponse {
try {
console.log("Bot command response json", json);
const value = Value.Parse(ApiBotResponse, json);
return externalBotResponse(value);
} catch (err) {
return {
kind: "failure",
error: formatError(err),
};
}
}

function externalBotResponse(value: ApiBotResponse): BotCommandResponse {
if ("Success" in value) {
return {
kind: "success",
placeholder: mapOptional(value.Success.message, ({ message_id, message_content }) => {
return {
messageId: BigInt(message_id),
messageContent: messageContent(message_content, ""),
};
}),
};
}
return { kind: "failure", error: value };
}

export function callBotCommandEndpoint(
endpoint: string,
token: string,
): Promise<BotCommandResponse> {
const headers = new Headers();
headers.append("Content-type", "text/plain");
return fetch(`${endpoint}/execute_command`, {
method: "POST",
headers: headers,
body: token,
})
.then((res) => {
if (res.ok) {
return res.json();
} else {
return "InternalError";
}
})
.then(validateBotResponse)
.catch((err) => {
console.log("Bot command failed: ", err);
return { kind: "failure", error: err };
});
}
9 changes: 9 additions & 0 deletions frontend/openchat-agent/src/services/openchatAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,15 @@ export class OpenChatAgent extends EventTarget {

rehydrateUserSummary<T extends UserSummary>(userSummary: T): T {
const ref = userSummary.blobReference;
if (userSummary.kind === "bot") {
return {
...userSummary,
blobData: undefined,
blobUrl: `${this.config.blobUrlPattern
.replace("{canisterId}", this.config.userIndexCanister)
.replace("{blobType}", "avatar")}/${userSummary.userId}/${ref?.blobId}`,
};
}
return {
...userSummary,
blobData: undefined,
Expand Down
Loading
Loading