mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
Temp commit to merge main into this branch
This commit is contained in:
@@ -49,6 +49,49 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
||||
.replace(/ +/g, " ")
|
||||
.trim() || "";
|
||||
|
||||
if (
|
||||
(transcript?.response?.longSummary === null || true) &&
|
||||
transcript &&
|
||||
transcript.response
|
||||
) {
|
||||
transcript.response.longSummary = `
|
||||
**Meeting Summary:**
|
||||
|
||||
**Date:** November 21, 2023
|
||||
**Attendees:** Alice Johnson, Bob Smith, Carlos Gomez, Dana Lee
|
||||
**Agenda Items:**
|
||||
|
||||
1. **Project Alpha Update:**
|
||||
- Discussed current progress and minor setbacks.
|
||||
- Agreed on extending the deadline by two weeks.
|
||||
- Assigned new tasks to team members.
|
||||
|
||||
2. **Budget Review for Quarter 4:**
|
||||
- Reviewed financial performance.
|
||||
- Identified areas of overspending and discussed cost-cutting measures.
|
||||
- Decided to allocate additional funds to marketing.
|
||||
|
||||
3. **New Product Launch Strategy:**
|
||||
- Brainstormed ideas for the upcoming product launch.
|
||||
- Agreed on a digital-first marketing approach.
|
||||
- Set a tentative launch date for January 15, 2024.
|
||||
|
||||
**Key Decisions:**
|
||||
- Extend Project Alpha's deadline to allow for quality enhancement.
|
||||
- Implement cost-saving strategies in non-essential departments.
|
||||
- Proceed with the digital marketing plan for the new product launch.
|
||||
|
||||
**Action Items:**
|
||||
- Alice to coordinate with the marketing team for the new campaign.
|
||||
- Bob to oversee the budget adjustments and report back in one week.
|
||||
- Carlos to lead the task force for Project Alpha's final phase.
|
||||
- Dana to prepare a detailed report on competitor analysis for the next meeting.
|
||||
|
||||
**Next Meeting:**
|
||||
Scheduled for December 5, 2023, to review progress and finalize the new product launch details.
|
||||
`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!transcriptId || transcript?.loading || topics?.loading ? (
|
||||
@@ -56,10 +99,13 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
||||
) : (
|
||||
<>
|
||||
<ShareModal
|
||||
transcript={transcript.response}
|
||||
topics={topics ? topics.topics : null}
|
||||
show={showModal}
|
||||
setShow={(v) => setShowModal(v)}
|
||||
title={transcript?.response?.title}
|
||||
summary={transcript?.response?.longSummary}
|
||||
date={transcript?.response?.createdAt}
|
||||
url={window.location.href}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
@@ -90,19 +136,13 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
||||
/>
|
||||
<div className="w-full h-full grid grid-rows-layout-one grid-cols-1 gap-2 lg:gap-4">
|
||||
<section className=" bg-blue-400/20 rounded-lg md:rounded-xl p-2 md:px-4 h-full">
|
||||
<button
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
Open Modal
|
||||
</button>
|
||||
|
||||
{transcript?.response?.longSummary && (
|
||||
<FinalSummary
|
||||
protectedPath={protectedPath}
|
||||
fullTranscript={fullTranscript}
|
||||
summary={transcript?.response?.longSummary}
|
||||
transcriptId={transcript?.response?.id}
|
||||
openZulipModal={() => setShowModal(true)}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import SelectSearch from "react-select-search";
|
||||
import {
|
||||
getZulipMessage,
|
||||
sendZulipMessage,
|
||||
ZULIP_MSG_MAX_LENGTH,
|
||||
} from "../../../lib/zulip";
|
||||
import { Transcript } from "../webSocketTypes";
|
||||
import {
|
||||
GetTranscript,
|
||||
GetTranscriptSegmentTopic,
|
||||
GetTranscriptTopic,
|
||||
} from "../../../api";
|
||||
import "react-select-search/style.css";
|
||||
|
||||
type ShareModal = {
|
||||
show: boolean;
|
||||
@@ -7,16 +19,28 @@ type ShareModal = {
|
||||
title: string;
|
||||
url: string;
|
||||
summary: string;
|
||||
date: string;
|
||||
transcript: GetTranscript | null;
|
||||
topics: GetTranscriptTopic[] | null;
|
||||
};
|
||||
|
||||
interface Stream {
|
||||
id: number;
|
||||
name: string;
|
||||
topics: string[];
|
||||
}
|
||||
|
||||
interface SelectSearchOption {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const ShareModal = (props: ShareModal) => {
|
||||
const [stream, setStream] = useState(null);
|
||||
const [topic, setTopic] = useState(null);
|
||||
const [includeTranscript, setIncludeTranscript] = useState(false);
|
||||
const [includeSummary, setIncludeSummary] = useState(false);
|
||||
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({});
|
||||
const [streams, setStreams] = useState<Stream[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/streams.json")
|
||||
@@ -27,6 +51,9 @@ const ShareModal = (props: ShareModal) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
data = data.sort((a: Stream, b: Stream) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
console.log("Stream data:", data);
|
||||
setStreams(data);
|
||||
setIsLoading(false);
|
||||
@@ -38,67 +65,99 @@ const ShareModal = (props: ShareModal) => {
|
||||
}, []);
|
||||
|
||||
const handleSendToZulip = () => {
|
||||
const message = `### Reflector Recording\n\n**[${props.title}](${props.url})**\n\n${props.summary}`;
|
||||
if (!props.transcript) return;
|
||||
|
||||
alert("Send to zulip");
|
||||
const msg = getZulipMessage(props.transcript, props.topics, includeTopics);
|
||||
|
||||
if (stream && topic) sendZulipMessage(stream, topic, msg);
|
||||
};
|
||||
|
||||
if (props.show && isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
console.log(stream);
|
||||
|
||||
let streamOptions: SelectSearchOption[] = [];
|
||||
if (streams) {
|
||||
streams.forEach((stream) => {
|
||||
const value = stream.name;
|
||||
streamOptions.push({ name: value, value: value });
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.show && (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full">
|
||||
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white border-black border-1">
|
||||
<div className="mt-3 text-center">
|
||||
{/*
|
||||
<Select
|
||||
options={streamOptions}
|
||||
onChange={setStream}
|
||||
placeholder="Select Stream"
|
||||
/>
|
||||
<Select
|
||||
options={topicOptions}
|
||||
onChange={setTopic}
|
||||
placeholder="Select Topic"
|
||||
className="mt-4"
|
||||
/> */}
|
||||
<div className="flex flex-col mt-4">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeTranscript}
|
||||
onChange={(e) => setIncludeTranscript(e.target.checked)}
|
||||
/>
|
||||
Include Transcript
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeSummary}
|
||||
onChange={(e) => setIncludeSummary(e.target.checked)}
|
||||
/>
|
||||
Include Summary
|
||||
</label>
|
||||
<label>
|
||||
<h3 className="font-bold text-xl">Send to Zulip</h3>
|
||||
|
||||
{/* Checkbox for 'Include Topics' */}
|
||||
<div className="mt-4 text-left ml-5">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-checkbox rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
||||
checked={includeTopics}
|
||||
onChange={(e) => setIncludeTopics(e.target.checked)}
|
||||
/>
|
||||
Include Topics
|
||||
<span className="ml-2">Include topics</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mt-4">
|
||||
<span className="mr-2">#</span>
|
||||
<SelectSearch
|
||||
search={true}
|
||||
options={streamOptions}
|
||||
value={stream}
|
||||
onChange={(val) => {
|
||||
setTopic(undefined);
|
||||
setStream(val.toString());
|
||||
}}
|
||||
placeholder="Pick a stream"
|
||||
/>
|
||||
</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 })) || []
|
||||
}
|
||||
value={topic}
|
||||
onChange={(val) => setTopic(val.toString())}
|
||||
placeholder="Pick a topic"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded mt-4"
|
||||
onClick={handleSendToZulip}
|
||||
className={`bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded py-2 px-4 mr-3 ${
|
||||
!stream || !topic ? "opacity-50 cursor-not-allowed" : ""
|
||||
}`}
|
||||
disabled={!stream || !topic}
|
||||
onClick={() => {
|
||||
handleSendToZulip();
|
||||
props.setShow(false);
|
||||
}}
|
||||
>
|
||||
Send to Zulip
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded mt-4"
|
||||
className="bg-red-500 hover:bg-red-700 focus-visible:bg-red-700 text-white rounded py-2 px-4 mt-4"
|
||||
onClick={() => props.setShow(false)}
|
||||
>
|
||||
Close
|
||||
|
||||
@@ -9,6 +9,7 @@ type FinalSummaryProps = {
|
||||
summary: string;
|
||||
fullTranscript: string;
|
||||
transcriptId: string;
|
||||
openZulipModal: () => void;
|
||||
};
|
||||
|
||||
export default function FinalSummary(props: FinalSummaryProps) {
|
||||
@@ -117,33 +118,45 @@ export default function FinalSummary(props: FinalSummaryProps) {
|
||||
|
||||
{!isEditMode && (
|
||||
<>
|
||||
<button
|
||||
className={
|
||||
"bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base"
|
||||
}
|
||||
onClick={() => props.openZulipModal()}
|
||||
>
|
||||
<span className="text-xs">➡️ Zulip</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onEditClick}
|
||||
className={
|
||||
"bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base text-xs"
|
||||
"bg-blue-400 hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base"
|
||||
}
|
||||
>
|
||||
Edit Summary
|
||||
<span className="text-xs">✏️ Summary</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onCopyTranscriptClick}
|
||||
className={
|
||||
(isCopiedTranscript ? "bg-blue-500" : "bg-blue-400") +
|
||||
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base text-xs"
|
||||
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base"
|
||||
}
|
||||
style={{ minHeight: "30px" }}
|
||||
>
|
||||
{isCopiedTranscript ? "Copied!" : "Copy Transcript"}
|
||||
<span className="text-xs">
|
||||
{isCopiedTranscript ? "Copied!" : "Copy Transcript"}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onCopySummaryClick}
|
||||
className={
|
||||
(isCopiedSummary ? "bg-blue-500" : "bg-blue-400") +
|
||||
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base text-xs"
|
||||
" hover:bg-blue-500 focus-visible:bg-blue-500 text-white rounded p-2 sm:text-base"
|
||||
}
|
||||
style={{ minHeight: "30px" }}
|
||||
>
|
||||
{isCopiedSummary ? "Copied!" : "Copy Summary"}
|
||||
<span className="text-xs">
|
||||
{isCopiedSummary ? "Copied!" : "Copy Summary"}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -121,3 +121,13 @@ export const generateHighContrastColor = (
|
||||
|
||||
return getCssColor(red, green, blue);
|
||||
};
|
||||
|
||||
export function extractDomain(url) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.host;
|
||||
} catch (error) {
|
||||
console.error("Invalid URL:", error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
80
www/app/lib/zulip.ts
Normal file
80
www/app/lib/zulip.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { GetTranscript, GetTranscriptTopic } from "../api";
|
||||
import { formatTime } from "./time";
|
||||
import { extractDomain } from "./utils";
|
||||
|
||||
export async function sendZulipMessage(
|
||||
stream: string,
|
||||
topic: string,
|
||||
message: string,
|
||||
) {
|
||||
console.log("Sendiing zulip message", stream, topic);
|
||||
try {
|
||||
const response = await fetch("/api/send-zulip-message", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ stream, topic, message }),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const ZULIP_MSG_MAX_LENGTH = 10000;
|
||||
|
||||
export function getZulipMessage(
|
||||
transcript: GetTranscript,
|
||||
topics: GetTranscriptTopic[] | null,
|
||||
includeTopics: boolean,
|
||||
) {
|
||||
const date = new Date(transcript.createdAt);
|
||||
|
||||
// Get the timezone offset in minutes and convert it to hours and minutes
|
||||
const timezoneOffset = -date.getTimezoneOffset();
|
||||
const offsetHours = String(
|
||||
Math.floor(Math.abs(timezoneOffset) / 60),
|
||||
).padStart(2, "0");
|
||||
const offsetMinutes = String(Math.abs(timezoneOffset) % 60).padStart(2, "0");
|
||||
const offsetSign = timezoneOffset >= 0 ? "+" : "-";
|
||||
|
||||
// Combine to get the formatted timezone offset
|
||||
const formattedOffset = `${offsetSign}${offsetHours}:${offsetMinutes}`;
|
||||
|
||||
// Now you can format your date and time string using this offset
|
||||
const formattedDate = date.toISOString().slice(0, 10);
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const seconds = String(date.getSeconds()).padStart(2, "0");
|
||||
|
||||
const dateTimeString = `${formattedDate}T${hours}:${minutes}:${seconds}${formattedOffset}`;
|
||||
|
||||
const domain = window.location.origin; // Gives you "http://localhost:3000" or your deployment base URL
|
||||
const link = `${domain}/transcripts/${transcript.id}`;
|
||||
|
||||
let headerText = `# Reflector – ${transcript.title}
|
||||
|
||||
**Date**: <time:${dateTimeString}>
|
||||
**Link**: [${extractDomain(link)}](${link})
|
||||
**Duration**: ${formatTime(transcript.duration)}
|
||||
|
||||
`;
|
||||
let topicText = "";
|
||||
|
||||
if (topics && includeTopics) {
|
||||
topicText = "```spoiler Topics\n";
|
||||
topics.forEach((topic) => {
|
||||
topicText += `1. [${formatTime(topic.timestamp)}] ${topic.title}\n`;
|
||||
});
|
||||
topicText += "```\n\n";
|
||||
}
|
||||
|
||||
let summary = "```spoiler Summary\n";
|
||||
summary += transcript.longSummary;
|
||||
summary += "```\n\n";
|
||||
|
||||
const message = "----\n\n" + headerText + summary + topicText + "-----\n";
|
||||
return message;
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import axios from "axios";
|
||||
|
||||
type ZulipStream = {
|
||||
id: number;
|
||||
name: string;
|
||||
topics: string[];
|
||||
};
|
||||
|
||||
async function getTopics(stream_id: number): Promise<string[]> {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`https://${process.env.ZULIP_REALM}/api/v1/users/me/${stream_id}/topics`,
|
||||
{
|
||||
auth: {
|
||||
username: process.env.ZULIP_BOT_EMAIL || "?",
|
||||
password: process.env.ZULIP_API_KEY || "?",
|
||||
},
|
||||
},
|
||||
);
|
||||
return response.data.topics.map((topic) => topic.name);
|
||||
} catch (error) {
|
||||
console.error("Error fetching topics for stream " + stream_id, error);
|
||||
throw error; // Propagate the error up to be handled by the caller
|
||||
}
|
||||
}
|
||||
|
||||
async function getStreams(): Promise<ZulipStream[]> {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`https://${process.env.ZULIP_REALM}/api/v1/streams`,
|
||||
{
|
||||
auth: {
|
||||
username: process.env.ZULIP_BOT_EMAIL || "?",
|
||||
password: process.env.ZULIP_API_KEY || "?",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const streams: ZulipStream[] = [];
|
||||
for (const stream of response.data.streams) {
|
||||
console.log("Loading topics for " + stream.name);
|
||||
const topics = await getTopics(stream.stream_id);
|
||||
streams.push({ id: stream.stream_id, name: stream.name, topics });
|
||||
}
|
||||
|
||||
return streams;
|
||||
} catch (error) {
|
||||
console.error("Error fetching zulip streams", error);
|
||||
throw error; // Propagate the error up
|
||||
}
|
||||
}
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
const streams = await getStreams();
|
||||
return res.status(200).json({ streams });
|
||||
} catch (error) {
|
||||
// Handle errors more gracefully
|
||||
return res.status(500).json({ error: "Internal Server Error" });
|
||||
}
|
||||
};
|
||||
44
www/pages/api/send-zulip-message.ts
Normal file
44
www/pages/api/send-zulip-message.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import axios from "axios";
|
||||
import { URLSearchParams } from "url";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === "POST") {
|
||||
const { stream, topic, message } = req.body;
|
||||
console.log("Sending zulip message", stream, topic);
|
||||
|
||||
if (!stream || !topic || !message) {
|
||||
return res.status(400).json({ error: "Missing required parameters" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Construct URL-encoded data
|
||||
const params = new URLSearchParams();
|
||||
params.append("type", "stream");
|
||||
params.append("to", stream);
|
||||
params.append("topic", topic);
|
||||
params.append("content", message);
|
||||
|
||||
// Send the request1
|
||||
const zulipResponse = await axios.post(
|
||||
`https://${process.env.ZULIP_REALM}/api/v1/messages`,
|
||||
params,
|
||||
{
|
||||
auth: {
|
||||
username: process.env.ZULIP_BOT_EMAIL || "?",
|
||||
password: process.env.ZULIP_API_KEY || "?",
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json(zulipResponse.data);
|
||||
} catch (error) {
|
||||
return res.status(500).json({ failed: true, error: error });
|
||||
}
|
||||
} else {
|
||||
res.setHeader("Allow", ["POST"]);
|
||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user