Merge pull request #403 from Monadical-SAS/room-config

Room config
This commit is contained in:
2024-09-04 13:58:29 +02:00
committed by GitHub
10 changed files with 388 additions and 47 deletions

View File

@@ -0,0 +1,86 @@
"""Add room options
Revision ID: 62dea3db63a5
Revises: 1340c04426b8
Create Date: 2024-09-03 16:19:26.861027
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "62dea3db63a5"
down_revision: Union[str, None] = "1340c04426b8"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"meeting",
sa.Column(
"is_locked", sa.Boolean(), server_default=sa.text("0"), nullable=False
),
)
op.add_column(
"meeting",
sa.Column("room_mode", sa.String(), server_default="normal", nullable=False),
)
op.add_column(
"meeting",
sa.Column(
"recording_type", sa.String(), server_default="cloud", nullable=False
),
)
op.add_column(
"meeting",
sa.Column(
"recording_trigger",
sa.String(),
server_default="automatic-2nd-participant",
nullable=False,
),
)
op.add_column(
"room",
sa.Column(
"is_locked", sa.Boolean(), server_default=sa.text("0"), nullable=False
),
)
op.add_column(
"room",
sa.Column("room_mode", sa.String(), server_default="normal", nullable=False),
)
op.add_column(
"room",
sa.Column(
"recording_type", sa.String(), server_default="cloud", nullable=False
),
)
op.add_column(
"room",
sa.Column(
"recording_trigger",
sa.String(),
server_default="automatic-2nd-participant",
nullable=False,
),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("room", "recording_trigger")
op.drop_column("room", "recording_type")
op.drop_column("room", "room_mode")
op.drop_column("room", "is_locked")
op.drop_column("meeting", "recording_trigger")
op.drop_column("meeting", "recording_type")
op.drop_column("meeting", "room_mode")
op.drop_column("meeting", "is_locked")
# ### end Alembic commands ###

View File

@@ -1,9 +1,12 @@
from datetime import datetime, timezone
from typing import Literal
import sqlalchemy
from fastapi import HTTPException
from pydantic import BaseModel
from reflector.db import database, metadata
from reflector.db.rooms import Room
from sqlalchemy.sql import false
meetings = sqlalchemy.Table(
"meeting",
@@ -17,6 +20,21 @@ meetings = sqlalchemy.Table(
sqlalchemy.Column("end_date", sqlalchemy.DateTime),
sqlalchemy.Column("user_id", sqlalchemy.String),
sqlalchemy.Column("room_id", sqlalchemy.String),
sqlalchemy.Column(
"is_locked", sqlalchemy.Boolean, nullable=False, server_default=false()
),
sqlalchemy.Column(
"room_mode", sqlalchemy.String, nullable=False, server_default="normal"
),
sqlalchemy.Column(
"recording_type", sqlalchemy.String, nullable=False, server_default="cloud"
),
sqlalchemy.Column(
"recording_trigger",
sqlalchemy.String,
nullable=False,
server_default="automatic-2nd-participant",
),
)
@@ -30,6 +48,12 @@ class Meeting(BaseModel):
end_date: datetime
user_id: str | None = None
room_id: str | None = None
is_locked: bool = False
room_mode: Literal["normal", "group"] = "normal"
recording_type: Literal["none", "local", "cloud"] = "cloud"
recording_trigger: Literal[
"none", "prompt", "automatic", "automatic-2nd-participant"
] = "automatic-2nd-participant"
class MeetingController:
@@ -43,7 +67,7 @@ class MeetingController:
start_date: datetime,
end_date: datetime,
user_id: str,
room_id: str = None,
room: Room,
):
"""
Create a new meeting
@@ -57,7 +81,11 @@ class MeetingController:
start_date=start_date,
end_date=end_date,
user_id=user_id,
room_id=room_id,
room_id=room.id,
is_locked=room.is_locked,
room_mode=room.room_mode,
recording_type=room.recording_type,
recording_trigger=room.recording_trigger,
)
query = meetings.insert().values(**meeting.model_dump())
await database.execute(query)
@@ -77,15 +105,23 @@ class MeetingController:
return Meeting(**result)
async def get_latest(self, room_id: str) -> Meeting:
async def get_latest(self, room: Room) -> Meeting:
"""
Get latest meeting for a room.
"""
end_date = getattr(meetings.c, "end_date")
query = (
meetings.select()
.where(meetings.c.room_id == room_id)
.where(meetings.c.end_date > datetime.now(timezone.utc))
.where(
sqlalchemy.and_(
meetings.c.room_id == room.id,
meetings.c.is_locked == room.is_locked,
meetings.c.room_mode == room.room_mode,
meetings.c.recording_type == room.recording_type,
meetings.c.recording_trigger == room.recording_trigger,
meetings.c.end_date > datetime.now(timezone.utc),
)
)
.order_by(end_date.desc())
)
result = await database.fetch_one(query)

