Replace streams json

This commit is contained in:
2024-09-06 17:26:32 +02:00
parent 5267ab2d37
commit 901de8c009
7 changed files with 247 additions and 53 deletions

View File

@@ -24,6 +24,7 @@ from reflector.views.transcripts_upload import router as transcripts_upload_rout
from reflector.views.transcripts_webrtc import router as transcripts_webrtc_router
from reflector.views.transcripts_websocket import router as transcripts_websocket_router
from reflector.views.user import router as user_router
from reflector.views.zulip import router as zulip_router
try:
import sentry_sdk
@@ -79,6 +80,7 @@ app.include_router(transcripts_websocket_router, prefix="/v1")
app.include_router(transcripts_webrtc_router, prefix="/v1")
app.include_router(transcripts_process_router, prefix="/v1")
app.include_router(user_router, prefix="/v1")
app.include_router(zulip_router, prefix="/v1")
add_pagination(app)
# prepare celery

View File

@@ -0,0 +1,46 @@
from typing import Annotated, Optional
import reflector.auth as auth
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from reflector.zulip import get_zulip_streams, get_zulip_topics
router = APIRouter()
class Stream(BaseModel):
stream_id: int
name: str
class Topic(BaseModel):
name: str
@router.get("/zulip/streams")
async def zulip_get_streams(
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
) -> list[Stream]:
"""
Get all Zulip streams.
"""
if not user:
raise HTTPException(status_code=403, detail="Authentication required")
streams = get_zulip_streams()
return streams
@router.get("/zulip/streams/{stream_id}/topics")
async def zulip_get_topics(
stream_id: int,
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
) -> list[Topic]:
"""
Get all topics for a specific Zulip stream.
"""
if not user:
raise HTTPException(status_code=403, detail="Authentication required")
topics = get_zulip_topics(stream_id)
return topics

View File

