diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants.tsx
deleted file mode 100644
index 4d7428a7a0..0000000000
--- a/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants.tsx
+++ /dev/null
@@ -1,432 +0,0 @@
-import { X } from "lucide-react";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-
-import type { SpeakerHintStorage, Transcript } from "@hypr/store";
-import { Badge } from "@hypr/ui/components/ui/badge";
-import { Button } from "@hypr/ui/components/ui/button";
-import { cn } from "@hypr/utils";
-
-import * as main from "../../../../../../store/tinybase/main";
-import { useTabs } from "../../../../../../store/zustand/tabs/index";
-
-function createHuman(store: any, userId: string, name: string) {
- const humanId = crypto.randomUUID();
- store.setRow("humans", humanId, {
- user_id: userId,
- created_at: new Date().toISOString(),
- name,
- email: "",
- org_id: "",
- job_title: "",
- linkedin_username: "",
- is_user: false,
- memo: "",
- });
- return humanId;
-}
-
-function linkHumanToSession(
- store: any,
- userId: string,
- sessionId: string,
- humanId: string,
-) {
- const mappingId = crypto.randomUUID();
- store.setRow("mapping_session_participant", mappingId, {
- user_id: userId,
- created_at: new Date().toISOString(),
- session_id: sessionId,
- human_id: humanId,
- });
-}
-
-function createAndLinkHuman(
- store: any,
- userId: string,
- sessionId: string,
- name: string,
-) {
- const humanId = createHuman(store, userId, name);
- linkHumanToSession(store, userId, sessionId, humanId);
- return humanId;
-}
-
-export function ParticipantsDisplay({ sessionId }: { sessionId: string }) {
- const mappingIds = main.UI.useSliceRowIds(
- main.INDEXES.sessionParticipantsBySession,
- sessionId,
- main.STORE_ID,
- ) as string[];
-
- return (
-
- );
-}
-
-function useParticipantDetails(mappingId: string) {
- const result = main.UI.useResultRow(
- main.QUERIES.sessionParticipantsWithDetails,
- mappingId,
- main.STORE_ID,
- );
-
- if (!result) {
- return null;
- }
-
- return {
- mappingId,
- humanId: result.human_id as string,
- humanName: (result.human_name as string) || "",
- humanEmail: (result.human_email as string | undefined) || undefined,
- humanJobTitle: (result.human_job_title as string | undefined) || undefined,
- humanLinkedinUsername:
- (result.human_linkedin_username as string | undefined) || undefined,
- humanIsUser: result.human_is_user as boolean,
- orgId: (result.org_id as string | undefined) || undefined,
- orgName: result.org_name as string | undefined,
- sessionId: result.session_id as string,
- };
-}
-
-function parseHumanIdFromHintValue(value: unknown): string | undefined {
- const data =
- typeof value === "string"
- ? (() => {
- try {
- return JSON.parse(value);
- } catch {
- return undefined;
- }
- })()
- : value;
-
- if (data && typeof data === "object" && "human_id" in data) {
- const humanId = (data as Record).human_id;
- return typeof humanId === "string" ? humanId : undefined;
- }
-
- return undefined;
-}
-
-function useRemoveParticipant({
- mappingId,
- assignedHumanId,
- sessionId,
-}: {
- mappingId: string;
- assignedHumanId: string | undefined;
- sessionId: string | undefined;
-}) {
- const store = main.UI.useStore(main.STORE_ID);
-
- return useCallback(() => {
- if (!store) {
- return;
- }
-
- if (assignedHumanId && sessionId) {
- const hintIdsToDelete: string[] = [];
-
- store.forEachRow("speaker_hints", (hintId, _forEachCell) => {
- const hint = store.getRow("speaker_hints", hintId) as
- | SpeakerHintStorage
- | undefined;
- if (!hint || hint.type !== "user_speaker_assignment") {
- return;
- }
-
- const transcriptId = hint.transcript_id;
- if (typeof transcriptId !== "string") {
- return;
- }
-
- const transcript = store.getRow("transcripts", transcriptId) as
- | Transcript
- | undefined;
- if (!transcript || transcript.session_id !== sessionId) {
- return;
- }
-
- const hintHumanId = parseHumanIdFromHintValue(hint.value);
- if (hintHumanId === assignedHumanId) {
- hintIdsToDelete.push(hintId);
- }
- });
-
- hintIdsToDelete.forEach((hintId) => {
- store.delRow("speaker_hints", hintId);
- });
- }
-
- store.delRow("mapping_session_participant", mappingId);
- }, [store, mappingId, assignedHumanId, sessionId]);
-}
-
-function ParticipantChip({ mappingId }: { mappingId: string }) {
- const details = useParticipantDetails(mappingId);
- const openNew = useTabs.getState().openNew;
-
- const assignedHumanId = details?.humanId;
- const sessionId = details?.sessionId;
-
- const handleRemove = useRemoveParticipant({
- mappingId,
- assignedHumanId,
- sessionId,
- });
-
- const handleClick = useCallback(() => {
- if (assignedHumanId) {
- openNew({
- type: "contacts",
- state: { selectedOrganization: null, selectedPerson: assignedHumanId },
- });
- }
- }, [openNew, assignedHumanId]);
-
- if (!details) {
- return null;
- }
-
- const { humanName } = details;
-
- return (
-
- {humanName || "Unknown"}
-
-
- );
-}
-
-function ParticipantChipInput({
- sessionId,
- mappingIds,
-}: {
- sessionId: string;
- mappingIds: string[];
-}) {
- const [inputValue, setInputValue] = useState("");
- const [showDropdown, setShowDropdown] = useState(false);
- const [selectedIndex, setSelectedIndex] = useState(0);
- const inputRef = useRef(null);
- const containerRef = useRef(null);
- const store = main.UI.useStore(main.STORE_ID);
- const userId = main.UI.useValue("user_id", main.STORE_ID);
- const allHumanIds = main.UI.useRowIds("humans", main.STORE_ID) as string[];
- const queries = main.UI.useQueries(main.STORE_ID);
-
- const existingHumanIds = useMemo(() => {
- if (!queries) {
- return new Set();
- }
-
- const ids = new Set();
- for (const mappingId of mappingIds) {
- const result = queries.getResultRow(
- main.QUERIES.sessionParticipantsWithDetails,
- mappingId,
- );
- if (result?.human_id) {
- ids.add(result.human_id as string);
- }
- }
- return ids;
- }, [mappingIds, queries]);
-
- const candidates = useMemo(() => {
- const searchLower = inputValue.toLowerCase();
- return allHumanIds
- .filter((humanId: string) => !existingHumanIds.has(humanId))
- .map((humanId: string) => {
- const human = store?.getRow("humans", humanId);
- if (!human) {
- return null;
- }
-
- const name = (human.name || "") as string;
- const email = (human.email || "") as string;
- const nameMatch = name.toLowerCase().includes(searchLower);
- const emailMatch = email.toLowerCase().includes(searchLower);
-
- if (inputValue && !nameMatch && !emailMatch) {
- return null;
- }
-
- return {
- id: humanId,
- name,
- email,
- orgId: human.org_id as string | undefined,
- jobTitle: human.job_title as string | undefined,
- isNew: false,
- };
- })
- .filter((h): h is NonNullable => h !== null);
- }, [inputValue, allHumanIds, existingHumanIds, store]);
-
- const showCustomOption =
- inputValue.trim() &&
- !candidates.some((c) => c.name.toLowerCase() === inputValue.toLowerCase());
-
- const dropdownOptions = showCustomOption
- ? [
- {
- id: "new",
- name: inputValue.trim(),
- isNew: true,
- email: "",
- orgId: undefined,
- jobTitle: undefined,
- },
- ...candidates,
- ]
- : candidates;
-
- const handleAddParticipant = useCallback(
- (option: {
- id: string;
- name: string;
- isNew?: boolean;
- email?: string;
- orgId?: string;
- jobTitle?: string;
- }) => {
- if (!store || !userId) {
- return;
- }
-
- if (option.isNew) {
- createAndLinkHuman(store, userId, sessionId, option.name);
- } else {
- linkHumanToSession(store, userId, sessionId, option.id);
- }
-
- setInputValue("");
- setShowDropdown(false);
- setSelectedIndex(0);
- inputRef.current?.focus();
- },
- [store, userId, sessionId],
- );
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" && inputValue.trim()) {
- e.preventDefault();
- if (dropdownOptions.length > 0) {
- handleAddParticipant(dropdownOptions[selectedIndex]);
- }
- } else if (e.key === "ArrowDown") {
- e.preventDefault();
- setSelectedIndex((prev) =>
- prev < dropdownOptions.length - 1 ? prev + 1 : prev,
- );
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
- } else if (e.key === "Escape") {
- setShowDropdown(false);
- setSelectedIndex(0);
- } else if (e.key === "Backspace" && !inputValue && mappingIds.length > 0) {
- const lastMappingId = mappingIds[mappingIds.length - 1];
- if (store) {
- store.delRow("mapping_session_participant", lastMappingId);
- }
- }
- };
-
- const handleInputChange = (value: string) => {
- setInputValue(value);
- setShowDropdown(true);
- setSelectedIndex(0);
- };
-
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- containerRef.current &&
- !containerRef.current.contains(event.target as Node)
- ) {
- setShowDropdown(false);
- }
- };
-
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, []);
-
- return (
-
-
inputRef.current?.focus()}
- >
- {mappingIds.map((mappingId) => (
-
- ))}
-
handleInputChange(e.target.value)}
- onKeyDown={handleKeyDown}
- onFocus={() => setShowDropdown(true)}
- />
-
- {showDropdown && inputValue.trim() && dropdownOptions.length > 0 && (
-
-
- {dropdownOptions.map((option, index) => (
-
- ))}
-
-
- )}
-
- );
-}
diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/chip.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/chip.tsx
new file mode 100644
index 0000000000..17b859d9ab
--- /dev/null
+++ b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/chip.tsx
@@ -0,0 +1,158 @@
+import { X } from "lucide-react";
+import { useCallback } from "react";
+
+import type { SpeakerHintStorage } from "@hypr/store";
+import { Badge } from "@hypr/ui/components/ui/badge";
+import { Button } from "@hypr/ui/components/ui/button";
+
+import * as main from "../../../../../../../store/tinybase/main";
+import { useTabs } from "../../../../../../../store/zustand/tabs/index";
+
+export function ParticipantChip({ mappingId }: { mappingId: string }) {
+ const details = useParticipantDetails(mappingId);
+
+ const assignedHumanId = details?.humanId;
+ const sessionId = details?.sessionId;
+
+ const handleRemove = useRemoveParticipant({
+ mappingId,
+ assignedHumanId,
+ sessionId,
+ });
+
+ const handleClick = useCallback(() => {
+ if (assignedHumanId) {
+ useTabs.getState().openNew({
+ type: "contacts",
+ state: { selectedOrganization: null, selectedPerson: assignedHumanId },
+ });
+ }
+ }, [assignedHumanId]);
+
+ if (!details) {
+ return null;
+ }
+
+ const { humanName } = details;
+
+ return (
+
+ {humanName || "Unknown"}
+
+
+ );
+}
+
+function useParticipantDetails(mappingId: string) {
+ const result = main.UI.useResultRow(
+ main.QUERIES.sessionParticipantsWithDetails,
+ mappingId,
+ main.STORE_ID,
+ );
+
+ if (!result) {
+ return null;
+ }
+
+ return {
+ mappingId,
+ humanId: result.human_id as string,
+ humanName: (result.human_name as string) || "",
+ humanEmail: (result.human_email as string | undefined) || undefined,
+ humanJobTitle: (result.human_job_title as string | undefined) || undefined,
+ humanLinkedinUsername:
+ (result.human_linkedin_username as string | undefined) || undefined,
+ humanIsUser: result.human_is_user as boolean,
+ orgId: (result.org_id as string | undefined) || undefined,
+ orgName: result.org_name as string | undefined,
+ sessionId: result.session_id as string,
+ };
+}
+
+function parseHumanIdFromHintValue(value: unknown): string | undefined {
+ let data = value;
+ if (typeof value === "string") {
+ try {
+ data = JSON.parse(value);
+ } catch {
+ return undefined;
+ }
+ }
+
+ if (data && typeof data === "object" && "human_id" in data) {
+ const humanId = (data as Record).human_id;
+ return typeof humanId === "string" ? humanId : undefined;
+ }
+
+ return undefined;
+}
+
+function useRemoveParticipant({
+ mappingId,
+ assignedHumanId,
+ sessionId,
+}: {
+ mappingId: string;
+ assignedHumanId: string | undefined;
+ sessionId: string | undefined;
+}) {
+ const store = main.UI.useStore(main.STORE_ID);
+ const indexes = main.UI.useIndexes(main.STORE_ID);
+
+ return useCallback(() => {
+ if (!store) {
+ return;
+ }
+
+ if (assignedHumanId && sessionId && indexes) {
+ const hintIdsToDelete: string[] = [];
+
+ const transcriptIds = indexes.getSliceRowIds(
+ main.INDEXES.transcriptBySession,
+ sessionId,
+ );
+
+ for (const transcriptId of transcriptIds) {
+ const hintIds = indexes.getSliceRowIds(
+ main.INDEXES.speakerHintsByTranscript,
+ transcriptId,
+ );
+
+ for (const hintId of hintIds) {
+ const hint = store.getRow("speaker_hints", hintId) as
+ | SpeakerHintStorage
+ | undefined;
+ if (!hint || hint.type !== "user_speaker_assignment") {
+ continue;
+ }
+
+ const hintHumanId = parseHumanIdFromHintValue(hint.value);
+ if (hintHumanId === assignedHumanId) {
+ hintIdsToDelete.push(hintId);
+ }
+ }
+ }
+
+ for (const hintId of hintIdsToDelete) {
+ store.delRow("speaker_hints", hintId);
+ }
+ }
+
+ store.delRow("mapping_session_participant", mappingId);
+ }, [store, indexes, mappingId, assignedHumanId, sessionId]);
+}
diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/dropdown.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/dropdown.tsx
new file mode 100644
index 0000000000..1c750a423d
--- /dev/null
+++ b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/dropdown.tsx
@@ -0,0 +1,61 @@
+import { cn } from "@hypr/utils";
+
+type DropdownOption = {
+ id: string;
+ name: string;
+ isNew?: boolean;
+ email?: string;
+ orgId?: string;
+ jobTitle?: string;
+};
+
+export function ParticipantDropdown({
+ options,
+ selectedIndex,
+ onSelect,
+ onHover,
+}: {
+ options: DropdownOption[];
+ selectedIndex: number;
+ onSelect: (option: DropdownOption) => void;
+ onHover: (index: number) => void;
+}) {
+ if (options.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {options.map((option, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/index.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/index.tsx
new file mode 100644
index 0000000000..9c82602832
--- /dev/null
+++ b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/index.tsx
@@ -0,0 +1,10 @@
+import { ParticipantInput } from "./input";
+
+export function ParticipantsDisplay({ sessionId }: { sessionId: string }) {
+ return (
+
+ );
+}
diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/input.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/input.tsx
new file mode 100644
index 0000000000..dce951b133
--- /dev/null
+++ b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/input.tsx
@@ -0,0 +1,338 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+
+import { useAutoCloser } from "../../../../../../../hooks/useAutoCloser";
+import * as main from "../../../../../../../store/tinybase/main";
+import { ParticipantChip } from "./chip";
+import { ParticipantDropdown } from "./dropdown";
+
+export function ParticipantInput({ sessionId }: { sessionId: string }) {
+ const {
+ inputValue,
+ showDropdown,
+ setShowDropdown,
+ selectedIndex,
+ setSelectedIndex,
+ mappingIds,
+ dropdownOptions,
+ handleAddParticipant,
+ handleInputChange,
+ deleteLastParticipant,
+ resetInput,
+ } = useParticipantInput(sessionId);
+
+ const inputRef = useRef(null);
+ const containerRef = useAutoCloser(() => setShowDropdown(false), {
+ esc: false,
+ outside: true,
+ });
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && inputValue.trim()) {
+ e.preventDefault();
+ if (dropdownOptions.length > 0) {
+ handleAddParticipant(dropdownOptions[selectedIndex]);
+ inputRef.current?.focus();
+ }
+ } else if (e.key === "ArrowDown") {
+ e.preventDefault();
+ setSelectedIndex((prev) =>
+ prev < dropdownOptions.length - 1 ? prev + 1 : prev,
+ );
+ } else if (e.key === "ArrowUp") {
+ e.preventDefault();
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
+ } else if (e.key === "Escape") {
+ resetInput();
+ } else if (e.key === "Backspace" && !inputValue) {
+ deleteLastParticipant();
+ }
+ };
+
+ const handleSelect = (option: Candidate) => {
+ handleAddParticipant(option);
+ inputRef.current?.focus();
+ };
+
+ return (
+
+
inputRef.current?.focus()}
+ >
+ {mappingIds.map((mappingId) => (
+
+ ))}
+
+
handleInputChange(e.target.value)}
+ onKeyDown={handleKeyDown}
+ onFocus={() => setShowDropdown(true)}
+ />
+
+
+ {showDropdown && inputValue.trim() && (
+
+ )}
+
+ );
+}
+
+type Candidate = {
+ id: string;
+ name: string;
+ email?: string;
+ orgId?: string;
+ jobTitle?: string;
+ isNew?: boolean;
+};
+
+function useSessionParticipants(sessionId: string) {
+ const queries = main.UI.useQueries(main.STORE_ID);
+
+ const mappingIds = main.UI.useSliceRowIds(
+ main.INDEXES.sessionParticipantsBySession,
+ sessionId,
+ main.STORE_ID,
+ ) as string[];
+
+ const existingHumanIds = useMemo(() => {
+ if (!queries) {
+ return new Set();
+ }
+
+ const ids = new Set();
+ for (const mappingId of mappingIds) {
+ const result = queries.getResultRow(
+ main.QUERIES.sessionParticipantsWithDetails,
+ mappingId,
+ );
+ if (result?.human_id) {
+ ids.add(result.human_id as string);
+ }
+ }
+ return ids;
+ }, [mappingIds, queries]);
+
+ return { mappingIds, existingHumanIds };
+}
+
+function useCandidateSearch(
+ inputValue: string,
+ existingHumanIds: Set,
+): Candidate[] {
+ const store = main.UI.useStore(main.STORE_ID);
+ const allHumanIds = main.UI.useRowIds("humans", main.STORE_ID) as string[];
+
+ return useMemo(() => {
+ const searchLower = inputValue.toLowerCase();
+ return allHumanIds
+ .filter((humanId: string) => !existingHumanIds.has(humanId))
+ .map((humanId: string) => {
+ const human = store?.getRow("humans", humanId);
+ if (!human) {
+ return null;
+ }
+
+ const name = (human.name || "") as string;
+ const email = (human.email || "") as string;
+ const nameMatch = name.toLowerCase().includes(searchLower);
+ const emailMatch = email.toLowerCase().includes(searchLower);
+
+ if (inputValue && !nameMatch && !emailMatch) {
+ return null;
+ }
+
+ return {
+ id: humanId,
+ name,
+ email,
+ orgId: human.org_id as string | undefined,
+ jobTitle: human.job_title as string | undefined,
+ isNew: false,
+ };
+ })
+ .filter((h): h is NonNullable => h !== null);
+ }, [inputValue, allHumanIds, existingHumanIds, store]);
+}
+
+function useDropdownOptions(
+ inputValue: string,
+ candidates: Candidate[],
+): Candidate[] {
+ return useMemo(() => {
+ const showCustomOption =
+ inputValue.trim() &&
+ !candidates.some(
+ (c) => c.name.toLowerCase() === inputValue.toLowerCase(),
+ );
+
+ if (!showCustomOption) {
+ return candidates;
+ }
+
+ return [
+ {
+ id: "new",
+ name: inputValue.trim(),
+ isNew: true,
+ email: "",
+ orgId: undefined,
+ jobTitle: undefined,
+ },
+ ...candidates,
+ ];
+ }, [inputValue, candidates]);
+}
+
+function useParticipantMutations(sessionId: string, mappingIds: string[]) {
+ const store = main.UI.useStore(main.STORE_ID);
+ const userId = main.UI.useValue("user_id", main.STORE_ID);
+
+ const createHuman = useCreateHuman(userId || "");
+ const linkHumanToSession = useLinkHumanToSession(userId || "", sessionId);
+
+ const addParticipant = useCallback(
+ (option: Candidate) => {
+ if (!userId) {
+ return;
+ }
+
+ if (option.isNew) {
+ const humanId = createHuman(option.name);
+ linkHumanToSession(humanId);
+ } else {
+ linkHumanToSession(option.id);
+ }
+ },
+ [userId, createHuman, linkHumanToSession],
+ );
+
+ const deleteLastParticipant = useCallback(() => {
+ if (mappingIds.length > 0 && store) {
+ const lastMappingId = mappingIds[mappingIds.length - 1];
+ store.delRow("mapping_session_participant", lastMappingId);
+ }
+ }, [mappingIds, store]);
+
+ return { addParticipant, deleteLastParticipant };
+}
+
+function useParticipantInput(sessionId: string) {
+ const [inputValue, setInputValue] = useState("");
+ const [showDropdown, setShowDropdown] = useState(false);
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ const { mappingIds, existingHumanIds } = useSessionParticipants(sessionId);
+ const candidates = useCandidateSearch(inputValue, existingHumanIds);
+ const dropdownOptions = useDropdownOptions(inputValue, candidates);
+ const { addParticipant, deleteLastParticipant } = useParticipantMutations(
+ sessionId,
+ mappingIds,
+ );
+
+ useEffect(() => {
+ if (selectedIndex >= dropdownOptions.length && dropdownOptions.length > 0) {
+ setSelectedIndex(dropdownOptions.length - 1);
+ } else if (dropdownOptions.length === 0) {
+ setSelectedIndex(0);
+ }
+ }, [dropdownOptions.length, selectedIndex]);
+
+ const resetInput = useCallback(() => {
+ setInputValue("");
+ setShowDropdown(false);
+ setSelectedIndex(0);
+ }, []);
+
+ const handleAddParticipant = useCallback(
+ (option: Candidate) => {
+ addParticipant(option);
+ resetInput();
+ },
+ [addParticipant, resetInput],
+ );
+
+ const handleInputChange = useCallback((value: string) => {
+ setInputValue(value);
+ setShowDropdown(true);
+ setSelectedIndex(0);
+ }, []);
+
+ return {
+ inputValue,
+ showDropdown,
+ setShowDropdown,
+ selectedIndex,
+ setSelectedIndex,
+ mappingIds,
+ dropdownOptions,
+ handleAddParticipant,
+ handleInputChange,
+ deleteLastParticipant,
+ resetInput,
+ };
+}
+
+function useLinkHumanToSession(
+ userId: string,
+ sessionId: string,
+): (humanId: string) => void {
+ const linkMapping = main.UI.useSetRowCallback(
+ "mapping_session_participant",
+ () => crypto.randomUUID(),
+ (p: { humanId: string }) => ({
+ user_id: userId,
+ created_at: new Date().toISOString(),
+ session_id: sessionId,
+ human_id: p.humanId,
+ }),
+ [userId, sessionId],
+ main.STORE_ID,
+ );
+
+ return useCallback(
+ (humanId: string) => {
+ linkMapping({ humanId });
+ },
+ [linkMapping],
+ );
+}
+
+function useCreateHuman(userId: string): (name: string) => string {
+ const createHuman = main.UI.useSetRowCallback(
+ "humans",
+ (p: { name: string; humanId: string }) => p.humanId,
+ (p: { name: string; humanId: string }) => ({
+ user_id: userId,
+ created_at: new Date().toISOString(),
+ name: p.name,
+ email: "",
+ org_id: "",
+ job_title: "",
+ linkedin_username: "",
+ is_user: false,
+ memo: "",
+ }),
+ [userId],
+ main.STORE_ID,
+ );
+
+ return useCallback(
+ (name: string) => {
+ const humanId = crypto.randomUUID();
+ createHuman({ name, humanId });
+ return humanId;
+ },
+ [createHuman],
+ );
+}
diff --git a/apps/desktop/src/hooks/useAutoCloser.ts b/apps/desktop/src/hooks/useAutoCloser.ts
index 0a9e3c73a4..190286c9b4 100644
--- a/apps/desktop/src/hooks/useAutoCloser.ts
+++ b/apps/desktop/src/hooks/useAutoCloser.ts
@@ -1,5 +1,6 @@
-import { useCallback, useEffect, useRef } from "react";
+import { useCallback, useRef } from "react";
import { useHotkeys } from "react-hotkeys-hook";
+import { useOnClickOutside } from "usehooks-ts";
export function useAutoCloser(
onClose: () => void,
@@ -11,30 +12,14 @@ export function useAutoCloser(
outside?: boolean;
},
) {
- const ref = useRef(null);
+ const ref = useRef(null!);
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
useHotkeys("esc", handleClose, { enabled: esc }, [handleClose]);
-
- useEffect(() => {
- if (!outside) {
- return;
- }
-
- const handleClickOutside = (event: MouseEvent) => {
- if (ref.current && !ref.current.contains(event.target as Node)) {
- handleClose();
- }
- };
-
- document.addEventListener("mousedown", handleClickOutside);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- };
- }, [handleClose, outside]);
+ useOnClickOutside(ref, outside ? handleClose : () => {});
return ref;
}