mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
Merge branch 'main' into feat-api-speaker-reassignment
This commit is contained in:
@@ -9,9 +9,11 @@ export const DomainContext = createContext<DomainContextType>({
|
||||
requireLogin: false,
|
||||
privacy: true,
|
||||
browse: false,
|
||||
sendToZulip: false,
|
||||
},
|
||||
api_url: "",
|
||||
websocket_url: "",
|
||||
zulip_streams: "",
|
||||
});
|
||||
|
||||
export const DomainContextProvider = ({
|
||||
@@ -38,9 +40,10 @@ export const DomainContextProvider = ({
|
||||
|
||||
// Get feature config client-side with
|
||||
export const featureEnabled = (
|
||||
featureName: "requireLogin" | "privacy" | "browse",
|
||||
featureName: "requireLogin" | "privacy" | "browse" | "sendToZulip",
|
||||
) => {
|
||||
const context = useContext(DomainContext);
|
||||
|
||||
return context.features[featureName] as boolean | undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@ import FinalSummary from "../finalSummary";
|
||||
import ShareLink from "../shareLink";
|
||||
import QRCode from "react-qr-code";
|
||||
import TranscriptTitle from "../transcriptTitle";
|
||||
import ShareModal from "./shareModal";
|
||||
import Player from "../player";
|
||||
import WaveformLoading from "../waveformLoading";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { featureEnabled } from "../../domainContext";
|
||||
|
||||
type TranscriptDetails = {
|
||||
params: {
|
||||
@@ -33,6 +35,7 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
||||
const waveform = useWaveform(transcriptId);
|
||||
const useActiveTopic = useState<Topic | null>(null);
|
||||
const mp3 = useMp3(transcriptId);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const statusToRedirect = ["idle", "recording", "processing"];
|
||||
@@ -53,89 +56,100 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
||||
.replace(/ +/g, " ")
|
||||
.trim() || "";
|
||||
|
||||
if (transcript.error || topics?.error) {
|
||||
if (transcript && transcript.response) {
|
||||
if (transcript.error || topics?.error) {
|
||||
return (
|
||||
<Modal
|
||||
title="Transcription Not Found"
|
||||
text="A trascription with this ID does not exist."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!transcriptId || transcript?.loading || topics?.loading) {
|
||||
return <Modal title="Loading" text={"Loading transcript..."} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Transcription Not Found"
|
||||
text="A trascription with this ID does not exist."
|
||||
/>
|
||||
<>
|
||||
{featureEnabled("sendToZulip") && (
|
||||
<ShareModal
|
||||
transcript={transcript.response}
|
||||
topics={topics ? topics.topics : null}
|
||||
show={showModal}
|
||||
setShow={(v) => setShowModal(v)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
{transcript?.response?.title && (
|
||||
<TranscriptTitle
|
||||
title={transcript.response.title}
|
||||
transcriptId={transcript.response.id}
|
||||
/>
|
||||
)}
|
||||
{waveform.waveform && mp3.media ? (
|
||||
<Player
|
||||
topics={topics?.topics || []}
|
||||
useActiveTopic={useActiveTopic}
|
||||
waveform={waveform.waveform.data}
|
||||
media={mp3.media}
|
||||
mediaDuration={transcript.response.duration}
|
||||
/>
|
||||
) : waveform.error ? (
|
||||
<div>"error loading this recording"</div>
|
||||
) : (
|
||||
<WaveformLoading />
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-2 lg:grid-rows-1 gap-2 lg:gap-4 h-full">
|
||||
<TopicList
|
||||
topics={topics.topics || []}
|
||||
useActiveTopic={useActiveTopic}
|
||||
autoscroll={false}
|
||||
/>
|
||||
|
||||
<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">
|
||||
{transcript.response.longSummary ? (
|
||||
<FinalSummary
|
||||
fullTranscript={fullTranscript}
|
||||
summary={transcript.response.longSummary}
|
||||
transcriptId={transcript.response.id}
|
||||
openZulipModal={() => setShowModal(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col h-full justify-center content-center">
|
||||
{transcript.response.status == "processing" ? (
|
||||
<p>Loading Transcript</p>
|
||||
) : (
|
||||
<p>
|
||||
There was an error generating the final summary, please
|
||||
come back later
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="flex items-center">
|
||||
<div className="mr-4 hidden md:block h-auto">
|
||||
<QRCode
|
||||
value={`${location.origin}/transcripts/${details.params.transcriptId}`}
|
||||
level="L"
|
||||
size={98}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow max-w-full">
|
||||
<ShareLink
|
||||
transcriptId={transcript?.response?.id}
|
||||
userId={transcript?.response?.userId}
|
||||
shareMode={transcript?.response?.shareMode}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (transcript?.loading || topics?.loading) {
|
||||
return <Modal title="Loading" text={"Loading transcript..."} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
{transcript?.response?.title && (
|
||||
<TranscriptTitle
|
||||
title={transcript.response.title}
|
||||
transcriptId={transcript.response.id}
|
||||
/>
|
||||
)}
|
||||
{waveform.waveform && mp3.media ? (
|
||||
<Player
|
||||
topics={topics?.topics || []}
|
||||
useActiveTopic={useActiveTopic}
|
||||
waveform={waveform.waveform.data}
|
||||
media={mp3.media}
|
||||
mediaDuration={transcript.response.duration}
|
||||
/>
|
||||
) : waveform.error ? (
|
||||
<div>"error loading this recording"</div>
|
||||
) : (
|
||||
<WaveformLoading />
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-2 lg:grid-rows-1 gap-2 lg:gap-4 h-full">
|
||||
<TopicList
|
||||
topics={topics.topics || []}
|
||||
useActiveTopic={useActiveTopic}
|
||||
autoscroll={false}
|
||||
/>
|
||||
|
||||
<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">
|
||||
{transcript.response.longSummary ? (
|
||||
<FinalSummary
|
||||
fullTranscript={fullTranscript}
|
||||
summary={transcript.response.longSummary}
|
||||
transcriptId={transcript.response.id}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col h-full justify-center content-center">
|
||||
{transcript.response.status == "processing" ? (
|
||||
<p>Loading Transcript</p>
|
||||
) : (
|
||||
<p>
|
||||
There was an error generating the final summary, please come
|
||||
back later
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="flex items-center">
|
||||
<div className="mr-4 hidden md:block h-auto">
|
||||
<QRCode
|
||||
value={`${location.origin}/transcripts/${details.params.transcriptId}`}
|
||||
level="L"
|
||||
size={98}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow max-w-full">
|
||||
<ShareLink
|
||||
transcriptId={transcript?.response?.id}
|
||||
userId={transcript?.response?.userId}
|
||||
shareMode={transcript?.response?.shareMode}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
159
www/app/[domain]/transcripts/[transcriptId]/shareModal.tsx
Normal file
159
www/app/[domain]/transcripts/[transcriptId]/shareModal.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import SelectSearch from "react-select-search";
|
||||
import { getZulipMessage, sendZulipMessage } from "../../../lib/zulip";
|
||||
import { GetTranscript, GetTranscriptTopic } from "../../../api";
|
||||
import "react-select-search/style.css";
|
||||
import { DomainContext } from "../../domainContext";
|
||||
|
||||
type ShareModal = {
|
||||
show: boolean;
|
||||
setShow: (show: boolean) => void;
|
||||
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<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);
|
||||
|
||||
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);
|
||||
setIsLoading(false);
|
||||
// data now contains the JavaScript object decoded from JSON
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was a problem with your fetch operation:", error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSendToZulip = () => {
|
||||
if (!props.transcript) return;
|
||||
|
||||
const msg = getZulipMessage(props.transcript, props.topics, includeTopics);
|
||||
|
||||
if (stream && topic) sendZulipMessage(stream, topic, msg);
|
||||
};
|
||||
|
||||
if (props.show && isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
let streamOptions: SelectSearchOption[] = [];
|
||||
if (streams) {
|
||||
streams.forEach((stream) => {
|
||||
const value = stream.name;
|
||||
streamOptions.push({ name: value, value: value });
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute">
|
||||
{props.show && (
|
||||
<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 w-96 shadow-lg rounded-md bg-white">
|
||||
<div className="mt-3 text-center">
|
||||
<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)}
|
||||
/>
|
||||
<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-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 focus-visible:bg-red-700 text-white rounded py-2 px-4 mt-4"
|
||||
onClick={() => props.setShow(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareModal;
|
||||
@@ -3,11 +3,13 @@ import React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import "../../styles/markdown.css";
|
||||
import getApi from "../../lib/getApi";
|
||||
import { featureEnabled } from "../domainContext";
|
||||
|
||||
type FinalSummaryProps = {
|
||||
summary: string;
|
||||
fullTranscript: string;
|
||||
transcriptId: string;
|
||||
openZulipModal: () => void;
|
||||
};
|
||||
|
||||
export default function FinalSummary(props: FinalSummaryProps) {
|
||||
@@ -116,33 +118,48 @@ export default function FinalSummary(props: FinalSummaryProps) {
|
||||
|
||||
{!isEditMode && (
|
||||
<>
|
||||
{featureEnabled("sendToZulip") && (
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import { get } from "@vercel/edge-config";
|
||||
import { isDevelopment } from "./utils";
|
||||
|
||||
const localConfig = {
|
||||
features: {
|
||||
requireLogin: true,
|
||||
privacy: true,
|
||||
browse: true,
|
||||
},
|
||||
api_url: "http://127.0.0.1:1250",
|
||||
websocket_url: "ws://127.0.0.1:1250",
|
||||
auth_callback_url: "http://localhost:3000/auth-callback",
|
||||
};
|
||||
|
||||
type EdgeConfig = {
|
||||
[domainWithDash: string]: {
|
||||
features: {
|
||||
[featureName in "requireLogin" | "privacy" | "browse"]: boolean;
|
||||
[featureName in
|
||||
| "requireLogin"
|
||||
| "privacy"
|
||||
| "browse"
|
||||
| "sendToZulip"]: boolean;
|
||||
};
|
||||
auth_callback_url: string;
|
||||
websocket_url: string;
|
||||
api_url: string;
|
||||
zulip_streams: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,8 +30,8 @@ export function edgeDomainToKey(domain: string) {
|
||||
|
||||
// get edge config server-side (prefer DomainContext when available), domain is the hostname
|
||||
export async function getConfig(domain: string) {
|
||||
if (isDevelopment()) {
|
||||
return localConfig;
|
||||
if (process.env.NEXT_PUBLIC_ENV === "development") {
|
||||
return require("../../config").localConfig;
|
||||
}
|
||||
|
||||
let config = await get(edgeDomainToKey(domain));
|
||||
|
||||
@@ -98,26 +98,41 @@ export function murmurhash3_32_gc(key: string, seed: number = 0) {
|
||||
|
||||
export const generateHighContrastColor = (
|
||||
name: string,
|
||||
backgroundColor: [number, number, number] | null = null,
|
||||
backgroundColor: [number, number, number],
|
||||
) => {
|
||||
const hash = murmurhash3_32_gc(name);
|
||||
let red = (hash & 0xff0000) >> 16;
|
||||
let green = (hash & 0x00ff00) >> 8;
|
||||
let blue = hash & 0x0000ff;
|
||||
let loopNumber = 0;
|
||||
let minAcceptedContrast = 3.5;
|
||||
while (true && /* Just as a safeguard */ loopNumber < 100) {
|
||||
++loopNumber;
|
||||
|
||||
const getCssColor = (red: number, green: number, blue: number) =>
|
||||
`rgb(${red}, ${green}, ${blue})`;
|
||||
if (loopNumber > 5) minAcceptedContrast -= 0.5;
|
||||
|
||||
if (!backgroundColor) return getCssColor(red, green, blue);
|
||||
const hash = murmurhash3_32_gc(name + loopNumber);
|
||||
let red = (hash & 0xff0000) >> 16;
|
||||
let green = (hash & 0x00ff00) >> 8;
|
||||
let blue = hash & 0x0000ff;
|
||||
|
||||
const contrast = getContrastRatio([red, green, blue], backgroundColor);
|
||||
let contrast = getContrastRatio([red, green, blue], backgroundColor);
|
||||
|
||||
// Adjust the color to achieve better contrast if necessary (WCAG recommends at least 4.5:1 for text)
|
||||
if (contrast < 4.5) {
|
||||
if (contrast > minAcceptedContrast) return `rgb(${red}, ${green}, ${blue})`;
|
||||
|
||||
// Try to invert the color to increase contrat - this works best the more away the color is from gray
|
||||
red = Math.abs(255 - red);
|
||||
green = Math.abs(255 - green);
|
||||
blue = Math.abs(255 - blue);
|
||||
}
|
||||
|
||||
return getCssColor(red, green, blue);
|
||||
contrast = getContrastRatio([red, green, blue], backgroundColor);
|
||||
|
||||
if (contrast > minAcceptedContrast) return `rgb(${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 ?? "Unnamed recording"}
|
||||
|
||||
**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 = headerText + summary + topicText + "-----\n";
|
||||
return message;
|
||||
}
|
||||
Reference in New Issue
Block a user