@@ -10,6 +10,34 @@ class InvalidMessageError(Exception):
pass
def get_zulip_topics(stream_id: int) -> list[dict]:
try:
response = requests.get(
f"https://{settings.ZULIP_REALM}/api/v1/users/me/{stream_id}/topics",
auth=(settings.ZULIP_BOT_EMAIL, settings.ZULIP_API_KEY),
)
response.raise_for_status()
return response.json().get("topics", [])
except requests.RequestException as error:
raise Exception(f"Failed to get topics: {error}")
def get_zulip_streams() -> list[dict]:
try:
response = requests.get(
f"https://{settings.ZULIP_REALM}/api/v1/streams",
auth=(settings.ZULIP_BOT_EMAIL, settings.ZULIP_API_KEY),
)
response.raise_for_status()
return response.json().get("streams", [])
except requests.RequestException as error:
raise Exception(f"Failed to get streams: {error}")
def send_message_to_zulip(stream: str, topic: str, content: str):
try:
response = requests.post(

View File

@@ -1,11 +1,11 @@
import React, { useContext, useState, useEffect, useCallback } from "react";
import React, { useContext, useState, useEffect } from "react";
import SelectSearch from "react-select-search";
import { GetTranscript, GetTranscriptTopic } from "../../../api";
import "react-select-search/style.css";
import { DomainContext } from "../../../domainContext";
import useApi from "../../../lib/useApi";
type ShareModal = {
type ShareModalProps = {
show: boolean;
setShow: (show: boolean) => void;
transcript: GetTranscript | null;
@@ -13,9 +13,12 @@ type ShareModal = {
};
interface Stream {
id: number;
stream_id: number;
name: string;
}
interface Topic {
name: string;
topics: string[];
}
interface SelectSearchOption {
@@ -23,41 +26,54 @@ interface SelectSearchOption {
value: string;
}
const ShareModal = (props: ShareModal) => {
const ShareModal = (props: ShareModalProps) => {
const [stream, setStream] = useState<string | undefined>(undefined);
const [topic, setTopic] = useState<string | undefined>(undefined);
const [includeTopics, setIncludeTopics] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [streams, setStreams] = useState<Stream[]>([]);
const { zulip_streams } = useContext(DomainContext);
const [topics, setTopics] = useState<Topic[]>([]);
const api = useApi();
useEffect(() => {
fetch(zulip_streams + "/streams.json")
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then((data) => {
data = data.sort((a: Stream, b: Stream) =>
a.name.localeCompare(b.name),
);
setStreams(data);
const fetchZulipStreams = async () => {
if (!api) return;
try {
const response = await api.v1ZulipGetStreams();
setStreams(response);
setIsLoading(false);
// data now contains the JavaScript object decoded from JSON
})
.catch((error) => {
console.error("There was a problem with your fetch operation:", error);
} catch (error) {
console.error("Error fetching Zulip streams:", error);
}
};
fetchZulipStreams();
}, [!api]);
useEffect(() => {
const fetchZulipTopics = async () => {
if (!api || !stream) return;
try {
const selectedStream = streams.find((s) => s.name === stream);
if (selectedStream) {
const response = await api.v1ZulipGetTopics({
streamId: selectedStream.stream_id,
});
}, []);
setTopics(response);
}
} catch (error) {
console.error("Error fetching Zulip topics:", error);
}
};
fetchZulipTopics();
}, [stream, streams, api]);
const handleSendToZulip = async () => {
if (!props.transcript) return;
if (!api || !props.transcript) return;
if (stream && topic) {
if (!api) return;
try {
await api.v1TranscriptPostToZulip({
transcriptId: props.transcript.id,
@@ -75,13 +91,15 @@ const ShareModal = (props: ShareModal) => {
return <div>Loading...</div>;
}
let streamOptions: SelectSearchOption[] = [];
if (streams) {
streams.forEach((stream) => {
const value = stream.name;
streamOptions.push({ name: value, value: value });
});
}
const streamOptions: SelectSearchOption[] = streams.map((stream) => ({
name: stream.name,
value: stream.name,
}));
const topicOptions: SelectSearchOption[] = topics.map((topic) => ({
name: topic.name,
value: topic.name,
}));
return (
<div className="absolute">
@@ -111,7 +129,7 @@ const ShareModal = (props: ShareModal) => {
options={streamOptions}
value={stream}
onChange={(val) => {
setTopic(undefined);
setTopic(undefined); // Reset topic when stream changes
setStream(val.toString());
}}
placeholder="Pick a stream"
@@ -119,25 +137,16 @@ const ShareModal = (props: ShareModal) => {
</div>
{stream && (
<>
<div className="flex items-center mt-4">
<span className="mr-2 invisible">#</span>
<SelectSearch
search={true}
options={
streams
.find((s) => s.name == stream)
?.topics.sort((a: string, b: string) =>
a.localeCompare(b),
)
.map((t) => ({ name: t, value: t })) || []
}
options={topicOptions}
value={topic}
onChange={(val) => setTopic(val.toString())}
placeholder="Pick a topic"
/>
</div>
</>
)}
<button

View File

@@ -828,6 +828,34 @@ export const $SpeakerWords = {
title: "SpeakerWords",
} as const;
export const $Stream = {
properties: {
stream_id: {
type: "integer",
title: "Stream Id",
},
name: {
type: "string",
title: "Name",
},
},
type: "object",
required: ["stream_id", "name"],
title: "Stream",
} as const;
export const $Topic = {
properties: {
name: {
type: "string",
title: "Name",
},
},
type: "object",
required: ["name"],
title: "Topic",
} as const;
export const $TranscriptParticipant = {
properties: {
id: {

View File

@@ -61,6 +61,9 @@ import type {
V1TranscriptProcessData,
V1TranscriptProcessResponse,
V1UserMeResponse,
V1ZulipGetStreamsResponse,
V1ZulipGetTopicsData,
V1ZulipGetTopicsResponse,
} from "./types.gen";
export class DefaultService {
@@ -762,4 +765,40 @@ export class DefaultService {
url: "/v1/me",
});
}
/**
* Zulip Get Streams
* Get all Zulip streams.
* @returns Stream Successful Response
* @throws ApiError
*/
public v1ZulipGetStreams(): CancelablePromise<V1ZulipGetStreamsResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/zulip/streams",
});
}
/**
* Zulip Get Topics
* Get all topics for a specific Zulip stream.
* @param data The data for the request.
* @param data.streamId
* @returns Topic Successful Response
* @throws ApiError
*/
public v1ZulipGetTopics(
data: V1ZulipGetTopicsData,
): CancelablePromise<V1ZulipGetTopicsResponse> {
return this.httpRequest.request({
method: "GET",
url: "/v1/zulip/streams/{stream_id}/topics",
path: {
stream_id: data.streamId,
},
errors: {
422: "Validation Error",
},
});
}
}

View File

@@ -168,6 +168,15 @@ export type SpeakerWords = {
words: Array<Word>;
};
export type Stream = {
stream_id: number;
name: string;
};
export type Topic = {
name: string;
};
export type TranscriptParticipant = {
id?: string;
speaker: number | null;
@@ -427,6 +436,14 @@ export type V1TranscriptProcessResponse = unknown;
export type V1UserMeResponse = UserInfo | null;
export type V1ZulipGetStreamsResponse = Array<Stream>;
export type V1ZulipGetTopicsData = {
streamId: number;
};
export type V1ZulipGetTopicsResponse = Array<Topic>;
export type $OpenApiTs = {
"/metrics": {
get: {
@@ -850,4 +867,29 @@ export type $OpenApiTs = {
};
};
};
"/v1/zulip/streams": {
get: {
res: {
/**
* Successful Response
*/
200: Array<Stream>;
};
};
};
"/v1/zulip/streams/{stream_id}/topics": {
get: {
req: V1ZulipGetTopicsData;
res: {
/**
* Successful Response
*/
200: Array<Topic>;
/**
* Validation Error
*/
422: HTTPValidationError;
};
};
};
};