View File

@@ -1,4 +1,5 @@
from datetime import datetime
from typing import Literal
import sqlalchemy
from fastapi import HTTPException
@@ -19,6 +20,21 @@ rooms = sqlalchemy.Table(
),
sqlalchemy.Column("zulip_stream", sqlalchemy.String),
sqlalchemy.Column("zulip_topic", sqlalchemy.String),
sqlalchemy.Column(
"is_locked", sqlalchemy.Boolean, nullable=False, server_default=false()
),
sqlalchemy.Column(
"room_mode", sqlalchemy.String, nullable=False, server_default="normal"
),
sqlalchemy.Column(
"recording_type", sqlalchemy.String, nullable=False, server_default="cloud"
),
sqlalchemy.Column(
"recording_trigger",
sqlalchemy.String,
nullable=False,
server_default="automatic-2nd-participant",
),
)
@@ -30,6 +46,12 @@ class Room(BaseModel):
zulip_auto_post: bool = False
zulip_stream: str = ""
zulip_topic: str = ""
is_locked: bool = False
room_mode: Literal["normal", "group"] = "normal"
recording_type: Literal["none", "local", "cloud"] = "cloud"
recording_trigger: Literal[
"none", "prompt", "automatic", "automatic-2nd-participant"
] = "automatic-2nd-participant"
class RoomController:
@@ -71,6 +93,10 @@ class RoomController:
zulip_auto_post: bool,
zulip_stream: str,
zulip_topic: str,
is_locked: bool,
room_mode: str,
recording_type: str,
recording_trigger: str,
):
"""
Add a new room
@@ -81,6 +107,10 @@ class RoomController:
zulip_auto_post=zulip_auto_post,
zulip_stream=zulip_stream,
zulip_topic=zulip_topic,
is_locked=is_locked,
room_mode=room_mode,
recording_type=recording_type,
recording_trigger=recording_trigger,
)
query = rooms.insert().values(**room.model_dump())
await database.execute(query)

View File

@@ -24,6 +24,10 @@ class Room(BaseModel):
zulip_auto_post: bool
zulip_stream: str
zulip_topic: str
is_locked: bool
room_mode: str
recording_type: str
recording_trigger: str
class Meeting(BaseModel):
@@ -41,6 +45,10 @@ class CreateRoom(BaseModel):
zulip_auto_post: bool
zulip_stream: str
zulip_topic: str
is_locked: bool
room_mode: str
recording_type: str
recording_trigger: str
class UpdateRoom(BaseModel):
@@ -48,6 +56,10 @@ class UpdateRoom(BaseModel):
zulip_auto_post: bool
zulip_stream: str
zulip_topic: str
is_locked: bool
room_mode: str
recording_type: str
recording_trigger: str
class DeletionStatus(BaseModel):
@@ -85,6 +97,10 @@ async def rooms_create(
zulip_auto_post=room.zulip_auto_post,
zulip_stream=room.zulip_stream,
zulip_topic=room.zulip_topic,
is_locked=room.is_locked,
room_mode=room.room_mode,
recording_type=room.recording_type,
recording_trigger=room.recording_trigger,
)
@@ -126,11 +142,13 @@ async def rooms_create_meeting(
if not room:
raise HTTPException(status_code=404, detail="Room not found")
meeting = await meetings_controller.get_latest(room_id=room.id)
meeting = await meetings_controller.get_latest(room=room)
if meeting is None:
start_date = datetime.now(timezone.utc)
end_date = start_date + timedelta(hours=1)
meeting = await create_meeting("", start_date=start_date, end_date=end_date)
meeting = await create_meeting(
"", start_date=start_date, end_date=end_date, room=room
)
meeting = await meetings_controller.create(
id=meeting["meetingId"],
@@ -141,7 +159,10 @@ async def rooms_create_meeting(
start_date=datetime.fromisoformat(meeting["startDate"]),
end_date=datetime.fromisoformat(meeting["endDate"]),
user_id=user_id,
room_id=room.id,
room=room,
)
if user_id is None:
meeting.host_room_url = ""
return meeting

View File

@@ -1,11 +1,12 @@
from datetime import datetime
import httpx
from reflector.db.rooms import Room
from reflector.settings import settings
async def create_meeting(
room_name_prefix: str, start_date: datetime, end_date: datetime
room_name_prefix: str, start_date: datetime, end_date: datetime, room: Room
):
headers = {
"Content-Type": "application/json; charset=utf-8",
@@ -13,14 +14,14 @@ async def create_meeting(
}
data = {
"templateType": "viewerMode",
"isLocked": False,
"isLocked": room.is_locked,
"roomNamePrefix": room_name_prefix,
"roomNamePattern": "uuid",
"roomMode": "normal",
"roomMode": room.room_mode,
"startDate": start_date.isoformat(),
"endDate": end_date.isoformat(),
"recording": {
"type": "cloud",
"type": room.recording_type,
"destination": {
"provider": "s3",
"bucket": settings.AWS_WHEREBY_S3_BUCKET,
@@ -28,7 +29,7 @@ async def create_meeting(
"accessKeySecret": settings.AWS_WHEREBY_ACCESS_KEY_SECRET,
"fileFormat": "mp4",
},
"startTrigger": "automatic-2nd-participant",
"startTrigger": room.recording_trigger,
},
}

View File

@@ -32,7 +32,7 @@ import {
} from "@chakra-ui/react";
import { useContext, useEffect, useState } from "react";
import { Container } from "@chakra-ui/react";
import { FaEllipsisVertical, FaTrash, FaPencil } from "react-icons/fa6";
import { FaEllipsisVertical, FaTrash, FaPencil, FaLink } from "react-icons/fa6";
import useApi from "../../lib/useApi";
import useRoomList from "./useRoomList";
import { DomainContext } from "../../domainContext";
@@ -51,14 +51,31 @@ interface SelectOption extends OptionBase {
const RESERVED_PATHS = ["browse", "rooms", "transcripts"];
const roomModeOptions: Options<SelectOption> = [
{ label: "2-4 people", value: "normal" },
{ label: "2-200 people", value: "group" },
];
const recordingTriggerOptions: Options<SelectOption> = [
{ label: "None", value: "none" },
{ label: "Prompt", value: "prompt" },
{ label: "Automatic", value: "automatic-2nd-participant" },
];
const roomInitialState = {
name: "",
zulipAutoPost: false,
zulipStream: "",
zulipTopic: "",
isLocked: false,
roomMode: "normal",
recordingType: "cloud",
recordingTrigger: "automatic-2nd-participant",
};
export default function RoomsList() {
const { isOpen, onOpen, onClose } = useDisclosure();
const [room, setRoom] = useState({
name: "",
zulipAutoPost: false,
zulipStream: "",
zulipTopic: "",
});
const [room, setRoom] = useState(roomInitialState);
const [isEditing, setIsEditing] = useState(false);
const [editRoomId, setEditRoomId] = useState("");
const api = useApi();
@@ -66,6 +83,7 @@ export default function RoomsList() {
const { loading, response, refetch } = useRoomList(page);
const [streams, setStreams] = useState<Stream[]>([]);
const [error, setError] = useState("");
const [linkCopied, setLinkCopied] = useState("");
const { zulip_streams } = useContext(DomainContext);
@@ -100,6 +118,16 @@ export default function RoomsList() {
.find((stream) => stream.name === room.zulipStream)
?.topics.map((topic) => ({ label: topic, value: topic })) || [];
const handleCopyUrl = (roomName: string) => {
const roomUrl = `${window.location.origin}/${roomName}`;
navigator.clipboard.writeText(roomUrl);
setLinkCopied(roomName);
setTimeout(() => {
setLinkCopied("");
}, 2000);
};
const handleSaveRoom = async () => {
try {
if (RESERVED_PATHS.includes(room.name)) {
@@ -107,32 +135,29 @@ export default function RoomsList() {
return;
}
const roomData = {
name: room.name,
zulip_auto_post: room.zulipAutoPost,
zulip_stream: room.zulipStream,
zulip_topic: room.zulipTopic,
is_locked: room.isLocked,
room_mode: room.roomMode,
recording_type: room.recordingType,
recording_trigger: room.recordingTrigger,
};
if (isEditing) {
await api?.v1RoomsUpdate({
roomId: editRoomId,
requestBody: {
name: room.name,
zulip_auto_post: room.zulipAutoPost,
zulip_stream: room.zulipStream,
zulip_topic: room.zulipTopic,
},
requestBody: roomData,
});
} else {
await api?.v1RoomsCreate({
requestBody: {
name: room.name,
zulip_auto_post: room.zulipAutoPost,
zulip_stream: room.zulipStream,
zulip_topic: room.zulipTopic,
},
requestBody: roomData,
});
}
setRoom({
name: "",
zulipAutoPost: false,
zulipStream: "",
zulipTopic: "",
});
setRoom(roomInitialState);
setIsEditing(false);
setEditRoomId("");
setError("");
@@ -149,6 +174,10 @@ export default function RoomsList() {
zulipAutoPost: roomData.zulip_auto_post,
zulipStream: roomData.zulip_stream,
zulipTopic: roomData.zulip_topic,
isLocked: roomData.is_locked,
roomMode: roomData.room_mode,
recordingType: roomData.recording_type,
recordingTrigger: roomData.recording_trigger,
});
setEditRoomId(roomId);
setIsEditing(true);
@@ -204,12 +233,7 @@ export default function RoomsList() {
colorScheme="blue"
onClick={() => {
setIsEditing(false);
setRoom({
name: "",
zulipAutoPost: false,
zulipStream: "",
zulipTopic: "",
});
setRoom(roomInitialState);
setError("");
onOpen();
}}
@@ -236,6 +260,53 @@ export default function RoomsList() {
{error && <Text color="red.500">{error}</Text>}
</FormControl>
<FormControl mt={4}>
<Checkbox
name="isLocked"
isChecked={room.isLocked}
onChange={handleRoomChange}
>
Locked room
</Checkbox>
</FormControl>
<FormControl mt={4}>
<FormLabel>Room size</FormLabel>
<Select
name="roomMode"
options={roomModeOptions}
value={{
label: roomModeOptions.find(
(rm) => rm.value === room.roomMode,
)?.label,
value: room.roomMode,
}}
onChange={(newValue) =>
setRoom({
...room,
roomMode: newValue!.value,
})
}
/>
</FormControl>
<FormControl mt={4}>
<FormLabel>Recording start trigger</FormLabel>
<Select
name="recordingTrigger"
options={recordingTriggerOptions}
value={{
label: recordingTriggerOptions.find(
(rt) => rt.value === room.recordingTrigger,
)?.label,
value: room.recordingTrigger,
}}
onChange={(newValue) =>
setRoom({
...room,
recordingTrigger: newValue!.value,
})
}
/>
</FormControl>
<FormControl mt={8}>
<Checkbox
name="zulipAutoPost"
@@ -309,6 +380,19 @@ export default function RoomsList() {
<Link href={`/${roomData.name}`}>{roomData.name}</Link>
</Heading>
<Spacer />
{linkCopied === roomData.name ? (
<Text mr={2} color="green.500">
Link copied!
</Text>
) : (
<IconButton
aria-label="Copy URL"
icon={<FaLink />}
onClick={() => handleCopyUrl(roomData.name)}
mr={2}
/>
)}
<Menu closeOnSelect={true}>
<MenuButton
as={IconButton}

View File

@@ -71,9 +71,34 @@ export const $CreateRoom = {
type: "string",
title: "Zulip Topic",
},
is_locked: {
type: "boolean",
title: "Is Locked",
},
room_mode: {
type: "string",
title: "Room Mode",
},
recording_type: {
type: "string",
title: "Recording Type",
},
recording_trigger: {
type: "string",
title: "Recording Trigger",
},
},
type: "object",
required: ["name", "zulip_auto_post", "zulip_stream", "zulip_topic"],
required: [
"name",
"zulip_auto_post",
"zulip_stream",
"zulip_topic",
"is_locked",
"room_mode",
"recording_type",
"recording_trigger",
],
title: "CreateRoom",
} as const;
@@ -667,6 +692,22 @@ export const $Room = {
type: "string",
title: "Zulip Topic",
},
is_locked: {
type: "boolean",
title: "Is Locked",
},
room_mode: {
type: "string",
title: "Room Mode",
},
recording_type: {
type: "string",
title: "Recording Type",
},
recording_trigger: {
type: "string",
title: "Recording Trigger",
},
},
type: "object",
required: [
@@ -677,6 +718,10 @@ export const $Room = {
"zulip_auto_post",
"zulip_stream",
"zulip_topic",
"is_locked",
"room_mode",
"recording_type",
"recording_trigger",
],
title: "Room",
} as const;
@@ -857,9 +902,34 @@ export const $UpdateRoom = {
type: "string",
title: "Zulip Topic",
},
is_locked: {
type: "boolean",
title: "Is Locked",
},
room_mode: {
type: "string",
title: "Room Mode",
},
recording_type: {
type: "string",
title: "Recording Type",
},
recording_trigger: {
type: "string",
title: "Recording Trigger",
},
},
type: "object",
required: ["name", "zulip_auto_post", "zulip_stream", "zulip_topic"],
required: [
"name",
"zulip_auto_post",
"zulip_stream",
"zulip_topic",
"is_locked",
"room_mode",
"recording_type",
"recording_trigger",
],
title: "UpdateRoom",
} as const;

View File

@@ -19,6 +19,10 @@ export type CreateRoom = {
zulip_auto_post: boolean;
zulip_stream: string;
zulip_topic: string;
is_locked: boolean;
room_mode: string;
recording_type: string;
recording_trigger: string;
};
export type CreateTranscript = {
@@ -132,6 +136,10 @@ export type Room = {
zulip_auto_post: boolean;
zulip_stream: string;
zulip_topic: string;
is_locked: boolean;
room_mode: string;
recording_type: string;
recording_trigger: string;
};
export type RtcOffer = {
@@ -176,6 +184,10 @@ export type UpdateRoom = {
zulip_auto_post: boolean;
zulip_stream: string;
zulip_topic: string;
is_locked: boolean;
room_mode: string;
recording_type: string;
recording_trigger: string;
};
export type UpdateTranscript = {

View File

@@ -16,7 +16,7 @@ export const viewport: Viewport = {
};
export const metadata: Metadata = {
metadataBase: new URL(process.env.DEV_URL || "https://reflector.media"),
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL!),
title: {
template: "%s Reflector",
default: "Reflector - AI-Powered Meeting Transcriptions by Monadical",

View File

@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
experimental: { esmExternals: "loose" },
};
module.exports = nextConfig;