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
@@ -0,0 +1,46 @@
{
"collection": "conversation",
"field": "recording_started_at",
"type": "timestamp",
"meta": {
"collection": "conversation",
"conditions": null,
"display": "datetime",
"display_options": {
"relative": true
},
"field": "recording_started_at",
"group": null,
"hidden": false,
"interface": "datetime",
"note": null,
"options": null,
"readonly": false,
"required": false,
"searchable": true,
"sort": 30,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "recording_started_at",
"table": "conversation",
"data_type": "timestamp with time zone",
"default_value": null,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import {
useSelectAllContextMutation,
} from "./hooks";
import { SelectAllConfirmationModal } from "./SelectAllConfirmationModal";
import { getConversationStartTime } from "./utils";

type SortOption = {
label: string;
Expand Down Expand Up @@ -623,7 +624,7 @@ const ConversationAccordionItem = ({
<div className="flex items-center justify-between gap-4">
<Text size="xs" c="gray.6" className="pl-[4px]">
{formatRelative(
new Date(conversation.created_at ?? new Date()),
new Date(getConversationStartTime(conversation) ?? new Date()),
new Date(),
)}
</Text>
Expand Down
15 changes: 13 additions & 2 deletions echo/frontend/src/components/conversation/ConversationEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,17 +301,28 @@ export const ConversationEdit = ({
)}

<Box>
<Text size="sm" c="dimmed">
<Text size="sm">
<Trans>Created on</Trans>
</Text>
<Text size="sm">
{new Date(conversation.created_at ?? new Date()).toLocaleString()}
</Text>
</Box>

{conversation.recording_started_at && (
<Box>
<Text size="sm">
<Trans>Recording started</Trans>
</Text>
<Text size="sm">
{new Date(conversation.recording_started_at).toLocaleString()}
</Text>
</Box>
)}

{conversation.duration != null && conversation.duration > 0 && (
<Box>
<Text size="sm" c="dimmed">
<Text size="sm">
<Trans>Duration</Trans>
</Text>
<Text size="sm">{formatDuration(conversation.duration)}</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
useSelectAllContextMutation,
} from "./hooks";
import { LockedTranscriptOverlay } from "./LockedTranscriptOverlay";
import { getConversationStartTime } from "./utils";

type SortOption = {
label: string;
Expand Down Expand Up @@ -123,9 +124,9 @@ const hasVerifiedArtifacts = (conversation: Conversation) =>
(artifact) => (artifact as ConversationArtifact).approved_at,
) ?? false;

const formatCreatedAt = (createdAt: string | null) => {
if (!createdAt) return t`Unknown date`;
return t`${formatDistanceToNowStrict(new Date(createdAt), {
const formatStartedAt = (startedAt: string | null) => {
if (!startedAt) return t`Unknown date`;
return t`${formatDistanceToNowStrict(new Date(startedAt), {
addSuffix: true,
})}`;
};
Expand Down Expand Up @@ -346,7 +347,7 @@ const ConversationRow = ({
</Tooltip>
)}
<Text size="xs" c="dimmed">
{formatCreatedAt(conversation.created_at)}
{formatStartedAt(getConversationStartTime(conversation))}
</Text>
{conversation.live && (
<Badge size="xs" color="red" variant="light">
Expand Down
11 changes: 11 additions & 0 deletions echo/frontend/src/components/conversation/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* When the conversation actually started, for display purposes.
*
* `recording_started_at` is set on the first captured chunk; rows that
* never got a chunk (and rows from before the column existed) fall back
* to `created_at`. Sorting stays on `created_at`.
*/
export const getConversationStartTime = (
conversation: Pick<Conversation, "created_at" | "recording_started_at">,
): string | null =>
conversation.recording_started_at ?? conversation.created_at;
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export const ParticipantConversationAudio = () => {
startRecording,
stopRecording,
isRecording,
isStarting,
recordingTime,
errored,
permissionError,
Expand Down Expand Up @@ -535,10 +536,11 @@ export const ParticipantConversationAudio = () => {
navigate(textModeUrl);
};

const handleResumeRecording = () => {
const handleResumeRecording = async () => {
const timeToResume = stoppedRecordingTime ?? 0;
// Don't clear stoppedRecordingTime here - let the useEffect do it when recording starts
startRecording(timeToResume);
const started = await startRecording(timeToResume);
if (!started) return;
// Obtain wakelock on user interaction
if (wakeLock.isSupported) {
wakeLock.obtainWakeLock();
Expand Down Expand Up @@ -632,12 +634,14 @@ export const ParticipantConversationAudio = () => {
}
};

const handleStartRecording = () => {
const handleStartRecording = async () => {
if (s3Status !== "passed") {
return;
}

startRecording();
const started = await startRecording();
if (!started) return;

posthog.capture("recording_started", {
conversation_id: conversationId,
project_id: projectId,
Expand Down Expand Up @@ -1002,7 +1006,11 @@ export const ParticipantConversationAudio = () => {
radius="md"
rightSection={<IconMicrophone />}
onClick={handleStartRecording}
loading={s3Status === "checking" || s3Status === "pending"}
loading={
isStarting ||
s3Status === "checking" ||
s3Status === "pending"
}
disabled={s3Status !== "passed"}
className="flex-grow"
{...testId("portal-audio-record-button")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "@mantine/core";
import { AxiosError } from "axios";
import posthog from "posthog-js";
import { useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useForm } from "react-hook-form";
import { useSearchParams } from "react-router";
import { z } from "zod";
Expand Down Expand Up @@ -84,21 +84,42 @@ export const ParticipantInitiateForm = ({ project }: { project: Project }) => {
const { isSuccess, isError, ...initiateConversationMutation } =
useInitiateConversationMutation();

const startConversation = (data: FormValues) => {
posthog.capture("conversation_started", {
project_id: project.id,
source: "PORTAL_AUDIO",
});
initiateConversationMutation.mutate({
name: data.name ?? t`Participant`,
email: data.email || undefined,
pin: "",
projectId: project.id,
source: "PORTAL_AUDIO",
tagIdList: data.tagIdList,
visitorId: getVisitorId(project.id),
});
};
// Re-entrancy latch: isPending flips too late to block a fast double click
// or a StrictMode double-invoke of the auto-submit effect.
const hasInitiatedRef = useRef(false);

const { mutate: initiateConversation } = initiateConversationMutation;

const startConversation = useCallback(
(data: FormValues) => {
if (hasInitiatedRef.current) return;
hasInitiatedRef.current = true;

posthog.capture("conversation_started", {
project_id: project.id,
source: "PORTAL_AUDIO",
});
initiateConversation(
{
// `??` not `||`: an empty name is intentional when the project does
// not ask for one, and keeps the dashboard's auto-title fallback
name: data.name ?? t`Participant`,
email: data.email || undefined,
pin: "",
projectId: project.id,
source: "PORTAL_AUDIO",
tagIdList: data.tagIdList,
visitorId: getVisitorId(project.id),
},
{
onError: () => {
hasInitiatedRef.current = false;
},
},
);
},
[project.id, initiateConversation],
);

// Auto-submit if skipOnboarding is requested and we have required fields prefilled
useEffect(() => {
Expand All @@ -113,18 +134,14 @@ export const ParticipantInitiateForm = ({ project }: { project: Project }) => {
!isSuccess &&
!isError
) {
initiateConversationMutation.mutate({
// startConversation owns the latch, the payload, and the analytics
startConversation({
name: defaultName || t`Participant`,
email: defaultEmail || undefined,
pin: "",
projectId: project.id,
source: "PORTAL_AUDIO",
email: defaultEmail,
tagIdList: defaultTagIdList,
visitorId: getVisitorId(project.id),
});
}
}, [
project.id,
project.default_conversation_ask_for_participant_name,
defaultName,
defaultEmail,
Expand All @@ -133,7 +150,7 @@ export const ParticipantInitiateForm = ({ project }: { project: Project }) => {
isError,
searchParams,
initiateConversationMutation.isPending,
initiateConversationMutation.mutate,
startConversation,
]);

useEffect(() => {
Expand All @@ -153,6 +170,8 @@ export const ParticipantInitiateForm = ({ project }: { project: Project }) => {
`/${project.id}/conversation/${initiateConversationMutation.data?.id}${pathSuffix}${queryStr}`,
);
} else {
// release the latch so Continue works again
hasInitiatedRef.current = false;
reset();
}
}
Expand Down
Loading
Loading