Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getInitials } from "@hypr/utils";

const NO_ORGANIZATION_ID = "__NO_ORGANIZATION__";

export function useParticipantsWithOrg(sessionId: string) {
function useParticipantsWithOrg(sessionId: string) {
const { data: participants = [] } = useQuery({
queryKey: ["participants", sessionId],
queryFn: async () => {
Expand Down Expand Up @@ -112,7 +112,7 @@ export function ParticipantsChip({
);
}

export function ParticipantsChipInner(
function ParticipantsChipInner(
{ sessionId, handleClickHuman }: { sessionId: string; handleClickHuman: (human: Human) => void },
) {
const participants = useParticipantsWithOrg(sessionId);
Expand All @@ -122,43 +122,71 @@ export function ParticipantsChipInner(
? <ParticipantAddControl sessionId={sessionId} />
: (
<div className="flex flex-col gap-3">
<div className="text-sm font-medium text-neutral-700">Participants</div>
<div className="flex flex-col gap-4 max-h-[40vh] overflow-y-auto custom-scrollbar pr-1">
{participants.map(({ organization, participants }) => (
<div key={organization?.id ?? NO_ORGANIZATION_ID} className="flex flex-col gap-1.5">
<div className="text-xs font-medium text-neutral-400 truncate">
{organization?.name ?? "No organization"}
</div>
<div className="flex flex-col rounded-md overflow-hidden bg-neutral-50 border border-neutral-100">
{(participants ?? []).map((member, index) => (
<ParticipentItem
key={member.id}
member={member}
sessionId={sessionId}
isLast={index === (participants ?? []).length - 1}
handleClickHuman={handleClickHuman}
/>
))}
</div>
</div>
))}
</div>
<ParticipantList sessionId={sessionId} handleClickHuman={handleClickHuman} />
<ParticipantAddControl sessionId={sessionId} />
</div>
)
);
}

export const ParticipantList = (
{ sessionId, handleClickHuman, allowMutate = true, selectedHuman }: {
sessionId: string;
selectedHuman?: Human | null;
handleClickHuman: (human: Human) => void;
allowMutate?: boolean;
},
) => {
const participants = useParticipantsWithOrg(sessionId);

return (
<div className="flex flex-col gap-4 max-h-[40vh] overflow-y-auto custom-scrollbar pr-1">
{(!allowMutate && participants.length === 0) && (
<div className="bg-neutral-50 rounded-lg border border-neutral-200 p-4 flex items-center gap-3">
<span className="bg-neutral-100 rounded-full w-8 h-8 flex items-center justify-center text-lg flex-shrink-0">
</span>
<div className="flex flex-col">
<span className="text-neutral-700 text-sm font-medium">Add participants</span>
<span className="text-neutral-500 text-xs">in the note section</span>
</div>
</div>
)}
{participants.map(({ organization, participants }) => (
<div key={organization?.id ?? NO_ORGANIZATION_ID} className="flex flex-col gap-1.5">
<div className="text-xs font-medium text-neutral-400 truncate">
{organization?.name ?? "No organization"}
</div>
<div className="flex flex-col rounded-md overflow-hidden bg-neutral-50 border border-neutral-100">
{(participants ?? []).map((member, index) => (
<ParticipentItem
key={member.id}
member={member}
sessionId={sessionId}
handleClickHuman={handleClickHuman}
allowRemove={allowMutate}
selected={member.id === selectedHuman?.id}
/>
))}
</div>
</div>
))}
</div>
);
};

function ParticipentItem({
member,
sessionId,
isLast = false,
handleClickHuman,
allowRemove = true,
selected = false,
}: {
member: Human;
sessionId: string;
isLast?: boolean;
handleClickHuman: (human: Human) => void;
allowRemove?: boolean;
selected?: boolean;
}) {
const queryClient = useQueryClient();
const { userId } = useHypr();
Expand All @@ -179,43 +207,50 @@ function ParticipentItem({
<div
className={clsx(
"flex items-center justify-between gap-2 py-2 px-3",
"hover:bg-neutral-100 cursor-pointer group transition-colors",
!isLast && "border-b border-neutral-100",
selected ? "border border-gray-400 rounded-md" : "hover:bg-neutral-100",
"cursor-pointer group transition-colors",
)}
onClick={() => handleClickHuman(member)}
>
<div className="flex items-center gap-2.5 relative min-w-0">
<div className="relative size-7 flex items-center justify-center flex-shrink-0">
<div className="absolute inset-0 flex items-center justify-center group-hover:opacity-0 transition-opacity">
<div
className={clsx(
"absolute inset-0 flex items-center justify-center transition-opacity",
allowRemove && "group-hover:opacity-0",
)}
>
<Avatar className="size-7">
<AvatarFallback className="text-xs bg-neutral-200 text-neutral-700 font-medium">
{member.full_name ? getInitials(member.full_name) : "?"}
</AvatarFallback>
</Avatar>
</div>
<div
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
handleRemoveParticipant(member.id);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
{allowRemove && (
<div
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
handleRemoveParticipant(member.id);
}
}}
className={clsx([
"flex items-center justify-center",
"text-red-400 hover:text-red-600",
"absolute inset-0 rounded-full opacity-0 group-hover:opacity-100 transition-opacity",
"bg-white shadow-sm",
])}
>
<CircleMinus className="size-4" />
</div>
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();
handleRemoveParticipant(member.id);
}
}}
className={clsx([
"flex items-center justify-center",
"text-red-400 hover:text-red-600",
"absolute inset-0 rounded-full opacity-0 group-hover:opacity-100 transition-opacity",
"bg-white shadow-sm",
])}
>
<CircleMinus className="size-4" />
</div>
)}
</div>
<div className="flex flex-col min-w-0 flex-1">
{member.full_name
Expand Down
82 changes: 58 additions & 24 deletions apps/desktop/src/components/right-panel/views/transcript-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "lucide-react";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";

import { ParticipantsChipInner } from "@/components/editor-area/note-header/chips/participants-chip";
import { ParticipantList } from "@/components/editor-area/note-header/chips/participants-chip";
import { useHypr } from "@/contexts";
import { useContainerWidth } from "@/hooks/use-container-width";
import { commands as dbCommands, Human, Word2 } from "@hypr/plugin-db";
Expand Down Expand Up @@ -99,6 +99,7 @@ function RenderNotInMeeting({ sessionId, words }: { sessionId: string; words: Wo
const [editable, setEditable] = useState(false);
const [isSearchActive, setIsSearchActive] = useState(false);
const speakerChunks = useMemo(() => wordsToSpeakerChunks(words), [words]);
const [editorWords, setEditorWords] = useState<Word2[]>([]);

const editorRef = useRef<TranscriptEditorRef | null>(null);
const { isAtBottom, scrollContainerRef, handleScroll, scrollToBottom } = useScrollToBottom([speakerChunks]);
Expand All @@ -116,6 +117,20 @@ function RenderNotInMeeting({ sessionId, words }: { sessionId: string; words: Wo
}
}, [words]);

useEffect(() => {
if (!editable) {
dbCommands.getSession({ id: sessionId }).then((session) => {
if (session) {
dbCommands.upsertSession({ ...session, words: editorWords }).then(() => {
queryClient.invalidateQueries({
queryKey: ["session", "words", sessionId],
});
});
}
});
}
}, [editable]);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
Expand Down Expand Up @@ -154,11 +169,7 @@ function RenderNotInMeeting({ sessionId, words }: { sessionId: string; words: Wo
}, []);

const handleUpdate = (words: Word2[]) => {
dbCommands.getSession({ id: sessionId }).then((session) => {
if (session) {
dbCommands.upsertSession({ ...session, words });
}
});
setEditorWords(words);
};

if (isSearchActive) {
Expand Down Expand Up @@ -405,6 +416,7 @@ const MemoizedSpeakerSelector = memo(({
const [speakerRange, setSpeakerRange] = useState<SpeakerChangeRange>("current");
const inactive = useOngoingSession(s => s.status === "inactive");
const [human, setHuman] = useState<Human | null>(null);
const [candidate, setCandidate] = useState<Human | null>(null);

const noteMatch = useMatch({ from: "/app/note/$id", shouldThrow: false });
const sessionId = noteMatch?.params.id;
Expand All @@ -429,9 +441,12 @@ const MemoizedSpeakerSelector = memo(({
}
}, [participants, speakerId]);

useEffect(() => {
setCandidate(null);
}, [isOpen]);

const handleClickHuman = (human: Human) => {
setHuman(human);
setIsOpen(false);
setCandidate(human);
};

if (!sessionId) {
Expand Down Expand Up @@ -477,14 +492,30 @@ const MemoizedSpeakerSelector = memo(({
</PopoverTrigger>
<PopoverContent align="start" side="bottom">
<div className="space-y-4">
<div className="border-b border-neutral-100 pb-3">
<SpeakerRangeSelector
value={speakerRange}
onChange={setSpeakerRange}
/>
</div>

<ParticipantsChipInner sessionId={sessionId} handleClickHuman={handleClickHuman} />
<ParticipantList
selectedHuman={candidate}
allowMutate={false}
sessionId={sessionId}
handleClickHuman={handleClickHuman}
/>
{candidate && (
<div className="flex flex-col gap-2">
<hr className="mb-2" />
<SpeakerRangeSelector
value={speakerRange}
onChange={setSpeakerRange}
/>
<Button
size="sm"
onClick={() => {
onSpeakerChange(candidate, speakerRange);
setIsOpen(false);
}}
>
Apply Speaker Change
</Button>
</div>
)}
</div>
</PopoverContent>
</Popover>
Expand All @@ -499,15 +530,15 @@ interface SpeakerRangeSelectorProps {

function SpeakerRangeSelector({ value, onChange }: SpeakerRangeSelectorProps) {
const options = [
{ value: "current" as const, label: "Just this" },
{ value: "all" as const, label: "Replace all" },
{ value: "current" as const, label: "Only this" },
{ value: "fromHere" as const, label: "From here" },
{ value: "all" as const, label: "All" },
];

return (
<div className="space-y-1.5">
<div className="flex rounded-md border border-neutral-200 p-0.5 bg-neutral-50">
{options.map((option) => (
<div className="flex rounded-md border border-neutral-200 bg-white">
{options.map((option, index) => (
<label
key={option.value}
className="flex-1 cursor-pointer"
Expand All @@ -521,11 +552,14 @@ function SpeakerRangeSelector({ value, onChange }: SpeakerRangeSelectorProps) {
onChange={() => onChange(option.value)}
/>
<div
className={`px-2 py-1 text-xs font-medium text-center rounded transition-colors ${
className={clsx(
"px-2 py-1.5 text-xs font-medium text-center transition-colors border-neutral-200",
index === 0 && "border-r rounded-l-md",
index === options.length - 1 && "border-l rounded-r-md",
value === option.value
? "bg-white text-neutral-900 shadow-sm"
: "text-neutral-600 hover:text-neutral-900 hover:bg-white/50"
}`}
? "bg-gray-100 text-neutral-900"
: "hover:bg-gray-50 text-neutral-500",
)}
>
{option.label}
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/locales/en/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ msgstr "Control how autonomous the AI enhancement should be."
#~ msgid "Control how creative the AI enhancement should be"
#~ msgstr "Control how creative the AI enhancement should be"

#: src/components/editor-area/note-header/chips/participants-chip.tsx:528
#: src/components/editor-area/note-header/chips/participants-chip.tsx:563
#: src/routes/app.human.$id.tsx:535
msgid "Create"
msgstr "Create"
Expand Down Expand Up @@ -856,7 +856,7 @@ msgstr "Feature Requests"
#~ msgid "Feedback"
#~ msgstr "Feedback"

#: src/components/editor-area/note-header/chips/participants-chip.tsx:350
#: src/components/editor-area/note-header/chips/participants-chip.tsx:385
msgid "Find person"
msgstr "Find person"

Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/locales/ko/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ msgstr ""
#~ msgid "Control how creative the AI enhancement should be"
#~ msgstr ""

#: src/components/editor-area/note-header/chips/participants-chip.tsx:528
#: src/components/editor-area/note-header/chips/participants-chip.tsx:563
#: src/routes/app.human.$id.tsx:535
msgid "Create"
msgstr ""
Expand Down Expand Up @@ -860,7 +860,7 @@ msgstr ""
#~ msgid "Feedback"
#~ msgstr ""

#: src/components/editor-area/note-header/chips/participants-chip.tsx:350
#: src/components/editor-area/note-header/chips/participants-chip.tsx:385
msgid "Find person"
msgstr ""

Expand Down
Loading