mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
Merge pull request #394 from Monadical-SAS/zulip-auto-post
Zulip auto post
This commit is contained in:
@@ -7,6 +7,7 @@ database = databases.Database(settings.DATABASE_URL)
|
|||||||
metadata = sqlalchemy.MetaData()
|
metadata = sqlalchemy.MetaData()
|
||||||
|
|
||||||
# import models
|
# import models
|
||||||
|
import reflector.db.meetings # noqa
|
||||||
import reflector.db.rooms # noqa
|
import reflector.db.rooms # noqa
|
||||||
import reflector.db.transcripts # noqa
|
import reflector.db.transcripts # noqa
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ class MeetingController:
|
|||||||
|
|
||||||
return Meeting(**result)
|
return Meeting(**result)
|
||||||
|
|
||||||
|
async def get_by_id(self, meeting_id: str, **kwargs) -> Meeting | None:
|
||||||
|
"""
|
||||||
|
Get a meeting by id
|
||||||
|
"""
|
||||||
|
query = meetings.select().where(meetings.c.id == meeting_id)
|
||||||
|
result = await database.fetch_one(query)
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
return Meeting(**result)
|
||||||
|
|
||||||
async def get_by_id_for_http(self, meeting_id: str, user_id: str | None) -> Meeting:
|
async def get_by_id_for_http(self, meeting_id: str, user_id: str | None) -> Meeting:
|
||||||
"""
|
"""
|
||||||
Get a meeting by ID for HTTP request.
|
Get a meeting by ID for HTTP request.
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ class RoomController:
|
|||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
zulip_auto_post: bool,
|
||||||
|
zulip_stream: str,
|
||||||
|
zulip_topic: str,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add a new room
|
Add a new room
|
||||||
@@ -75,11 +78,24 @@ class RoomController:
|
|||||||
room = Room(
|
room = Room(
|
||||||
name=name,
|
name=name,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
zulip_auto_post=zulip_auto_post,
|
||||||
|
zulip_stream=zulip_stream,
|
||||||
|
zulip_topic=zulip_topic,
|
||||||
)
|
)
|
||||||
query = rooms.insert().values(**room.model_dump())
|
query = rooms.insert().values(**room.model_dump())
|
||||||
await database.execute(query)
|
await database.execute(query)
|
||||||
return room
|
return room
|
||||||
|
|
||||||
|
async def update(self, room: Room, values: dict, mutate=True):
|
||||||
|
"""
|
||||||
|
Update a room fields with key/values in values
|
||||||
|
"""
|
||||||
|
query = rooms.update().where(rooms.c.id == room.id).values(**values)
|
||||||
|
await database.execute(query)
|
||||||
|
if mutate:
|
||||||
|
for key, value in values.items():
|
||||||
|
setattr(room, key, value)
|
||||||
|
|
||||||
async def get_by_id(self, room_id: str, **kwargs) -> Room | None:
|
async def get_by_id(self, room_id: str, **kwargs) -> Room | None:
|
||||||
"""
|
"""
|
||||||
Get a room by id
|
Get a room by id
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from contextlib import asynccontextmanager
|
|||||||
|
|
||||||
from celery import chord, group, shared_task
|
from celery import chord, group, shared_task
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from reflector.db.meetings import meetings_controller
|
||||||
|
from reflector.db.rooms import rooms_controller
|
||||||
from reflector.db.transcripts import (
|
from reflector.db.transcripts import (
|
||||||
Transcript,
|
Transcript,
|
||||||
TranscriptDuration,
|
TranscriptDuration,
|
||||||
@@ -53,6 +55,7 @@ from reflector.processors.types import (
|
|||||||
from reflector.processors.types import Transcript as TranscriptProcessorType
|
from reflector.processors.types import Transcript as TranscriptProcessorType
|
||||||
from reflector.settings import settings
|
from reflector.settings import settings
|
||||||
from reflector.ws_manager import WebsocketManager, get_ws_manager
|
from reflector.ws_manager import WebsocketManager, get_ws_manager
|
||||||
|
from reflector.zulip import get_zulip_message, send_message_to_zulip
|
||||||
from structlog import BoundLogger as Logger
|
from structlog import BoundLogger as Logger
|
||||||
|
|
||||||
|
|
||||||
@@ -564,6 +567,31 @@ async def pipeline_summaries(transcript: Transcript, logger: Logger):
|
|||||||
logger.info("Summaries done")
|
logger.info("Summaries done")
|
||||||
|
|
||||||
|
|
||||||
|
@get_transcript
|
||||||
|
async def pipeline_post_to_zulip(transcript: Transcript, logger: Logger):
|
||||||
|
logger.info("Starting post to zulip")
|
||||||
|
|
||||||
|
if not transcript.meeting_id:
|
||||||
|
logger.info("Transcript has no meeting")
|
||||||
|
return
|
||||||
|
|
||||||
|
meeting = await meetings_controller.get_by_id(transcript.meeting_id)
|
||||||
|
if not meeting:
|
||||||
|
logger.info("No meeting found for this transcript")
|
||||||
|
return
|
||||||
|
|
||||||
|
room = await rooms_controller.get_by_id(meeting.room_id)
|
||||||
|
if not room:
|
||||||
|
logger.error(f"Missing room for a meeting {meeting.id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if room.zulip_auto_post:
|
||||||
|
message = get_zulip_message(transcript=transcript)
|
||||||
|
send_message_to_zulip(room.zulip_stream, room.zulip_topic, message)
|
||||||
|
|
||||||
|
logger.info("Posted to zulip")
|
||||||
|
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# Celery tasks that can be called from the API
|
# Celery tasks that can be called from the API
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
@@ -611,6 +639,12 @@ async def task_pipeline_final_summaries(*, transcript_id: str):
|
|||||||
await pipeline_summaries(transcript_id=transcript_id)
|
await pipeline_summaries(transcript_id=transcript_id)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
@asynctask
|
||||||
|
async def task_pipeline_post_to_zulip(*, transcript_id: str):
|
||||||
|
await pipeline_post_to_zulip(transcript_id=transcript_id)
|
||||||
|
|
||||||
|
|
||||||
def pipeline_post(*, transcript_id: str):
|
def pipeline_post(*, transcript_id: str):
|
||||||
"""
|
"""
|
||||||
Run the post pipeline
|
Run the post pipeline
|
||||||
@@ -632,7 +666,8 @@ def pipeline_post(*, transcript_id: str):
|
|||||||
chain = chord(
|
chain = chord(
|
||||||
group(chain_mp3_and_diarize, chain_title_preview),
|
group(chain_mp3_and_diarize, chain_title_preview),
|
||||||
chain_final_summaries,
|
chain_final_summaries,
|
||||||
)
|
) | task_pipeline_post_to_zulip.si(transcript_id=transcript_id)
|
||||||
|
|
||||||
chain.delay()
|
chain.delay()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -141,5 +141,9 @@ class Settings(BaseSettings):
|
|||||||
AWS_WHEREBY_ACCESS_KEY_ID: str | None = None
|
AWS_WHEREBY_ACCESS_KEY_ID: str | None = None
|
||||||
AWS_WHEREBY_ACCESS_KEY_SECRET: str | None = None
|
AWS_WHEREBY_ACCESS_KEY_SECRET: str | None = None
|
||||||
|
|
||||||
|
ZULIP_REALM: str | None = None
|
||||||
|
ZULIP_API_KEY: str | None = None
|
||||||
|
ZULIP_BOT_EMAIL: str | None = None
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ class Room(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
user_id: str
|
user_id: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
zulip_auto_post: bool
|
||||||
|
zulip_stream: str
|
||||||
|
zulip_topic: str
|
||||||
|
|
||||||
|
|
||||||
class Meeting(BaseModel):
|
class Meeting(BaseModel):
|
||||||
@@ -35,6 +38,16 @@ class Meeting(BaseModel):
|
|||||||
|
|
||||||
class CreateRoom(BaseModel):
|
class CreateRoom(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
zulip_auto_post: bool
|
||||||
|
zulip_stream: str
|
||||||
|
zulip_topic: str
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateRoom(BaseModel):
|
||||||
|
name: str
|
||||||
|
zulip_auto_post: bool
|
||||||
|
zulip_stream: str
|
||||||
|
zulip_topic: str
|
||||||
|
|
||||||
|
|
||||||
class DeletionStatus(BaseModel):
|
class DeletionStatus(BaseModel):
|
||||||
@@ -69,9 +82,27 @@ async def rooms_create(
|
|||||||
return await rooms_controller.add(
|
return await rooms_controller.add(
|
||||||
name=room.name,
|
name=room.name,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
zulip_auto_post=room.zulip_auto_post,
|
||||||
|
zulip_stream=room.zulip_stream,
|
||||||
|
zulip_topic=room.zulip_topic,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/rooms/{room_id}", response_model=Room)
|
||||||
|
async def rooms_update(
|
||||||
|
room_id: str,
|
||||||
|
info: UpdateRoom,
|
||||||
|
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
||||||
|
):
|
||||||
|
user_id = user["sub"] if user else None
|
||||||
|
room = await rooms_controller.get_by_id_for_http(room_id, user_id=user_id)
|
||||||
|
if not room:
|
||||||
|
raise HTTPException(status_code=404, detail="Room not found")
|
||||||
|
values = info.dict(exclude_unset=True)
|
||||||
|
await rooms_controller.update(room, values)
|
||||||
|
return room
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/rooms/{room_id}", response_model=DeletionStatus)
|
@router.delete("/rooms/{room_id}", response_model=DeletionStatus)
|
||||||
async def rooms_delete(
|
async def rooms_delete(
|
||||||
room_id: str,
|
room_id: str,
|
||||||
|
|||||||
72
server/reflector/zulip.py
Normal file
72
server/reflector/zulip.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from reflector.db.transcripts import Transcript
|
||||||
|
from reflector.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
def send_message_to_zulip(stream: str, topic: str, message: str):
|
||||||
|
if not stream or not topic or not message:
|
||||||
|
raise ValueError("Missing required parameters")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"https://{settings.ZULIP_REALM}/api/v1/messages",
|
||||||
|
data={
|
||||||
|
"type": "stream",
|
||||||
|
"to": stream,
|
||||||
|
"topic": topic,
|
||||||
|
"content": message,
|
||||||
|
},
|
||||||
|
auth=(settings.ZULIP_BOT_EMAIL, settings.ZULIP_API_KEY),
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
except requests.RequestException as error:
|
||||||
|
raise Exception(f"Failed to send message to Zulip: {error}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_zulip_message(transcript: Transcript):
|
||||||
|
domain = (
|
||||||
|
"http://localhost:3000" # Replace this with your deployment base URL if needed
|
||||||
|
)
|
||||||
|
transcript_url = f"{domain}/transcripts/{transcript.id}"
|
||||||
|
|
||||||
|
header_text = f"# Reflector – {transcript.title or 'Unnamed recording'}\n\n"
|
||||||
|
header_text += f"**Date**: <time:{transcript.created_at.isoformat()}>\n"
|
||||||
|
header_text += f"**Link**: [{extract_domain(transcript_url)}]({transcript_url})\n"
|
||||||
|
header_text += f"**Duration**: {format_time_ms(transcript.duration)}\n\n"
|
||||||
|
|
||||||
|
topic_text = ""
|
||||||
|
|
||||||
|
if transcript.topics:
|
||||||
|
topic_text = "```spoiler Topics\n"
|
||||||
|
for topic in transcript.topics:
|
||||||
|
topic_text += f"1. [{format_time(topic.timestamp)}] {topic.title}\n"
|
||||||
|
topic_text += "```\n\n"
|
||||||
|
|
||||||
|
summary = "```spoiler Summary\n"
|
||||||
|
summary += transcript.long_summary
|
||||||
|
summary += "```\n\n"
|
||||||
|
|
||||||
|
message = header_text + summary + topic_text + "-----\n"
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def extract_domain(url: str) -> str:
|
||||||
|
return urlparse(url).netloc
|
||||||
|
|
||||||
|
|
||||||
|
def format_time_ms(milliseconds: float) -> str:
|
||||||
|
return format_time(milliseconds // 1000)
|
||||||
|
|
||||||
|
|
||||||
|
def format_time(seconds: float) -> str:
|
||||||
|
td = timedelta(seconds=seconds)
|
||||||
|
time = str(td - timedelta(microseconds=td.microseconds))
|
||||||
|
|
||||||
|
return time[2:] if time.startswith("0:") else time
|
||||||
5
www/.gitignore
vendored
5
www/.gitignore
vendored
@@ -40,4 +40,7 @@ next-env.d.ts
|
|||||||
# Sentry Auth Token
|
# Sentry Auth Token
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
|
||||||
config.ts
|
config.ts
|
||||||
|
|
||||||
|
# openapi logs
|
||||||
|
openapi-ts-error-*.log
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
@@ -9,7 +8,6 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
FormHelperText,
|
FormHelperText,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Grid,
|
|
||||||
Heading,
|
Heading,
|
||||||
Input,
|
Input,
|
||||||
Link,
|
Link,
|
||||||
@@ -29,47 +27,145 @@ import {
|
|||||||
MenuButton,
|
MenuButton,
|
||||||
MenuList,
|
MenuList,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
AlertDialog,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Checkbox,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import NextLink from "next";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import React, { ReactNode, useState } from "react";
|
|
||||||
import { Container } from "@chakra-ui/react";
|
import { Container } from "@chakra-ui/react";
|
||||||
import { PlusSquareIcon } from "@chakra-ui/icons";
|
import { FaEllipsisVertical, FaTrash, FaPencil } from "react-icons/fa6";
|
||||||
import useApi from "../../lib/useApi";
|
import useApi from "../../lib/useApi";
|
||||||
import useRoomList from "./useRoomList";
|
import useRoomList from "./useRoomList";
|
||||||
import { FaEllipsisVertical, FaTrash } from "react-icons/fa6";
|
import { DomainContext } from "../domainContext";
|
||||||
import next from "next";
|
import { Select, Options, OptionBase } from "chakra-react-select";
|
||||||
|
|
||||||
|
interface Stream {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
topics: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectOption extends OptionBase {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function RoomsList() {
|
export default function RoomsList() {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const [roomName, setRoomName] = useState("");
|
const [room, setRoom] = useState({
|
||||||
|
name: "",
|
||||||
|
zulipAutoPost: false,
|
||||||
|
zulipStream: "",
|
||||||
|
zulipTopic: "",
|
||||||
|
});
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editRoomId, setEditRoomId] = useState("");
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
const { loading, response, refetch } = useRoomList(page);
|
const { loading, response, refetch } = useRoomList(page);
|
||||||
|
const [streams, setStreams] = useState<Stream[]>([]);
|
||||||
|
|
||||||
const handleAddRoom = async () => {
|
const { zulip_streams } = useContext(DomainContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchZulipStreams = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(zulip_streams + "/streams.json");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
let data = await response.json();
|
||||||
|
data = data.sort((a: Stream, b: Stream) =>
|
||||||
|
a.name.localeCompare(b.name),
|
||||||
|
);
|
||||||
|
setStreams(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching streams:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (room.zulipAutoPost) {
|
||||||
|
fetchZulipStreams();
|
||||||
|
}
|
||||||
|
}, [room.zulipAutoPost]);
|
||||||
|
|
||||||
|
const streamOptions: Options<SelectOption> = streams.map((stream) => {
|
||||||
|
return { label: stream.name, value: stream.name };
|
||||||
|
});
|
||||||
|
|
||||||
|
const topicOptions =
|
||||||
|
streams
|
||||||
|
.find((stream) => stream.name === room.zulipStream)
|
||||||
|
?.topics.map((topic) => ({ label: topic, value: topic })) || [];
|
||||||
|
|
||||||
|
const handleSaveRoom = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api?.v1RoomsCreate({
|
console.log(room);
|
||||||
requestBody: { name: roomName },
|
if (isEditing) {
|
||||||
|
await api?.v1RoomsUpdate({
|
||||||
|
roomId: editRoomId,
|
||||||
|
requestBody: {
|
||||||
|
name: room.name,
|
||||||
|
zulip_auto_post: room.zulipAutoPost,
|
||||||
|
zulip_stream: room.zulipStream,
|
||||||
|
zulip_topic: room.zulipTopic,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await api?.v1RoomsCreate({
|
||||||
|
requestBody: {
|
||||||
|
name: room.name,
|
||||||
|
zulip_auto_post: room.zulipAutoPost,
|
||||||
|
zulip_stream: room.zulipStream,
|
||||||
|
zulip_topic: room.zulipTopic,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setRoom({
|
||||||
|
name: "",
|
||||||
|
zulipAutoPost: false,
|
||||||
|
zulipStream: "",
|
||||||
|
zulipTopic: "",
|
||||||
});
|
});
|
||||||
setRoomName("");
|
setIsEditing(false);
|
||||||
|
setEditRoomId("");
|
||||||
refetch();
|
refetch();
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditRoom = (roomId, roomData) => {
|
||||||
|
setRoom({
|
||||||
|
name: roomData.name,
|
||||||
|
zulipAutoPost: roomData.zulip_auto_post,
|
||||||
|
zulipStream: roomData.zulip_stream,
|
||||||
|
zulipTopic: roomData.zulip_topic,
|
||||||
|
});
|
||||||
|
setEditRoomId(roomId);
|
||||||
|
setIsEditing(true);
|
||||||
|
onOpen();
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteRoom = async (roomId: string) => {
|
const handleDeleteRoom = async (roomId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await api?.v1RoomsDelete({
|
await api?.v1RoomsDelete({
|
||||||
roomId,
|
roomId,
|
||||||
});
|
});
|
||||||
refetch();
|
refetch();
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRoomNameChange = (e) => {
|
const handleRoomChange = (e) => {
|
||||||
setRoomName(e.target.value);
|
let { name, value, type, checked } = e.target;
|
||||||
|
if (name === "name") {
|
||||||
|
value = value
|
||||||
|
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
setRoom({
|
||||||
|
...room,
|
||||||
|
[name]: type === "checkbox" ? checked : value,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading && !response)
|
if (loading && !response)
|
||||||
@@ -91,23 +187,81 @@ export default function RoomsList() {
|
|||||||
>
|
>
|
||||||
<Heading>Rooms</Heading>
|
<Heading>Rooms</Heading>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Button colorScheme="blue" onClick={onOpen}>
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setRoom({
|
||||||
|
name: "",
|
||||||
|
zulipAutoPost: false,
|
||||||
|
zulipStream: "",
|
||||||
|
zulipTopic: "",
|
||||||
|
});
|
||||||
|
onOpen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
Add Room
|
Add Room
|
||||||
</Button>
|
</Button>
|
||||||
<Modal isOpen={isOpen} onClose={onClose}>
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Add Room</ModalHeader>
|
<ModalHeader>{isEditing ? "Edit Room" : "Add Room"}</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Room name</FormLabel>
|
<FormLabel>Room name</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
|
name="name"
|
||||||
placeholder="room-name"
|
placeholder="room-name"
|
||||||
value={roomName}
|
value={room.name}
|
||||||
onChange={handleRoomNameChange}
|
onChange={handleRoomChange}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
No spaces or special characters allowed
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl mt={8}>
|
||||||
|
<Checkbox
|
||||||
|
name="zulipAutoPost"
|
||||||
|
isChecked={room.zulipAutoPost}
|
||||||
|
onChange={handleRoomChange}
|
||||||
|
>
|
||||||
|
Automatically post transcription to Zulip
|
||||||
|
</Checkbox>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl mt={4}>
|
||||||
|
<FormLabel>Zulip stream</FormLabel>
|
||||||
|
<Select
|
||||||
|
name="zulipStream"
|
||||||
|
options={streamOptions}
|
||||||
|
placeholder="Select stream"
|
||||||
|
value={{ label: room.zulipStream, value: room.zulipStream }}
|
||||||
|
onChange={(newValue) =>
|
||||||
|
setRoom({
|
||||||
|
...room,
|
||||||
|
zulipStream: newValue!.value,
|
||||||
|
zulipTopic: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={!room.zulipAutoPost}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl mt={4}>
|
||||||
|
<FormLabel>Zulip topic</FormLabel>
|
||||||
|
<Select
|
||||||
|
name="zulipTopic"
|
||||||
|
options={topicOptions}
|
||||||
|
placeholder="Select topic"
|
||||||
|
value={{ label: room.zulipTopic, value: room.zulipTopic }}
|
||||||
|
onChange={(newValue) =>
|
||||||
|
setRoom({
|
||||||
|
...room,
|
||||||
|
zulipTopic: newValue!.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={!room.zulipAutoPost}
|
||||||
/>
|
/>
|
||||||
<FormHelperText>Please enter room name</FormHelperText>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
@@ -116,8 +270,14 @@ export default function RoomsList() {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button colorScheme="blue" onClick={handleAddRoom}>
|
<Button
|
||||||
Add
|
colorScheme="blue"
|
||||||
|
onClick={handleSaveRoom}
|
||||||
|
isDisabled={
|
||||||
|
!room.name || (room.zulipAutoPost && !room.zulipTopic)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isEditing ? "Save" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
@@ -126,17 +286,13 @@ export default function RoomsList() {
|
|||||||
|
|
||||||
<VStack>
|
<VStack>
|
||||||
{response?.items && response.items.length > 0 ? (
|
{response?.items && response.items.length > 0 ? (
|
||||||
response.items.map((room) => (
|
response.items.map((roomData) => (
|
||||||
<Card w={"full"}>
|
<Card w={"full"} key={roomData.id}>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Flex align={"center"}>
|
<Flex align={"center"}>
|
||||||
<Heading size="md">
|
<Heading size="md">
|
||||||
<Link
|
<Link href={`/rooms/${roomData.name}`}>
|
||||||
// as={NextLink}
|
{roomData.name}
|
||||||
href={`/rooms/${room.name}`}
|
|
||||||
noOfLines={2}
|
|
||||||
>
|
|
||||||
{room.name}
|
|
||||||
</Link>
|
</Link>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
@@ -148,7 +304,13 @@ export default function RoomsList() {
|
|||||||
/>
|
/>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => handleDeleteRoom(room.id)}
|
onClick={() => handleEditRoom(roomData.id, roomData)}
|
||||||
|
icon={<FaPencil />}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => handleDeleteRoom(roomData.id)}
|
||||||
icon={<FaTrash color={"red.500"} />}
|
icon={<FaTrash color={"red.500"} />}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
|
|||||||
@@ -59,9 +59,21 @@ export const $CreateRoom = {
|
|||||||
type: "string",
|
type: "string",
|
||||||
title: "Name",
|
title: "Name",
|
||||||
},
|
},
|
||||||
|
zulip_auto_post: {
|
||||||
|
type: "boolean",
|
||||||
|
title: "Zulip Auto Post",
|
||||||
|
},
|
||||||
|
zulip_stream: {
|
||||||
|
type: "string",
|
||||||
|
title: "Zulip Stream",
|
||||||
|
},
|
||||||
|
zulip_topic: {
|
||||||
|
type: "string",
|
||||||
|
title: "Zulip Topic",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["name"],
|
required: ["name", "zulip_auto_post", "zulip_stream", "zulip_topic"],
|
||||||
title: "CreateRoom",
|
title: "CreateRoom",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -643,9 +655,29 @@ export const $Room = {
|
|||||||
format: "date-time",
|
format: "date-time",
|
||||||
title: "Created At",
|
title: "Created At",
|
||||||
},
|
},
|
||||||
|
zulip_auto_post: {
|
||||||
|
type: "boolean",
|
||||||
|
title: "Zulip Auto Post",
|
||||||
|
},
|
||||||
|
zulip_stream: {
|
||||||
|
type: "string",
|
||||||
|
title: "Zulip Stream",
|
||||||
|
},
|
||||||
|
zulip_topic: {
|
||||||
|
type: "string",
|
||||||
|
title: "Zulip Topic",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["id", "name", "user_id", "created_at"],
|
required: [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"user_id",
|
||||||
|
"created_at",
|
||||||
|
"zulip_auto_post",
|
||||||
|
"zulip_stream",
|
||||||
|
"zulip_topic",
|
||||||
|
],
|
||||||
title: "Room",
|
title: "Room",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -807,6 +839,30 @@ export const $UpdateParticipant = {
|
|||||||
title: "UpdateParticipant",
|
title: "UpdateParticipant",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const $UpdateRoom = {
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
title: "Name",
|
||||||
|
},
|
||||||
|
zulip_auto_post: {
|
||||||
|
type: "boolean",
|
||||||
|
title: "Zulip Auto Post",
|
||||||
|
},
|
||||||
|
zulip_stream: {
|
||||||
|
type: "string",
|
||||||
|
title: "Zulip Stream",
|
||||||
|
},
|
||||||
|
zulip_topic: {
|
||||||
|
type: "string",
|
||||||
|
title: "Zulip Topic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "object",
|
||||||
|
required: ["name", "zulip_auto_post", "zulip_stream", "zulip_topic"],
|
||||||
|
title: "UpdateRoom",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const $UpdateTranscript = {
|
export const $UpdateTranscript = {
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
name: {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type {
|
|||||||
V1RoomsListResponse,
|
V1RoomsListResponse,
|
||||||
V1RoomsCreateData,
|
V1RoomsCreateData,
|
||||||
V1RoomsCreateResponse,
|
V1RoomsCreateResponse,
|
||||||
|
V1RoomsUpdateData,
|
||||||
|
V1RoomsUpdateResponse,
|
||||||
V1RoomsDeleteData,
|
V1RoomsDeleteData,
|
||||||
V1RoomsDeleteResponse,
|
V1RoomsDeleteResponse,
|
||||||
V1RoomsCreateMeetingData,
|
V1RoomsCreateMeetingData,
|
||||||
@@ -120,6 +122,31 @@ export class DefaultService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rooms Update
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.roomId
|
||||||
|
* @param data.requestBody
|
||||||
|
* @returns Room Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public v1RoomsUpdate(
|
||||||
|
data: V1RoomsUpdateData,
|
||||||
|
): CancelablePromise<V1RoomsUpdateResponse> {
|
||||||
|
return this.httpRequest.request({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/v1/rooms/{room_id}",
|
||||||
|
path: {
|
||||||
|
room_id: data.roomId,
|
||||||
|
},
|
||||||
|
body: data.requestBody,
|
||||||
|
mediaType: "application/json",
|
||||||
|
errors: {
|
||||||
|
422: "Validation Error",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rooms Delete
|
* Rooms Delete
|
||||||
* @param data The data for the request.
|
* @param data The data for the request.
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export type CreateParticipant = {
|
|||||||
|
|
||||||
export type CreateRoom = {
|
export type CreateRoom = {
|
||||||
name: string;
|
name: string;
|
||||||
|
zulip_auto_post: boolean;
|
||||||
|
zulip_stream: string;
|
||||||
|
zulip_topic: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateTranscript = {
|
export type CreateTranscript = {
|
||||||
@@ -126,6 +129,9 @@ export type Room = {
|
|||||||
name: string;
|
name: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
zulip_auto_post: boolean;
|
||||||
|
zulip_stream: string;
|
||||||
|
zulip_topic: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RtcOffer = {
|
export type RtcOffer = {
|
||||||
@@ -165,6 +171,13 @@ export type UpdateParticipant = {
|
|||||||
name?: string | null;
|
name?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateRoom = {
|
||||||
|
name: string;
|
||||||
|
zulip_auto_post: boolean;
|
||||||
|
zulip_stream: string;
|
||||||
|
zulip_topic: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateTranscript = {
|
export type UpdateTranscript = {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
locked?: boolean | null;
|
locked?: boolean | null;
|
||||||
@@ -216,6 +229,13 @@ export type V1RoomsCreateData = {
|
|||||||
|
|
||||||
export type V1RoomsCreateResponse = Room;
|
export type V1RoomsCreateResponse = Room;
|
||||||
|
|
||||||
|
export type V1RoomsUpdateData = {
|
||||||
|
requestBody: UpdateRoom;
|
||||||
|
roomId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type V1RoomsUpdateResponse = Room;
|
||||||
|
|
||||||
export type V1RoomsDeleteData = {
|
export type V1RoomsDeleteData = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
};
|
};
|
||||||
@@ -426,6 +446,19 @@ export type $OpenApiTs = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
"/v1/rooms/{room_id}": {
|
"/v1/rooms/{room_id}": {
|
||||||
|
patch: {
|
||||||
|
req: V1RoomsUpdateData;
|
||||||
|
res: {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: Room;
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HTTPValidationError;
|
||||||
|
};
|
||||||
|
};
|
||||||
delete: {
|
delete: {
|
||||||
req: V1RoomsDeleteData;
|
req: V1RoomsDeleteData;
|
||||||
res: {
|
res: {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"@whereby.com/browser-sdk": "^3.3.4",
|
"@whereby.com/browser-sdk": "^3.3.4",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"chakra-react-select": "^4.7.6",
|
"chakra-react-select": "^4.9.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-next": "^14.0.4",
|
"eslint-config-next": "^14.0.4",
|
||||||
"fontawesome": "^5.6.3",
|
"fontawesome": "^5.6.3",
|
||||||
|
|||||||
@@ -2347,12 +2347,12 @@ caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.300015
|
|||||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz"
|
||||||
integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==
|
integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==
|
||||||
|
|
||||||
chakra-react-select@^4.7.6:
|
chakra-react-select@^4.9.1:
|
||||||
version "4.7.6"
|
version "4.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/chakra-react-select/-/chakra-react-select-4.7.6.tgz#3be8ffb314b8e75ef02663fd3e2fdf872b79683b"
|
resolved "https://registry.yarnpkg.com/chakra-react-select/-/chakra-react-select-4.9.1.tgz#38e421a0400c26e7f25d3dd28e6b93a021f08b77"
|
||||||
integrity sha512-ZL43hyXPnWf1g/HjsZDecbeJ4F2Q6tTPYJozlKWkrQ7lIX7ORP0aZYwmc5/Wly4UNzMimj2Vuosl6MmIXH+G2g==
|
integrity sha512-jmgfN+S/wnTaCp3pW30GYDIZ5J8jWcT1gIbhpw6RdKV+atm/U4/sT+gaHOHHhRL8xeaYip+iI/m8MPGREkve0w==
|
||||||
dependencies:
|
dependencies:
|
||||||
react-select "5.7.7"
|
react-select "5.8.0"
|
||||||
|
|
||||||
chalk@3.0.0:
|
chalk@3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
@@ -5116,10 +5116,10 @@ react-select-search@^4.1.7:
|
|||||||
resolved "https://registry.yarnpkg.com/react-select-search/-/react-select-search-4.1.7.tgz#5662729b9052282bde52e1352006d495d9c5ed6e"
|
resolved "https://registry.yarnpkg.com/react-select-search/-/react-select-search-4.1.7.tgz#5662729b9052282bde52e1352006d495d9c5ed6e"
|
||||||
integrity sha512-pU7ONAdK+bmz2tbhBWYQv9m5mnXOn8yImuiy+5UhimIG80d5iKv3nSYJIjJWjDbdrrdoXiCRwQm8xbA8llTjmQ==
|
integrity sha512-pU7ONAdK+bmz2tbhBWYQv9m5mnXOn8yImuiy+5UhimIG80d5iKv3nSYJIjJWjDbdrrdoXiCRwQm8xbA8llTjmQ==
|
||||||
|
|
||||||
react-select@5.7.7:
|
react-select@5.8.0:
|
||||||
version "5.7.7"
|
version "5.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.7.tgz#dbade9dbf711ef2a181970c10f8ab319ac37fbd0"
|
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.8.0.tgz#bd5c467a4df223f079dd720be9498076a3f085b5"
|
||||||
integrity sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==
|
integrity sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.0"
|
"@babel/runtime" "^7.12.0"
|
||||||
"@emotion/cache" "^11.4.0"
|
"@emotion/cache" "^11.4.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user