@marktiderman/genesis-chat 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -7
- package/dist/{chunk-5D5A7I4Q.js → chunk-A4YJX7KT.js} +130 -16
- package/dist/chunk-A4YJX7KT.js.map +1 -0
- package/dist/chunk-AZS3HNLM.js +30 -0
- package/dist/chunk-AZS3HNLM.js.map +1 -0
- package/dist/{chunk-7S6HYNWW.js → chunk-GBJ3YI4E.js} +72 -5
- package/dist/chunk-GBJ3YI4E.js.map +1 -0
- package/dist/{chunk-PZTOY2LY.js → chunk-JU2FAPBI.js} +46 -13
- package/dist/chunk-JU2FAPBI.js.map +1 -0
- package/dist/{client-DMa2j7kz.d.ts → client-DUIG1hum.d.ts} +36 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/native.d.ts +36 -7
- package/dist/native.js +239 -116
- package/dist/native.js.map +1 -1
- package/dist/{outbox-B0y3phE0.d.ts → outbox-B9NAWQ58.d.ts} +74 -6
- package/dist/push.d.ts +2 -2
- package/dist/react.d.ts +48 -21
- package/dist/react.js +3 -3
- package/dist/server.d.ts +43 -2
- package/dist/server.js +115 -15
- package/dist/server.js.map +1 -1
- package/dist/{thread-DWXHKPdw.d.ts → thread-oPra7pkW.d.ts} +1 -1
- package/dist/web.d.ts +31 -11
- package/dist/web.js +97 -48
- package/dist/web.js.map +1 -1
- package/migrations/0003_genesis_chat_search_threads.sql +85 -0
- package/migrations/tests/0002_genesis_chat_coaching.test.sql +7 -3
- package/migrations/tests/0003_genesis_chat_search_threads.test.sql +50 -0
- package/package.json +7 -3
- package/src/__tests__/coaching-core.test.ts +3 -1
- package/src/__tests__/coaching-react.test.tsx +73 -2
- package/src/__tests__/coaching-web.test.tsx +78 -6
- package/src/__tests__/fake-xhr.ts +54 -0
- package/src/__tests__/outbox-hosted.test.ts +269 -0
- package/src/__tests__/review-279.test.ts +41 -0
- package/src/__tests__/review-fixes-web.test.tsx +13 -10
- package/src/__tests__/server-fake-video.test.ts +81 -0
- package/src/__tests__/server-fakes.ts +7 -1
- package/src/__tests__/server-safety.test.ts +17 -4
- package/src/__tests__/web.test.tsx +16 -0
- package/src/core/client.ts +28 -2
- package/src/core/outbox.ts +222 -16
- package/src/core/transfer.ts +71 -0
- package/src/core/types.ts +17 -0
- package/src/index.ts +1 -0
- package/src/native/__tests__/emoji.test.tsx +16 -0
- package/src/native/__tests__/organise-screens.test.tsx +80 -0
- package/src/native/__tests__/video-media.test.tsx +81 -4
- package/src/native/attachment-cards.tsx +22 -6
- package/src/native/inbox-screen.tsx +12 -6
- package/src/native/index.ts +2 -2
- package/src/native/message-actions-sheet.tsx +10 -5
- package/src/native/message-bubble.tsx +3 -11
- package/src/native/modules.ts +2 -0
- package/src/native/schedule-sheet.tsx +23 -9
- package/src/native/search-screen.tsx +26 -5
- package/src/native/thread-screen.tsx +30 -3
- package/src/native/topics.tsx +70 -15
- package/src/native/video-player.tsx +46 -6
- package/src/native/video-recorder.tsx +6 -3
- package/src/native/voice-player.tsx +18 -0
- package/src/react/index.tsx +103 -19
- package/src/react/previews.ts +51 -0
- package/src/server/fake-video.ts +171 -0
- package/src/server/index.ts +1 -0
- package/src/web/attachments.tsx +6 -6
- package/src/web/bubble.tsx +22 -0
- package/src/web/inbox.tsx +7 -6
- package/src/web/memory-client.ts +11 -3
- package/src/web/panel.tsx +2 -2
- package/src/web/schedule.tsx +14 -2
- package/src/web/thread.tsx +36 -8
- package/src/web/video.tsx +28 -34
- package/dist/chunk-5D5A7I4Q.js.map +0 -1
- package/dist/chunk-7S6HYNWW.js.map +0 -1
- package/dist/chunk-PZTOY2LY.js.map +0 -1
package/dist/web.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ComponentType, ReactNode, Ref, Component, ErrorInfo } from 'react';
|
|
3
3
|
import { SlotOverride } from '@marktiderman/genesis-core/slots';
|
|
4
|
-
import { q as ChatMessage, m as ChatInboxItem, o as ChatLinkPreview$1, h as ChatAttachment, k as ChatConversation, p as ChatMember, t as ChatRole, i as ChatClient, g as ChatActivity, C as ChatAvailability, x as ChatTopic, T as TopicInput, u as ChatSearchHit,
|
|
4
|
+
import { q as ChatMessage, m as ChatInboxItem, o as ChatLinkPreview$1, h as ChatAttachment, k as ChatConversation, p as ChatMember, t as ChatRole, i as ChatClient, g as ChatActivity, C as ChatAvailability, x as ChatTopic, T as TopicInput, u as ChatSearchHit, F as SendInput, A as AttachmentInput } from './client-DUIG1hum.js';
|
|
5
5
|
import { ChatAttachmentKind, ConversationState, ChatVideoUpload } from './react.js';
|
|
6
|
-
import { T as ThreadItem } from './thread-
|
|
7
|
-
import { L as LocalFile } from './outbox-
|
|
6
|
+
import { T as ThreadItem } from './thread-oPra7pkW.js';
|
|
7
|
+
import { L as LocalFile } from './outbox-B9NAWQ58.js';
|
|
8
8
|
import '@supabase/supabase-js';
|
|
9
9
|
import '@tanstack/query-core';
|
|
10
10
|
|
|
@@ -209,8 +209,16 @@ interface ChatThreadProps {
|
|
|
209
209
|
threadRootId?: string;
|
|
210
210
|
/** One topic's messages. */
|
|
211
211
|
topic?: string;
|
|
212
|
-
/**
|
|
213
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Shown on messages that have replies, to open their thread. Also called when a search result
|
|
214
|
+
* is a reply in a thread, with `jumpTo` (that reply) for the thread's own ChatThread.
|
|
215
|
+
*/
|
|
216
|
+
onOpenThread?: (message: ChatMessage, opts?: {
|
|
217
|
+
jumpTo?: {
|
|
218
|
+
messageId: string;
|
|
219
|
+
seq: number;
|
|
220
|
+
};
|
|
221
|
+
}) => void;
|
|
214
222
|
/** The quick reactions; 👍 ❤️ 😂 😮 😢 🙏 by default. */
|
|
215
223
|
quickReactions?: readonly string[];
|
|
216
224
|
placeholder?: string;
|
|
@@ -220,6 +228,7 @@ interface ChatThreadProps {
|
|
|
220
228
|
jumpTo?: {
|
|
221
229
|
messageId: string;
|
|
222
230
|
seq?: number;
|
|
231
|
+
threadRootId?: string | null;
|
|
223
232
|
} | null;
|
|
224
233
|
/** A search button in the header. Default true. */
|
|
225
234
|
search?: boolean;
|
|
@@ -508,7 +517,7 @@ interface ChatScheduledListProps {
|
|
|
508
517
|
conversationTitle?: (conversationId: string) => string;
|
|
509
518
|
className?: string;
|
|
510
519
|
}
|
|
511
|
-
/** Your scheduled messages, soonest first, each with when it goes and Cancel. */
|
|
520
|
+
/** Your scheduled messages, soonest first, each with when it goes and Cancel (Remove for one that failed). */
|
|
512
521
|
declare function ChatScheduledList({ conversationId, conversationTitle, className }: ChatScheduledListProps): react.JSX.Element;
|
|
513
522
|
|
|
514
523
|
interface ChatAwayBannerProps {
|
|
@@ -643,12 +652,18 @@ declare function useVideoRecorder({ maxMs, onTick }: UseVideoRecorderOptions): {
|
|
|
643
652
|
handOff: () => void;
|
|
644
653
|
};
|
|
645
654
|
type VideoRecorder = ReturnType<typeof useVideoRecorder>;
|
|
646
|
-
/**
|
|
647
|
-
|
|
655
|
+
/**
|
|
656
|
+
* A recording as a file for the outbox: `hosted` sends it to the hosted provider (the provider's
|
|
657
|
+
* `videoUpload`, the message going at once with the video pending); otherwise it goes to storage.
|
|
658
|
+
*/
|
|
659
|
+
declare function videoFile(rec: VideoRecording, opts?: {
|
|
660
|
+
hosted?: boolean;
|
|
661
|
+
}): LocalFile;
|
|
648
662
|
/**
|
|
649
663
|
* Uploads a recording through the app's `videoUpload` (a Mux direct upload: PUT the bytes to
|
|
650
664
|
* the URL it returns) and gives back the attachment to send, pending until Mux's webhook marks
|
|
651
|
-
* it ready.
|
|
665
|
+
* it ready. The composer no longer uses it: it sends `videoFile(rec, { hosted: true })` through
|
|
666
|
+
* the outbox, so the message keeps its place in the order while the bytes go up.
|
|
652
667
|
*/
|
|
653
668
|
declare function uploadVideo(videoUpload: ChatVideoUpload, conversationId: string, rec: Pick<VideoRecording, "blob" | "contentType" | "durationMs" | "name">, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<AttachmentInput>;
|
|
654
669
|
interface ChatVideoRecorderProps {
|
|
@@ -660,14 +675,19 @@ interface ChatVideoRecorderProps {
|
|
|
660
675
|
send: (input: Omit<SendInput, "conversationId" | "clientId">, files?: LocalFile[]) => Promise<unknown>;
|
|
661
676
|
/** Merged into what's sent (a reply, a topic, an internal note). */
|
|
662
677
|
extra?: Omit<SendInput, "conversationId" | "clientId" | "attachments" | "body">;
|
|
678
|
+
/**
|
|
679
|
+
* Marks the take as hosted. Pass the same `videoUpload` the `ChatProvider` has (or give the
|
|
680
|
+
* provider a `hostedUpload`): the outbox does the upload, and without one it falls back to
|
|
681
|
+
* storage with no error.
|
|
682
|
+
*/
|
|
663
683
|
videoUpload?: ChatVideoUpload;
|
|
664
684
|
onTick?: () => void;
|
|
665
685
|
onSent?: () => void;
|
|
666
|
-
/**
|
|
686
|
+
/** @deprecated Unused: the bytes go through the outbox's `hostedUpload` now. */
|
|
667
687
|
fetchImpl?: typeof fetch;
|
|
668
688
|
}
|
|
669
689
|
/** The composer's video dialog: preview, record (to the role's limit), review, retake, send. */
|
|
670
|
-
declare function ChatVideoRecorder({ open, onOpenChange,
|
|
690
|
+
declare function ChatVideoRecorder({ open, onOpenChange, maxMs, send, extra, videoUpload, onTick, onSent }: ChatVideoRecorderProps): react.JSX.Element;
|
|
671
691
|
declare const muxStreamUrl: (playbackId: string) => string;
|
|
672
692
|
declare const muxPosterUrl: (playbackId: string) => string;
|
|
673
693
|
interface ChatVideoPlayerProps {
|
package/dist/web.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { attachmentPreview } from './chunk-AZS3HNLM.js';
|
|
1
2
|
import { snippetParts, isHttpsUrl, scheduleQuickPicks, isSchedulable, scheduleLabel, extractLinks, topicKey } from './chunk-BJCE5O7V.js';
|
|
2
|
-
import { useChat, useInbox, useSearch, useScheduled, useAvailability, useTopics, useConversation, useMessageActions, usePins, useFileUrls, useLinkPreviews, useBroadcast } from './chunk-
|
|
3
|
-
import { previewText, summarizeReactions, activityLine, itemSender, startsDay, dayLabel, itemTime, startsGroup, parseMentions } from './chunk-
|
|
4
|
-
import { fileExtension, formatDuration, nextPlaybackRate, awaySummary, awayLine, WEEKDAYS, CHAT_LIMITS, waveformPeaks, recordingLimitMs, isAllowedExtension, attachmentKindFor, randomId, ChatError } from './chunk-
|
|
3
|
+
import { useChat, useInbox, useSearch, useScheduled, useAvailability, useTopics, useConversation, useMessageActions, usePins, useFileUrls, useLinkPreviews, useBroadcast } from './chunk-JU2FAPBI.js';
|
|
4
|
+
import { previewText, summarizeReactions, activityLine, itemSender, startsDay, dayLabel, itemTime, startsGroup, parseMentions } from './chunk-A4YJX7KT.js';
|
|
5
|
+
import { fileExtension, formatDuration, nextPlaybackRate, awaySummary, awayLine, WEEKDAYS, CHAT_LIMITS, waveformPeaks, recordingLimitMs, isAllowedExtension, attachmentKindFor, randomId, ChatError } from './chunk-GBJ3YI4E.js';
|
|
5
6
|
import { createContext, useRef, useState, Fragment, Component, useCallback, useEffect, useMemo, useImperativeHandle, useLayoutEffect, useContext } from 'react';
|
|
6
7
|
import { MessageSquare, Pin, BellOff, AtSign, Search, X, Mic, ClipboardList, ExternalLink, VideoOff, Video, RotateCcw, SendHorizontal, Square, Circle, Pause, Play, Paperclip, ChevronLeft, ChevronRight, FileText, Download, Lock, Ban, EyeOff, ShieldAlert, AlertCircle, Clock, Reply, Copy, Pencil, PinOff, Bookmark, Flag, Trash2, SmilePlus, Plus, MoreHorizontal, ChevronUp, CalendarClock, Moon, RefreshCw, Smile, Hash, LockOpen, ArrowDown, ImagePlus, Megaphone, Link2, ArrowLeft } from 'lucide-react';
|
|
7
|
-
import { Skeleton, cn, EmptyState, Button, UserAvatar, Badge, Input, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Alert, AlertDescription, Spinner, Collapsible, CollapsibleTrigger, CollapsibleContent, VisuallyHidden, ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuSeparator, ContextMenuItem, Tooltip, Textarea, Popover, PopoverTrigger, PopoverContent, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuLabel, Label, DialogFooter, toast, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Toggle, ToggleGroup, ToggleGroupItem, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, RadioGroup, RadioGroupItem, Split, Checkbox } from '@marktiderman/genesis-ui';
|
|
8
|
+
import { Skeleton, cn, EmptyState, Button, UserAvatar, Badge, Input, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Alert, AlertDescription, Spinner, Collapsible, CollapsibleTrigger, CollapsibleContent, VisuallyHidden, Progress, ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuSeparator, ContextMenuItem, Tooltip, Textarea, Popover, PopoverTrigger, PopoverContent, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuLabel, Label, DialogFooter, toast, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Toggle, ToggleGroup, ToggleGroupItem, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, RadioGroup, RadioGroupItem, Split, Checkbox } from '@marktiderman/genesis-ui';
|
|
8
9
|
import { renderSlot } from '@marktiderman/genesis-core/slots';
|
|
9
10
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
10
11
|
import { EmojiPicker } from 'frimousse';
|
|
@@ -58,7 +59,7 @@ function inboxTitle(item, resolveUser, fallback) {
|
|
|
58
59
|
return fallback?.(item) ?? "Conversation";
|
|
59
60
|
}
|
|
60
61
|
function ChatInbox({ resolveUser, onSelect, selectedId, kind, archived = false, fallbackTitle, slots, className, ...rest }) {
|
|
61
|
-
const { userId: me } = useChat();
|
|
62
|
+
const { userId: me, attachmentKinds } = useChat();
|
|
62
63
|
const { items, isLoading, error, refetch } = useInbox({ kind, archived });
|
|
63
64
|
const list = useRef(null);
|
|
64
65
|
const onKeyDown = (e) => {
|
|
@@ -92,14 +93,14 @@ function ChatInbox({ resolveUser, onSelect, selectedId, kind, archived = false,
|
|
|
92
93
|
if (items.length === 0) {
|
|
93
94
|
return /* @__PURE__ */ jsx("div", { className, children: renderSlot(DefaultEmpty, slots?.empty, { archived }) });
|
|
94
95
|
}
|
|
95
|
-
return /* @__PURE__ */ jsx("nav", { "aria-label": rest["aria-label"] ?? "Conversations", className: cn("min-h-0", className), "data-testid": "genesis-chat-inbox", children: /* @__PURE__ */ jsx("ul", { ref: list, className: "flex flex-col", onKeyDown, children: items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(InboxRow, { item, me, resolveUser, fallbackTitle, selected: item.conversationId === selectedId, onSelect }) }, item.conversationId)) }) });
|
|
96
|
+
return /* @__PURE__ */ jsx("nav", { "aria-label": rest["aria-label"] ?? "Conversations", className: cn("min-h-0", className), "data-testid": "genesis-chat-inbox", children: /* @__PURE__ */ jsx("ul", { ref: list, className: "flex flex-col", onKeyDown, children: items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(InboxRow, { item, me, kinds: attachmentKinds, resolveUser, fallbackTitle, selected: item.conversationId === selectedId, onSelect }) }, item.conversationId)) }) });
|
|
96
97
|
}
|
|
97
|
-
function InboxRow({ item, me, resolveUser, fallbackTitle, selected, onSelect }) {
|
|
98
|
+
function InboxRow({ item, me, kinds, resolveUser, fallbackTitle, selected, onSelect }) {
|
|
98
99
|
const title = inboxTitle(item, resolveUser, fallbackTitle);
|
|
99
100
|
const other = item.otherUserId ? resolveUser(item.otherUserId) : null;
|
|
100
101
|
const last = item.lastMessage;
|
|
101
102
|
const unread = item.unread > 0;
|
|
102
|
-
let preview = last ?
|
|
103
|
+
let preview = last ? attachmentPreview(last.body, last.attachment, kinds, last.deleted) : "";
|
|
103
104
|
if (last && last.kind !== "system" && !last.deleted && last.senderId) {
|
|
104
105
|
if (last.senderId === me) preview = `You: ${preview}`;
|
|
105
106
|
else if (!item.otherUserId && item.memberCount > 2) preview = `${resolveUser(last.senderId).name}: ${preview}`;
|
|
@@ -646,13 +647,14 @@ function useVideoRecorder({ maxMs, onTick }) {
|
|
|
646
647
|
useEffect(() => () => reset(), [reset]);
|
|
647
648
|
return { state, error, elapsedMs, maxMs, stream, recording, open, start, stop, retake, reset, handOff };
|
|
648
649
|
}
|
|
649
|
-
function videoFile(rec) {
|
|
650
|
+
function videoFile(rec, opts = {}) {
|
|
650
651
|
return {
|
|
651
652
|
data: rec.blob,
|
|
652
653
|
name: rec.name,
|
|
653
654
|
contentType: rec.contentType,
|
|
654
655
|
previewUrl: rec.url ?? void 0,
|
|
655
|
-
meta: { kind: "video", durationMs: rec.durationMs, bytes: rec.blob.size }
|
|
656
|
+
meta: { kind: "video", durationMs: rec.durationMs, bytes: rec.blob.size },
|
|
657
|
+
...opts.hosted ? { hosted: true } : {}
|
|
656
658
|
};
|
|
657
659
|
}
|
|
658
660
|
async function uploadVideo(videoUpload, conversationId, rec, fetchImpl = fetch, signal) {
|
|
@@ -683,48 +685,36 @@ function Preview({ stream }) {
|
|
|
683
685
|
}, [stream]);
|
|
684
686
|
return /* @__PURE__ */ jsx("video", { ref, autoPlay: true, muted: true, playsInline: true, className: "h-full w-full -scale-x-100 object-cover", "aria-label": "Camera preview", "data-testid": "genesis-chat-video-preview" });
|
|
685
687
|
}
|
|
686
|
-
function ChatVideoRecorder({ open, onOpenChange,
|
|
688
|
+
function ChatVideoRecorder({ open, onOpenChange, maxMs, send, extra, videoUpload, onTick, onSent }) {
|
|
687
689
|
const rec = useVideoRecorder({ maxMs, onTick });
|
|
688
690
|
const [sending, setSending] = useState(false);
|
|
689
691
|
const [sendError, setSendError] = useState(null);
|
|
690
|
-
const
|
|
692
|
+
const sendingNow = useRef(false);
|
|
691
693
|
const { open: openCamera, reset } = rec;
|
|
692
694
|
useEffect(() => {
|
|
693
695
|
if (open) void openCamera();
|
|
694
696
|
else reset();
|
|
695
697
|
}, [open, openCamera, reset]);
|
|
696
|
-
useEffect(() => () => upload.current?.abort(), []);
|
|
697
698
|
const submit = async () => {
|
|
698
699
|
const take = rec.recording;
|
|
699
|
-
if (!take ||
|
|
700
|
-
|
|
701
|
-
upload.current = controller;
|
|
700
|
+
if (!take || sendingNow.current) return;
|
|
701
|
+
sendingNow.current = true;
|
|
702
702
|
setSending(true);
|
|
703
703
|
setSendError(null);
|
|
704
704
|
try {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (controller.signal.aborted) return;
|
|
708
|
-
await send({ ...extra, body: "", attachments: [attachment2] });
|
|
709
|
-
} else {
|
|
710
|
-
await send({ ...extra, body: "" }, [videoFile(take)]);
|
|
711
|
-
rec.handOff();
|
|
712
|
-
}
|
|
705
|
+
await send({ ...extra, body: "" }, [videoFile(take, { hosted: Boolean(videoUpload) })]);
|
|
706
|
+
rec.handOff();
|
|
713
707
|
onSent?.();
|
|
714
708
|
onOpenChange(false);
|
|
715
709
|
} catch (e) {
|
|
716
|
-
|
|
710
|
+
setSendError(e instanceof Error ? e.message : "The video didn't send");
|
|
717
711
|
} finally {
|
|
718
|
-
|
|
712
|
+
sendingNow.current = false;
|
|
719
713
|
setSending(false);
|
|
720
714
|
}
|
|
721
715
|
};
|
|
722
|
-
const close = (o) => {
|
|
723
|
-
if (!o) upload.current?.abort();
|
|
724
|
-
onOpenChange(o);
|
|
725
|
-
};
|
|
726
716
|
const recording = rec.state === "recording";
|
|
727
|
-
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange:
|
|
717
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (o) => (o || !sendingNow.current) && onOpenChange(o), children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-lg", "data-testid": "genesis-chat-video-recorder", children: [
|
|
728
718
|
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
729
719
|
/* @__PURE__ */ jsx(DialogTitle, { children: "Record a video" }),
|
|
730
720
|
/* @__PURE__ */ jsxs(DialogDescription, { children: [
|
|
@@ -750,8 +740,7 @@ function ChatVideoRecorder({ open, onOpenChange, conversationId, maxMs, send, ex
|
|
|
750
740
|
] }) : null,
|
|
751
741
|
sending ? /* @__PURE__ */ jsxs("span", { className: "absolute inset-0 flex flex-col items-center justify-center gap-2 bg-background/70 text-sm font-medium", role: "status", children: [
|
|
752
742
|
/* @__PURE__ */ jsx(Spinner, {}),
|
|
753
|
-
"
|
|
754
|
-
videoUpload ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => upload.current?.abort(), "data-testid": "genesis-chat-video-cancel-upload", children: "Cancel upload" }) : null
|
|
743
|
+
" Sending\u2026"
|
|
755
744
|
] }) : null
|
|
756
745
|
] }),
|
|
757
746
|
/* @__PURE__ */ jsx("div", { className: "sr-only", role: "status", children: recording ? "Recording" : rec.state === "recorded" ? "Recorded. Watch it, retake it or send it." : "" }),
|
|
@@ -1200,12 +1189,12 @@ function attachmentKindOf(kinds, kind) {
|
|
|
1200
1189
|
const k = kinds[kind];
|
|
1201
1190
|
return k && typeof k.title === "function" ? k : null;
|
|
1202
1191
|
}
|
|
1203
|
-
function attachmentCardOf(k, a) {
|
|
1192
|
+
function attachmentCardOf(k, a, ctx = { mine: false }) {
|
|
1204
1193
|
try {
|
|
1205
|
-
const title = k.title(a);
|
|
1194
|
+
const title = k.title(a, ctx);
|
|
1206
1195
|
if (typeof title !== "string") return null;
|
|
1207
|
-
const sub = typeof k.subtitle === "function" ? k.subtitle(a) : null;
|
|
1208
|
-
const act = typeof k.action === "function" ? k.action(a) : null;
|
|
1196
|
+
const sub = typeof k.subtitle === "function" ? k.subtitle(a, ctx) : null;
|
|
1197
|
+
const act = typeof k.action === "function" ? k.action(a, ctx) : null;
|
|
1209
1198
|
const action = act && typeof act === "object" && typeof act.label === "string" && (typeof act.href === "string" || typeof act.onPress === "function") ? { label: act.label, href: typeof act.href === "string" ? act.href : void 0, onPress: typeof act.onPress === "function" ? act.onPress : void 0 } : null;
|
|
1210
1199
|
return { title, subtitle: typeof sub === "string" ? sub : null, action };
|
|
1211
1200
|
} catch (e) {
|
|
@@ -1246,7 +1235,7 @@ function MessageAttachments({ views, message, mine, tone, slot, kinds }) {
|
|
|
1246
1235
|
const generic = /* @__PURE__ */ jsx(GenericAttachment, { attachment: v.attachment, message, url: v.url, mine });
|
|
1247
1236
|
const k = attachmentKindOf(kinds, v.kind);
|
|
1248
1237
|
if (k) {
|
|
1249
|
-
const card = attachmentCardOf(k, v.attachment);
|
|
1238
|
+
const card = attachmentCardOf(k, v.attachment, { mine });
|
|
1250
1239
|
return card ? /* @__PURE__ */ jsx(ChatAttachmentCard, { ...card, tone }, v.key) : /* @__PURE__ */ jsx(GenericAttachment, { attachment: v.attachment, message, url: v.url, mine }, v.key);
|
|
1251
1240
|
}
|
|
1252
1241
|
return Custom ? /* @__PURE__ */ jsx(ChatErrorBoundary, { fallback: generic, children: /* @__PURE__ */ jsx(Custom, { attachment: v.attachment, message, url: v.url, mine }) }, v.key) : null;
|
|
@@ -1359,6 +1348,12 @@ function pendingViews(p) {
|
|
|
1359
1348
|
pending: p.status === "sending"
|
|
1360
1349
|
}));
|
|
1361
1350
|
}
|
|
1351
|
+
function pendingProgress(p) {
|
|
1352
|
+
if (p.status !== "sending") return null;
|
|
1353
|
+
const files = p.files ?? [];
|
|
1354
|
+
if (!files.some((f) => typeof f.progress === "number")) return null;
|
|
1355
|
+
return Math.round(files.reduce((sum, f) => sum + (f.progress ?? 0), 0) / files.length * 100);
|
|
1356
|
+
}
|
|
1362
1357
|
function returnFocus(e, keepFocus) {
|
|
1363
1358
|
const next = keepFocus.current;
|
|
1364
1359
|
if (next) {
|
|
@@ -1385,6 +1380,7 @@ function MessageRow({ item, startsGroup: startsGroup2, className }) {
|
|
|
1385
1380
|
const showHeader = ctx.isGroup && !mine && startsGroup2;
|
|
1386
1381
|
const tone = mine && !note && !deleted ? "primary" : "default";
|
|
1387
1382
|
const views = pending ? pendingViews(pending) : message.attachments.map((a) => toAttachmentView(a, ctx.urls));
|
|
1383
|
+
const progress = pending ? pendingProgress(pending) : null;
|
|
1388
1384
|
const editing = ctx.editingId === message.id;
|
|
1389
1385
|
const reactions = pending ? [] : summarizeReactions(message, ctx.me);
|
|
1390
1386
|
const highlighted = ctx.highlightedId === message.id;
|
|
@@ -1417,7 +1413,22 @@ function MessageRow({ item, startsGroup: startsGroup2, className }) {
|
|
|
1417
1413
|
" Message deleted"
|
|
1418
1414
|
] }) : editing ? /* @__PURE__ */ jsx(EditBox, { message }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1419
1415
|
message.replyToId ? /* @__PURE__ */ jsx(ReplyQuote, { id: message.replyToId, tone }) : null,
|
|
1420
|
-
views.length ? /* @__PURE__ */
|
|
1416
|
+
views.length ? /* @__PURE__ */ jsxs("div", { className: cn(message.body ? "mb-1.5" : ""), children: [
|
|
1417
|
+
/* @__PURE__ */ jsx(MessageAttachments, { views, message: pending ? void 0 : message, mine, tone, slot: ctx.attachmentSlot, kinds: ctx.attachmentKinds }),
|
|
1418
|
+
progress !== null ? /* @__PURE__ */ jsx(
|
|
1419
|
+
Progress,
|
|
1420
|
+
{
|
|
1421
|
+
value: progress,
|
|
1422
|
+
className: cn("mt-1.5 h-1", tone === "primary" && "bg-primary-foreground/30 [&>div]:bg-primary-foreground"),
|
|
1423
|
+
role: "progressbar",
|
|
1424
|
+
"aria-label": "Uploading",
|
|
1425
|
+
"aria-valuemin": 0,
|
|
1426
|
+
"aria-valuemax": 100,
|
|
1427
|
+
"aria-valuenow": progress,
|
|
1428
|
+
"data-testid": "genesis-chat-upload-progress"
|
|
1429
|
+
}
|
|
1430
|
+
) : null
|
|
1431
|
+
] }) : null,
|
|
1421
1432
|
message.body ? /* @__PURE__ */ jsx(ChatMarkdown, { text: message.body, me: ctx.me, tone }) : null,
|
|
1422
1433
|
preview ? /* @__PURE__ */ jsx(ChatLinkPreview, { preview, images: ctx.linkPreviewImages, proxyUrl: ctx.linkPreviewProxyUrl, tone }) : null,
|
|
1423
1434
|
hidden && mine && !observer ? /* @__PURE__ */ jsxs("span", { className: "mt-1 flex items-center gap-1 text-xs font-medium", "data-testid": "genesis-chat-hidden-notice", children: [
|
|
@@ -1830,7 +1841,22 @@ function ChatScheduledList({ conversationId, conversationTitle, className }) {
|
|
|
1830
1841
|
/* @__PURE__ */ jsx("div", { className: "truncate text-sm", children: previewText(s.body, s.attachments[0]?.kind ?? null) }),
|
|
1831
1842
|
s.error ? /* @__PURE__ */ jsx("div", { className: "text-xs text-destructive", children: s.error }) : null
|
|
1832
1843
|
] }),
|
|
1833
|
-
s.status === "
|
|
1844
|
+
s.status === "failed" ? /* @__PURE__ */ jsxs(
|
|
1845
|
+
Button,
|
|
1846
|
+
{
|
|
1847
|
+
type: "button",
|
|
1848
|
+
variant: "ghost",
|
|
1849
|
+
size: "sm",
|
|
1850
|
+
className: "h-7 shrink-0 gap-1 text-xs",
|
|
1851
|
+
onClick: () => void cancel(s.id).then(() => toast.success("Removed"), () => toast.error("Couldn't remove it")),
|
|
1852
|
+
"aria-label": "Remove the message that couldn't send",
|
|
1853
|
+
"data-testid": "genesis-chat-scheduled-remove",
|
|
1854
|
+
children: [
|
|
1855
|
+
/* @__PURE__ */ jsx(X, { className: "size-3.5", "aria-hidden": true }),
|
|
1856
|
+
" Remove"
|
|
1857
|
+
]
|
|
1858
|
+
}
|
|
1859
|
+
) : s.status === "scheduled" ? /* @__PURE__ */ jsxs(
|
|
1834
1860
|
Button,
|
|
1835
1861
|
{
|
|
1836
1862
|
type: "button",
|
|
@@ -2799,7 +2825,7 @@ function ChatThread({
|
|
|
2799
2825
|
slots,
|
|
2800
2826
|
className
|
|
2801
2827
|
}) {
|
|
2802
|
-
const { userId: me, attachmentKinds } = useChat();
|
|
2828
|
+
const { userId: me, attachmentKinds, client: chatClient } = useChat();
|
|
2803
2829
|
const topicsState = useTopics(threadRootId ? null : conversationId);
|
|
2804
2830
|
const [chosenTopic, setChosenTopic] = useState(null);
|
|
2805
2831
|
const topic = topicProp ?? chosenTopic ?? void 0;
|
|
@@ -2940,9 +2966,30 @@ function ChatThread({
|
|
|
2940
2966
|
setHighlightedId(id);
|
|
2941
2967
|
setTimeout(() => setHighlightedId((h) => h === id ? null : h), 1600);
|
|
2942
2968
|
}, []);
|
|
2943
|
-
const
|
|
2969
|
+
const reach = useCallback(
|
|
2970
|
+
(target) => {
|
|
2971
|
+
const root = target.threadRootId;
|
|
2972
|
+
if (!root || root === threadRootId || root === target.messageId) {
|
|
2973
|
+
setPendingJump({ messageId: target.messageId, seq: target.seq, phase: "new" });
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
void chatClient.message(root).then(
|
|
2977
|
+
(m) => {
|
|
2978
|
+
if (!m) {
|
|
2979
|
+
toast.info("That message isn't here any more");
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
onOpenThread?.(m, target.seq != null ? { jumpTo: { messageId: target.messageId, seq: target.seq } } : void 0);
|
|
2983
|
+
setPendingJump({ messageId: m.id, seq: m.seq, phase: "new" });
|
|
2984
|
+
},
|
|
2985
|
+
(e) => toast.error(errorText(e))
|
|
2986
|
+
);
|
|
2987
|
+
},
|
|
2988
|
+
[chatClient, onOpenThread, threadRootId]
|
|
2989
|
+
);
|
|
2990
|
+
const [pendingJump, setPendingJump] = useState(jumpToProp && !jumpToProp.threadRootId ? { ...jumpToProp, phase: "new" } : null);
|
|
2944
2991
|
useEffect(() => {
|
|
2945
|
-
if (jumpToProp)
|
|
2992
|
+
if (jumpToProp) reach(jumpToProp);
|
|
2946
2993
|
}, [jumpToProp?.messageId, jumpToProp?.seq]);
|
|
2947
2994
|
const { loadUntil } = state;
|
|
2948
2995
|
useEffect(() => {
|
|
@@ -2970,9 +3017,9 @@ function ChatThread({
|
|
|
2970
3017
|
(hit) => {
|
|
2971
3018
|
setSearching(false);
|
|
2972
3019
|
if (topic && topicProp === void 0) setChosenTopic(null);
|
|
2973
|
-
|
|
3020
|
+
reach(hit);
|
|
2974
3021
|
},
|
|
2975
|
-
[topic, topicProp]
|
|
3022
|
+
[topic, topicProp, reach]
|
|
2976
3023
|
);
|
|
2977
3024
|
const run = useCallback(async (p, done) => {
|
|
2978
3025
|
try {
|
|
@@ -3349,7 +3396,7 @@ function ChatPanel({
|
|
|
3349
3396
|
autoFocus: true,
|
|
3350
3397
|
conversationTitle: titleOf,
|
|
3351
3398
|
onOpen: (hit) => {
|
|
3352
|
-
setJump({ messageId: hit.messageId, seq: hit.seq });
|
|
3399
|
+
setJump({ messageId: hit.messageId, seq: hit.seq, threadRootId: hit.threadRootId });
|
|
3353
3400
|
select(hit.conversationId);
|
|
3354
3401
|
},
|
|
3355
3402
|
className: "min-h-0 flex-1"
|
|
@@ -3734,6 +3781,8 @@ function createMemoryChatClient(options) {
|
|
|
3734
3781
|
async upload(conversationId, userId, file, opts) {
|
|
3735
3782
|
log("upload", conversationId, opts.name);
|
|
3736
3783
|
await wait();
|
|
3784
|
+
const total = file instanceof Blob ? file.size : file.byteLength;
|
|
3785
|
+
opts.onProgress?.(total, total);
|
|
3737
3786
|
const path = `${conversationId}/${userId}/${opts.id ?? uuid()}-${opts.name}`;
|
|
3738
3787
|
const blob = file instanceof Blob ? file : new Blob([file], { type: opts.contentType });
|
|
3739
3788
|
files[path] = typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(blob) : `memory://${path}`;
|
|
@@ -3827,7 +3876,7 @@ function createMemoryChatClient(options) {
|
|
|
3827
3876
|
},
|
|
3828
3877
|
async cancelScheduled(id) {
|
|
3829
3878
|
log("cancelScheduled", id);
|
|
3830
|
-
const s = scheduledList.find((x) => x.id === id && x.status
|
|
3879
|
+
const s = scheduledList.find((x) => x.id === id && x.status !== "sent" && x.status !== "cancelled");
|
|
3831
3880
|
if (!s) throw new ChatError("Nothing to cancel", "22023", "cancel_scheduled");
|
|
3832
3881
|
s.cancelledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3833
3882
|
s.status = "cancelled";
|
|
@@ -3879,7 +3928,7 @@ function createMemoryChatClient(options) {
|
|
|
3879
3928
|
if (conversationId && c.conversation.id !== conversationId) continue;
|
|
3880
3929
|
for (const m of c.messages) {
|
|
3881
3930
|
if (m.deletedAt || !visible(c, m)) continue;
|
|
3882
|
-
const base = { messageId: m.id, conversationId: c.conversation.id, seq: m.seq, senderId: m.senderId, createdAt: m.createdAt };
|
|
3931
|
+
const base = { messageId: m.id, conversationId: c.conversation.id, seq: m.seq, senderId: m.senderId, createdAt: m.createdAt, threadRootId: m.threadRootId ?? null };
|
|
3883
3932
|
if (m.body.toLowerCase().includes(q)) hits.push({ ...base, snippet: mark(m.body), inTranscript: false });
|
|
3884
3933
|
else {
|
|
3885
3934
|
const t = m.attachments.find((a) => a.transcript?.toLowerCase().includes(q))?.transcript;
|