Skip to content

Commit c0edd79

Browse files
authored
Edit history viewer (#3322)
Followup PR to #3280 Additions: - Endpoint for fetching the revision history of a message - Card which allows moderators to view the revision history of a message
1 parent b0bb643 commit c0edd79

File tree

12 files changed

+209
-25
lines changed

12 files changed

+209
-25
lines changed

backend/oasst_backend/api/v1/messages.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,22 @@ def edit_tx(session: deps.Session):
354354
edit_tx()
355355

356356

357+
@router.get("/{message_id}/history", response_model=list[protocol.MessageRevision])
358+
def get_revision_history(
359+
*,
360+
message_id: UUID,
361+
frontend_user: deps.FrontendUserId = Depends(deps.get_frontend_user_id),
362+
api_client: ApiClient = Depends(deps.get_trusted_api_client),
363+
db: Session = Depends(deps.get_db),
364+
):
365+
"""
366+
Get all revisions of this message sorted from oldest to most recent
367+
"""
368+
pr = PromptRepository(db, api_client, frontend_user=frontend_user)
369+
revisions = pr.fetch_message_revision_history(message_id)
370+
return utils.prepare_message_revision_list(revisions)
371+
372+
357373
@router.post("/{message_id}/emoji", status_code=HTTP_202_ACCEPTED)
358374
def post_message_emoji(
359375
*,

backend/oasst_backend/api/v1/utils.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import re
22
from uuid import UUID
33

4-
from oasst_backend.models import Message
4+
from oasst_backend.models import Message, MessageRevision
55
from oasst_shared.schemas import protocol
66

77

@@ -66,6 +66,21 @@ def prepare_tree(tree: list[Message], tree_id: UUID) -> protocol.MessageTree:
6666
return protocol.MessageTree(id=tree_id, messages=tree_messages)
6767

6868

69+
def prepare_message_revision(revision: MessageRevision) -> protocol.MessageRevision:
70+
return protocol.MessageRevision(
71+
id=revision.id,
72+
text=revision.payload.payload.text,
73+
message_id=revision.message_id,
74+
user_id=revision.user_id,
75+
created_date=revision.created_date,
76+
user_is_author=revision._user_is_author,
77+
)
78+
79+
80+
def prepare_message_revision_list(revisions: list[MessageRevision]) -> list[protocol.MessageRevision]:
81+
return [prepare_message_revision(revision) for revision in revisions]
82+
83+
6984
split_uuid_pattern = re.compile(
7085
r"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\$(.*)$"
7186
)

backend/oasst_backend/models/message_revision.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import sqlalchemy as sa
66
import sqlalchemy.dialects.postgresql as pg
7+
from pydantic import PrivateAttr
78
from sqlmodel import Field, SQLModel
89
from uuid_extensions import uuid7
910

@@ -23,3 +24,5 @@ class MessageRevision(SQLModel, table=True):
2324
created_date: Optional[datetime] = Field(
2425
sa_column=sa.Column(sa.DateTime(timezone=True), nullable=True, server_default=sa.func.current_timestamp())
2526
)
27+
28+
_user_is_author: Optional[bool] = PrivateAttr(default=None)

backend/oasst_backend/prompt_repository.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,16 @@ def fetch_message_text_labels(self, message_id: UUID, user_id: Optional[UUID] =
800800
query = query.filter(TextLabels.user_id == user_id)
801801
return query.all()
802802

803+
def fetch_message_revision_history(self, message_id: UUID) -> list[MessageRevision]:
804+
# the revisions are sorted by time using the uuid7 id
805+
revisions: list[MessageRevision] = sorted(
806+
self.db.query(MessageRevision).filter(MessageRevision.message_id == message_id).all(),
807+
key=lambda revision: revision.id.int >> 80,
808+
)
809+
for revision in revisions:
810+
revision._user_is_author = self.user_id == revision.user_id
811+
return revisions
812+
803813
@staticmethod
804814
def trace_conversation(messages: list[Message] | dict[UUID, Message], last_message: Message) -> list[Message]:
805815
"""

oasst-shared/oasst_shared/schemas/protocol.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@ class Message(ConversationMessage):
117117
user: Optional[FrontEndUser]
118118

119119

120+
class MessageRevision(BaseModel):
121+
id: UUID
122+
text: str
123+
message_id: UUID
124+
user_id: Optional[UUID]
125+
created_date: Optional[datetime]
126+
user_is_author: Optional[bool]
127+
128+
120129
class MessagePage(PageResult):
121130
items: list[Message]
122131

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Text, useColorModeValue } from "@chakra-ui/react";
2+
import { useCurrentLocale } from "src/hooks/locale/useCurrentLocale";
3+
4+
export const MessageCreateDate = ({ date }: { date: string }) => {
5+
const locale = useCurrentLocale();
6+
const createdDateColor = useColorModeValue("blackAlpha.600", "gray.400");
7+
8+
return (
9+
<Text as="span" fontSize="small" color={createdDateColor} fontWeight="medium" me={{ base: 3, md: 6 }}>
10+
{new Intl.DateTimeFormat(locale, {
11+
hour: "2-digit",
12+
minute: "2-digit",
13+
year: "numeric",
14+
month: "2-digit",
15+
day: "2-digit",
16+
}).format(new Date(date))}
17+
</Text>
18+
);
19+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Badge, Flex, Stack, Tooltip } from "@chakra-ui/react";
2+
import { boolean } from "boolean";
3+
import { User } from "lucide-react";
4+
import { useRouter } from "next/router";
5+
import { useTranslation } from "next-i18next";
6+
import { ROUTES } from "src/lib/routes";
7+
import { Message, MessageRevision } from "src/types/Conversation";
8+
9+
import { BaseMessageEntry } from "./BaseMessageEntry";
10+
import { MessageCreateDate } from "./MessageCreateDate";
11+
import { BaseMessageEmojiButton } from "./MessageEmojiButton";
12+
import { MessageInlineEmojiRow } from "./MessageInlineEmojiRow";
13+
14+
export interface MessageHistoryTableProps {
15+
message: Message;
16+
revisions: MessageRevision[];
17+
}
18+
19+
export function MessageHistoryTable({ message, revisions }: MessageHistoryTableProps) {
20+
const { t } = useTranslation(["message"]);
21+
const router = useRouter();
22+
23+
return (
24+
<Stack spacing={4}>
25+
{(revisions.length === 0
26+
? ([
27+
{
28+
text: message.text,
29+
created_date: message.created_date,
30+
user_id: message.user_id,
31+
user_is_author: message.user_is_author,
32+
},
33+
] as Omit<MessageRevision, "id" | "message_id">[])
34+
: (revisions.map((revision) => ({
35+
text: revision.text,
36+
created_date: revision.created_date,
37+
user_id: revision.user_id,
38+
user_is_author: revision.user_is_author,
39+
})) as Omit<MessageRevision, "id" | "message_id">[])
40+
).map(({ text, created_date, user_id, user_is_author }, index, array) => (
41+
<BaseMessageEntry
42+
key={`version-${index}`}
43+
content={text}
44+
avatarProps={{
45+
name: `${boolean(message.is_assistant) ? "Assistant" : "User"}`,
46+
src: `${boolean(message.is_assistant) ? "/images/logos/logo.png" : "/images/temp-avatars/av1.jpg"}`,
47+
}}
48+
highlight={index === array.length - 1}
49+
>
50+
<Flex justifyContent={"space-between"} marginTop={2} alignItems={"center"}>
51+
<MessageCreateDate date={created_date} />
52+
<MessageInlineEmojiRow>
53+
<BaseMessageEmojiButton
54+
emoji={User}
55+
label="Manage User"
56+
onClick={() => router.push(ROUTES.ADMIN_USER_DETAIL(user_id))}
57+
/>
58+
</MessageInlineEmojiRow>
59+
</Flex>
60+
<Flex
61+
position={"absolute"}
62+
gap="2"
63+
top="-2.5"
64+
style={{
65+
insetInlineEnd: "1.25rem",
66+
}}
67+
>
68+
{index === 0 && (
69+
<Tooltip label={"This is the original version of this message"} placement="top">
70+
<Badge colorScheme={"blue"}>Original</Badge>
71+
</Tooltip>
72+
)}
73+
{user_is_author && (
74+
<Tooltip label={t("message_author_explain")} placement="top">
75+
<Badge size="sm" colorScheme="green" textTransform="capitalize">
76+
{t("message_author")}
77+
</Badge>
78+
</Tooltip>
79+
)}
80+
</Flex>
81+
</BaseMessageEntry>
82+
))}
83+
</Stack>
84+
);
85+
}

website/src/components/Messages/MessageTableEntry.tsx

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
MenuList,
1010
Portal,
1111
SimpleGrid,
12-
Text,
1312
Tooltip,
1413
useColorModeValue,
1514
useDisclosure,
@@ -39,7 +38,6 @@ import { LabelMessagePopup } from "src/components/Messages/LabelPopup";
3938
import { MessageEmojiButton } from "src/components/Messages/MessageEmojiButton";
4039
import { ReportPopup } from "src/components/Messages/ReportPopup";
4140
import { useHasAnyRole } from "src/hooks/auth/useHasAnyRole";
42-
import { useCurrentLocale } from "src/hooks/locale/useCurrentLocale";
4341
import { useDeleteMessage } from "src/hooks/message/useDeleteMessage";
4442
import { post, put } from "src/lib/api";
4543
import { ROUTES } from "src/lib/routes";
@@ -52,6 +50,7 @@ import { useUndeleteMessage } from "../../hooks/message/useUndeleteMessage";
5250
import { BaseMessageEntry } from "./BaseMessageEntry";
5351
import { MessageInlineEmojiRow } from "./MessageInlineEmojiRow";
5452
import { MessageSyntheticBadge } from "./MessageSyntheticBadge";
53+
import { MessageCreateDate } from "./MessageCreateDate";
5554

5655
interface MessageTableEntryProps {
5756
message: Message;
@@ -113,7 +112,7 @@ export const MessageTableEntry = forwardRef<HTMLDivElement, MessageTableEntryPro
113112
>
114113
<Flex justifyContent="space-between" mt="2" alignItems="center">
115114
{showCreatedDate ? (
116-
<MessageCreateDate date={message.created_date}></MessageCreateDate>
115+
<MessageCreateDate date={message.created_date} />
117116
) : (
118117
// empty span is required to make emoji displayed at the end of row
119118
<span></span>
@@ -180,24 +179,6 @@ export const MessageTableEntry = forwardRef<HTMLDivElement, MessageTableEntryPro
180179
}
181180
);
182181

183-
const me = { base: 3, md: 6 };
184-
185-
const MessageCreateDate = ({ date }: { date: string }) => {
186-
const locale = useCurrentLocale();
187-
const createdDateColor = useColorModeValue("blackAlpha.600", "gray.400");
188-
return (
189-
<Text as="span" fontSize="small" color={createdDateColor} fontWeight="medium" me={me}>
190-
{new Intl.DateTimeFormat(locale, {
191-
hour: "2-digit",
192-
minute: "2-digit",
193-
year: "numeric",
194-
month: "2-digit",
195-
day: "2-digit",
196-
}).format(new Date(date))}
197-
</Text>
198-
);
199-
};
200-
201182
const EmojiMenuItem = ({
202183
emoji,
203184
checked,

website/src/lib/oasst_api_client.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EmojiOp, FetchMessagesCursorResponse, Message } from "src/types/Conversation";
1+
import type { EmojiOp, FetchMessagesCursorResponse, Message, MessageRevision } from "src/types/Conversation";
22
import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
33
import { Stats } from "src/types/Stat";
44
import type { AvailableTasks } from "src/types/Task";
@@ -208,6 +208,13 @@ export class OasstApiClient {
208208
}>(`/api/v1/messages/${message_id}/tree/state`);
209209
}
210210

211+
/**
212+
* Returns a list of revisions assoicated with `message_id`.
213+
*/
214+
async fetch_message_revision_history(message_id: string): Promise<MessageRevision[]> {
215+
return this.get<MessageRevision[]>(`/api/v1/messages/${message_id}/history`);
216+
}
217+
211218
/**
212219
* Delete a message by its id
213220
*/

website/src/pages/admin/messages/[id].tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ export { getServerSideProps } from "src/lib/defaultServerSideProps";
1919
import { AdminArea } from "src/components/AdminArea";
2020
import { JsonCard } from "src/components/JsonCard";
2121
import { AdminLayout } from "src/components/Layout";
22+
import { MessageHistoryTable } from "src/components/Messages/MessageHistoryTable";
2223
import { MessageTree } from "src/components/Messages/MessageTree";
2324
import { get } from "src/lib/api";
24-
import { Message, MessageWithChildren } from "src/types/Conversation";
25+
import { Message, MessageRevision, MessageWithChildren } from "src/types/Conversation";
2526
import useSWRImmutable from "swr/immutable";
2627

2728
const MessageDetail = () => {
@@ -42,16 +43,24 @@ const MessageDetail = () => {
4243
}>(`/api/admin/messages/${messageId}/tree`, get, {
4344
keepPreviousData: true,
4445
});
46+
const {
47+
data: revisions,
48+
isLoading: revisionsLoading,
49+
error: revisionError,
50+
} = useSWRImmutable<MessageRevision[]>(`/api/admin/messages/${messageId}/history`, get, { keepPreviousData: true });
4551

4652
return (
4753
<>
4854
<Head>
4955
<title>Open Assistant</title>
5056
</Head>
5157
<AdminArea>
52-
{isLoading && !data && <CircularProgress isIndeterminate></CircularProgress>}
58+
{(isLoading && !data) ||
59+
(revisionsLoading && !revisions && <CircularProgress isIndeterminate></CircularProgress>)}
5360
{error && "Unable to load message tree"}
61+
{revisionError && "Unable to load message revision history"}
5462
{data &&
63+
revisions &&
5564
(data.tree === null ? (
5665
"Unable to build tree"
5766
) : (
@@ -64,6 +73,14 @@ const MessageDetail = () => {
6473
<JsonCard>{data.message}</JsonCard>
6574
</CardBody>
6675
</Card>
76+
<Card>
77+
<CardHeader fontWeight="bold" fontSize="xl" pb="0">
78+
Message History
79+
</CardHeader>
80+
<CardBody>
81+
<MessageHistoryTable message={data?.message} revisions={revisions} />
82+
</CardBody>
83+
</Card>
6784
<Card>
6885
<CardHeader fontWeight="bold" fontSize="xl" pb="0">
6986
Tree {data.tree.id}

0 commit comments

Comments
 (0)