diff --git a/app/expert-finder/lib/utils.ts b/app/expert-finder/lib/utils.ts new file mode 100644 index 000000000..985d22cac --- /dev/null +++ b/app/expert-finder/lib/utils.ts @@ -0,0 +1,51 @@ +import type { ExpertSearchListItem } from '@/types/expertFinder'; +import type { AuthorProfile } from '@/types/authorProfile'; + +/** Default max length for picker / list item titles */ +export const SEARCH_DISPLAY_DEFAULT_MAX = 80; +/** Max length for library table name column */ +export const SEARCH_DISPLAY_TABLE_MAX = 60; + +export interface GetSearchDisplayTextOptions { + maxLength?: number; + /** Shown when name and query are both empty */ + emptyLabel?: string; +} + +/** + * Display string for an expert search (name, else query), truncated with ellipsis. + */ +export function getSearchDisplayText( + search: ExpertSearchListItem, + options?: GetSearchDisplayTextOptions +): string { + const maxLength = options?.maxLength ?? SEARCH_DISPLAY_DEFAULT_MAX; + const emptyLabel = options?.emptyLabel ?? 'Untitled search'; + const text = (search.name || search.query || '').trim(); + if (!text) return emptyLabel; + return text.length <= maxLength ? text : `${text.slice(0, maxLength)}…`; +} + +/** Library table / mobile card: shorter truncate, em dash when empty */ +export function getSearchTableDisplayText(search: ExpertSearchListItem): string { + return getSearchDisplayText(search, { + maxLength: SEARCH_DISPLAY_TABLE_MAX, + emptyLabel: '—', + }); +} + +/** + * Short author label for compact UI, e.g. "Nick T." from first name + last initial. + */ +export function getShortAuthorName(author: AuthorProfile | null | undefined): string { + if (!author) return ''; + const first = author.firstName?.trim(); + const last = author.lastName?.trim(); + if (first && last) return `${first} ${last.charAt(0)}.`; + if (author.fullName?.trim()) { + const parts = author.fullName.trim().split(/\s+/); + if (parts.length >= 2) return `${parts[0]} ${parts[parts.length - 1].charAt(0)}.`; + return parts[0]; + } + return ''; +} diff --git a/app/expert-finder/library/[searchId]/components/SearchDetailContent.tsx b/app/expert-finder/library/[searchId]/components/SearchDetailContent.tsx index 9079955fa..a9e02793a 100644 --- a/app/expert-finder/library/[searchId]/components/SearchDetailContent.tsx +++ b/app/expert-finder/library/[searchId]/components/SearchDetailContent.tsx @@ -205,46 +205,43 @@ export function SearchDetailContent({ searchId }: SearchDetailContentProps) {

Results ({searchDetail.expertResults.length})

-
- {selectedIndices.size > 0 && ( - <> - {selectedIndices.size < searchDetail.expertResults.length && ( - - )} - {selectedIndices.size === searchDetail.expertResults.length && ( - - )} - {selectedIndices.size} selected - - +
+ {selectedIndices.size === searchDetail.expertResults.length ? ( + + ) : ( + )} + {selectedIndices.size} selected +
diff --git a/app/expert-finder/library/[searchId]/components/SearchDetailHeader.tsx b/app/expert-finder/library/[searchId]/components/SearchDetailHeader.tsx index 159b650c3..5a04046c4 100644 --- a/app/expert-finder/library/[searchId]/components/SearchDetailHeader.tsx +++ b/app/expert-finder/library/[searchId]/components/SearchDetailHeader.tsx @@ -1,6 +1,8 @@ 'use client'; +import Link from 'next/link'; import { SearchStatusBadge } from '@/app/expert-finder/library/components/SearchStatusBadge'; +import { AuthorTooltip } from '@/components/ui/AuthorTooltip'; import { Badge } from '@/components/ui/Badge'; import { formatTimestamp } from '@/utils/date'; import type { ExpertSearchResult } from '@/types/expertFinder'; @@ -11,6 +13,9 @@ interface SearchDetailHeaderProps { } export function SearchDetailHeader({ search }: SearchDetailHeaderProps) { + const createdBy = search.createdBy?.author; + const authorId = createdBy?.id; + return ( <>
@@ -18,6 +23,23 @@ export function SearchDetailHeader({ search }: SearchDetailHeaderProps) { Created {formatTimestamp(search.createdAt, false)} + {createdBy && ( + + Created by:{' '} + {authorId ? ( + + + {createdBy.fullName} + + + ) : ( + {createdBy.fullName} + )} + + )}
{search.work && } diff --git a/app/expert-finder/library/[searchId]/outreach/[emailId]/OutreachDetailPageContent.tsx b/app/expert-finder/library/[searchId]/outreach/[emailId]/OutreachDetailPageContent.tsx index 08bbc66cd..c4c8d5054 100644 --- a/app/expert-finder/library/[searchId]/outreach/[emailId]/OutreachDetailPageContent.tsx +++ b/app/expert-finder/library/[searchId]/outreach/[emailId]/OutreachDetailPageContent.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import { Mail, Trash2, Send, Loader2, Save, Eye } from 'lucide-react'; import { getTemplateDisplayLabel } from '@/app/expert-finder/library/[searchId]/components/GenerateEmailModal'; import { Alert } from '@/components/ui/Alert'; +import { AuthorTooltip } from '@/components/ui/AuthorTooltip'; import { BaseSection } from '@/components/ui/BaseSection'; import { Breadcrumbs } from '@/components/ui/Breadcrumbs'; import { Button } from '@/components/ui/Button'; @@ -19,7 +20,9 @@ import { usePreviewEmails, useSendEmails, } from '@/hooks/useExpertFinder'; +import { useUser } from '@/contexts/UserContext'; import { toast } from 'react-hot-toast'; +import { isValidEmail } from '@/utils/validation'; export interface OutreachDetailPageContentProps { emailId: string; @@ -30,6 +33,7 @@ export function OutreachDetailPageContent({ emailId, librarySearchId, }: OutreachDetailPageContentProps) { + const { user } = useUser(); const [{ email, isLoading, error }, refetch] = useGeneratedEmailDetail(emailId); const [{ isLoading: isUpdating }, updateEmail] = useUpdateGeneratedEmail(); const [{ isLoading: isDeleting }, deleteEmail] = useDeleteGeneratedEmail(); @@ -42,6 +46,7 @@ export function OutreachDetailPageContent({ const [actionError, setActionError] = useState(null); const [editSubject, setEditSubject] = useState(''); const [editBody, setEditBody] = useState(''); + const [replyTo, setReplyTo] = useState(''); const backHref = `/expert-finder/library/${librarySearchId}?tab=outreach`; @@ -52,6 +57,12 @@ export function OutreachDetailPageContent({ } }, [email?.id, email?.emailSubject, email?.emailBody]); + useEffect(() => { + if (user?.email && replyTo === '') { + setReplyTo(user.email); + } + }, [user?.email]); + const isDraft = email?.status !== 'sent'; const hasEdits = isDraft && @@ -111,9 +122,17 @@ export function OutreachDetailPageContent({ const handleSendToExpert = async () => { if (!emailId || !email) return; + const trimmedReplyTo = (replyTo ?? '').trim(); + if (!trimmedReplyTo || !isValidEmail(trimmedReplyTo)) { + setActionError('Please enter a valid Reply To email address.'); + return; + } setActionError(null); try { - await sendEmails({ generated_email_ids: [Number(emailId)] }); + await sendEmails({ + generated_email_ids: [Number(emailId)], + reply_to: trimmedReplyTo, + }); setShowSendToExpertConfirm(false); refetch(); toast.success('Email sent to the expert.'); @@ -183,6 +202,23 @@ export function OutreachDetailPageContent({
)} + {email.createdBy?.author && ( +
+ Created by:{' '} + {email.createdBy.author.id ? ( + + + {email.createdBy.author.fullName} + + + ) : ( + {email.createdBy.author.fullName} + )} +
+ )}
@@ -250,6 +286,23 @@ export function OutreachDetailPageContent({ )} + {isDraft && ( + + setReplyTo(e.target.value)} + placeholder="Email address for replies" + error={ + replyTo.trim() && !isValidEmail(replyTo.trim()) + ? 'Please enter a valid email address' + : undefined + } + /> + + )} +
diff --git a/app/expert-finder/library/[searchId]/outreach/components/GeneratedEmailsList.tsx b/app/expert-finder/library/[searchId]/outreach/components/GeneratedEmailsList.tsx index 298924e6c..47a129219 100644 --- a/app/expert-finder/library/[searchId]/outreach/components/GeneratedEmailsList.tsx +++ b/app/expert-finder/library/[searchId]/outreach/components/GeneratedEmailsList.tsx @@ -1,19 +1,22 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Send } from 'lucide-react'; import { Alert } from '@/components/ui/Alert'; import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/form/Input'; import { PaginationButton } from '@/components/ui/PaginationButton'; import { Modal } from '@/components/ui/form/Modal'; import { useGeneratedEmails, useSendEmails } from '@/hooks/useExpertFinder'; import { useScreenSize } from '@/hooks/useScreenSize'; +import { useUser } from '@/contexts/UserContext'; import { OutreachTable, OUTREACH_TABLE_COLUMNS } from './OutreachTable'; import { OutreachMobileCard } from './OutreachMobileCard'; import { TableSkeleton } from '@/components/ui/Table/TableSkeleton'; import { ListCardSkeleton } from '@/components/ui/ListCardSkeleton'; import { toast } from 'react-hot-toast'; +import { isValidEmail } from '@/utils/validation'; import type { GeneratedEmail } from '@/types/expertFinder'; const DEFAULT_PAGE_SIZE = 10; @@ -45,10 +48,12 @@ export function GeneratedEmailsList({ emptyMessage = DEFAULT_EMPTY_MESSAGE, }: GeneratedEmailsListProps) { const router = useRouter(); + const { user } = useUser(); const { mdAndUp } = useScreenSize(); const [page, setPage] = useState(1); const [selectedIds, setSelectedIds] = useState>(new Set()); const [showSendConfirm, setShowSendConfirm] = useState(false); + const [bulkReplyTo, setBulkReplyTo] = useState(''); const offset = (page - 1) * pageSize; const [{ emails, pagination, isLoading, error }, refetch] = useGeneratedEmails({ @@ -90,19 +95,33 @@ export function GeneratedEmailsList({ setShowSendConfirm(true); }; + useEffect(() => { + if (showSendConfirm && user?.email && !bulkReplyTo.trim()) { + setBulkReplyTo(user.email); + } + }, [showSendConfirm, user?.email]); + const handleSendConfirm = async () => { const ids = Array.from(selectedIds); if (ids.length === 0) return; + const trimmedReplyTo = bulkReplyTo.trim(); + if (!trimmedReplyTo || !isValidEmail(trimmedReplyTo)) { + toast.error('Please enter a valid Reply To email address.'); + return; + } try { - await sendEmails({ generated_email_ids: ids }); + await sendEmails({ + generated_email_ids: ids, + reply_to: trimmedReplyTo, + }); setShowSendConfirm(false); setSelectedIds(new Set()); + setBulkReplyTo(''); toast.success( 'Emails are being sent. You can close this window and monitor status in the outreach table.' ); refetch(); } catch { - // Error already set in hook; toast or show in modal toast.error('Failed to send emails. Please try again.'); } }; @@ -139,31 +158,32 @@ export function GeneratedEmailsList({

Generated emails ({pagination.total})

-
- {selectedIds.size > 0 && ( - <> - {allSelected ? ( - - ) : ( - - )} - {selectedIds.size} selected - - +
+ {allSelected ? ( + + ) : ( + )} + {selectedIds.size} selected +
@@ -224,6 +244,21 @@ export function GeneratedEmailsList({

The selected emails will be sent to the experts.

+
+ setBulkReplyTo(e.target.value)} + placeholder="Email address for replies" + helperText="When experts hit Reply, their response goes to this address. Defaults to your account email; change it if you want replies elsewhere." + error={ + bulkReplyTo.trim() && !isValidEmail(bulkReplyTo.trim()) + ? 'Please enter a valid email address' + : undefined + } + /> +
-
diff --git a/app/expert-finder/library/[searchId]/outreach/components/OutreachMobileCard.tsx b/app/expert-finder/library/[searchId]/outreach/components/OutreachMobileCard.tsx index d0602265d..d6efcdca5 100644 --- a/app/expert-finder/library/[searchId]/outreach/components/OutreachMobileCard.tsx +++ b/app/expert-finder/library/[searchId]/outreach/components/OutreachMobileCard.tsx @@ -30,6 +30,7 @@ export function OutreachMobileCard({ email, onClick, className }: OutreachMobile const templateLabel = getTemplateDisplayLabel(email.template); const templateDescription = getTemplateDescription(email.template); const isSent = email.status === 'sent'; + const createdByName = email.createdBy?.author?.fullName; return ( @@ -37,6 +38,7 @@ export function OutreachMobileCard({ email, onClick, className }: OutreachMobile

{subject}

+ {createdByName &&

by {createdByName}

}
{isSent ? 'Sent' : 'Draft'} diff --git a/app/expert-finder/library/[searchId]/outreach/components/OutreachTable.tsx b/app/expert-finder/library/[searchId]/outreach/components/OutreachTable.tsx index 7f2a02c64..3a0c9b38e 100644 --- a/app/expert-finder/library/[searchId]/outreach/components/OutreachTable.tsx +++ b/app/expert-finder/library/[searchId]/outreach/components/OutreachTable.tsx @@ -24,7 +24,8 @@ const BASE_COLUMNS: SortableColumn[] = [ { key: 'subject', label: 'Subject', sortable: false }, { key: 'template', label: 'Template', sortable: false }, { key: 'status', label: 'Status', sortable: false }, - { key: 'createdAt', label: 'Created', sortable: false }, + { key: 'createdBy', label: 'Created By', sortable: false }, + { key: 'createdAt', label: 'Created Date', sortable: false }, { key: 'view', label: '', sortable: false }, ]; @@ -115,6 +116,7 @@ export function OutreachTable({ {email.status === 'sent' ? 'Sent' : 'Draft'} ), + createdBy: email.createdBy?.author?.fullName ?? '—', createdAt: formatTimestamp(email.createdAt, false), view: (

{displayName}

+ {createdByName &&

by {createdByName}

}
{expertCount !== null && ( diff --git a/app/expert-finder/library/components/SearchHistoryTable.tsx b/app/expert-finder/library/components/SearchHistoryTable.tsx index c496c8634..d9b0a680a 100644 --- a/app/expert-finder/library/components/SearchHistoryTable.tsx +++ b/app/expert-finder/library/components/SearchHistoryTable.tsx @@ -5,21 +5,16 @@ import { TableContainer, SortableColumn } from '@/components/ui/Table/TableConta import { SearchStatusBadge } from './SearchStatusBadge'; import { formatTimestamp } from '@/utils/date'; import type { ExpertSearchListItem } from '@/types/expertFinder'; +import { getSearchTableDisplayText } from '@/app/expert-finder/lib/utils'; const SEARCH_DETAIL_PATH = '/expert-finder/library'; -const QUERY_TRUNCATE_LENGTH = 60; - -export function getDisplayText(search: ExpertSearchListItem): string { - const text = (search.name || search.query || '').trim(); - if (!text) return '—'; - return text.length <= QUERY_TRUNCATE_LENGTH ? text : `${text.slice(0, QUERY_TRUNCATE_LENGTH)}…`; -} const COLUMNS: SortableColumn[] = [ { key: 'name', label: 'Name', sortable: false }, { key: 'expertCount', label: 'Expert Count', sortable: false }, - { key: 'createdAt', label: 'Created At', sortable: false }, { key: 'status', label: 'Status', sortable: false }, + { key: 'createdBy', label: 'Created By', sortable: false }, + { key: 'createdAt', label: 'Created At', sortable: false }, { key: 'view', label: '', sortable: false }, ]; @@ -31,9 +26,10 @@ interface SearchHistoryTableProps { export function SearchHistoryTable({ searches, onRowClick }: SearchHistoryTableProps) { const data = searches.map((search) => ({ searchId: search.searchId, - name: getDisplayText(search), + name: getSearchTableDisplayText(search), status: , expertCount: search.status === 'completed' ? search.expertCount : '—', + createdBy: search.createdBy?.author?.fullName ?? '—', createdAt: formatTimestamp(search.createdAt, false), view: ( @@ -31,6 +26,14 @@ export function ExpertSearchListItemDetail({ search }: ExpertSearchListItemDetai {formatTimeAgo(search.createdAt)} + {createdByShort && ( + <> + + + by {createdByShort} + + + )}
); diff --git a/app/expert-finder/library/new/components/ExpertSearchPicker.tsx b/app/expert-finder/library/new/components/ExpertSearchPicker.tsx index 65e0b3503..b7161943d 100644 --- a/app/expert-finder/library/new/components/ExpertSearchPicker.tsx +++ b/app/expert-finder/library/new/components/ExpertSearchPicker.tsx @@ -5,7 +5,8 @@ import { Check } from 'lucide-react'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/form/Input'; import { Dropdown, DropdownItem } from '@/components/ui/form/Dropdown'; -import { ExpertSearchListItemDetail, getSearchDisplayText } from './ExpertSearchListItemDetail'; +import { getSearchDisplayText } from '@/app/expert-finder/lib/utils'; +import { ExpertSearchListItemDetail } from './ExpertSearchListItemDetail'; import { useExpertSearches } from '@/hooks/useExpertFinder'; import type { ExpertSearchListItem } from '@/types/expertFinder'; import { cn } from '@/utils/styles'; diff --git a/app/expert-finder/templates/components/TemplateBodyEditor.tsx b/app/expert-finder/templates/components/TemplateBodyEditor.tsx index 135de8864..6a66343d8 100644 --- a/app/expert-finder/templates/components/TemplateBodyEditor.tsx +++ b/app/expert-finder/templates/components/TemplateBodyEditor.tsx @@ -36,11 +36,6 @@ const TEMPLATE_PLACEHOLDER_GROUPS = [ '{{user.organization}}', ], }, - { - label: 'RFP', - tone: 'amber', - tags: ['{{rfp.title}}', '{{rfp.deadline}}', '{{rfp.blurb}}', '{{rfp.amount}}', '{{rfp.url}}'], - }, { label: 'Expert', tone: 'emerald', @@ -52,6 +47,25 @@ const TEMPLATE_PLACEHOLDER_GROUPS = [ '{{expert.expertise}}', ], }, + { + label: 'RFP', + tone: 'amber', + tags: ['{{rfp.title}}', '{{rfp.deadline}}', '{{rfp.blurb}}', '{{rfp.amount}}', '{{rfp.url}}'], + }, + { + label: 'Proposal', + tone: 'purple', + tags: [ + '{{proposal.title}}', + '{{proposal.url}}', + '{{proposal.created_by_name}}', + '{{proposal.goal_amount}}', + '{{proposal.amount_raised}}', + '{{proposal.contributor_count}}', + '{{proposal.deadline}}', + '{{proposal.blurb}}', + ], + }, ] as const; const PLACEHOLDER_CLASS_BY_TOKEN: Record = Object.fromEntries( @@ -309,6 +323,9 @@ export function TemplateVariableEditor({ '[&_.template-placeholder-tag--amber]:border-amber-200', '[&_.template-placeholder-tag--amber]:bg-amber-50', '[&_.template-placeholder-tag--amber]:text-amber-700', + '[&_.template-placeholder-tag--purple]:border-purple-200', + '[&_.template-placeholder-tag--purple]:bg-purple-50', + '[&_.template-placeholder-tag--purple]:text-purple-700', '[&_.template-placeholder-tag--generic]:border-gray-200', '[&_.template-placeholder-tag--generic]:bg-gray-100', '[&_.template-placeholder-tag--generic]:text-gray-700', @@ -432,6 +449,8 @@ export function TemplateVariableEditor({ 'border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100', group.tone === 'amber' && 'border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100', + group.tone === 'purple' && + 'border-purple-200 bg-purple-50 text-purple-700 hover:bg-purple-100', group.tone === 'emerald' && 'border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100' )} diff --git a/app/expert-finder/templates/components/TemplateMobileCard.tsx b/app/expert-finder/templates/components/TemplateMobileCard.tsx index b9eece2dc..e8cddc5a3 100644 --- a/app/expert-finder/templates/components/TemplateMobileCard.tsx +++ b/app/expert-finder/templates/components/TemplateMobileCard.tsx @@ -30,11 +30,13 @@ function ContactSummary({ template }: { template: SavedTemplate }) { export function TemplateMobileCard({ template, onClick, className }: TemplateMobileCardProps) { const displayName = template.name?.trim() || '—'; + const createdByName = template.createdBy?.author?.fullName; return (

{displayName}

+ {createdByName &&

by {createdByName}

}

{formatTimestamp(template.createdDate, false)}

); diff --git a/app/expert-finder/templates/components/TemplatesTable.tsx b/app/expert-finder/templates/components/TemplatesTable.tsx index be4237f8e..3847384e7 100644 --- a/app/expert-finder/templates/components/TemplatesTable.tsx +++ b/app/expert-finder/templates/components/TemplatesTable.tsx @@ -3,36 +3,49 @@ import Link from 'next/link'; import { TableContainer, SortableColumn } from '@/components/ui/Table/TableContainer'; import { formatTimestamp } from '@/utils/date'; +import { stripHtml } from '@/utils/stringUtils'; import type { SavedTemplate } from '@/types/expertFinder'; const TEMPLATE_DETAIL_PATH = '/expert-finder/templates'; +const DETAIL_MAX_CHARS = 80; -function TemplateContactDetails({ template }: { template: SavedTemplate }) { - const name = template.contactName?.trim(); - const title = template.contactTitle?.trim(); - const institution = template.contactInstitution?.trim(); - const email = template.contactEmail?.trim(); +function TemplateDetailsCell({ template }: { template: SavedTemplate }) { + const isPromptContext = template.templateType === 'prompt-context'; + const textClassName = 'text-[10px] text-gray-600 truncate block max-w-[200px]'; - const nameTitleLine = [name, title].filter(Boolean).join(', ') || '—'; - const institutionLine = institution || '—'; + if (isPromptContext) { + const name = template.contactName?.trim(); + const title = template.contactTitle?.trim(); + const institution = template.contactInstitution?.trim(); + const email = template.contactEmail?.trim(); + const parts = [name, title, institution, email].filter(Boolean); + const summary = parts.length > 0 ? parts.join(' · ') : '—'; + const display = + summary.length > DETAIL_MAX_CHARS ? `${summary.slice(0, DETAIL_MAX_CHARS)}…` : summary; + return ( + + {display} + + ); + } + const bodySnippet = stripHtml(template.emailBody ?? ''); + const display = + bodySnippet.length > DETAIL_MAX_CHARS + ? `${bodySnippet.slice(0, DETAIL_MAX_CHARS)}…` + : bodySnippet || '—'; return ( -
- {nameTitleLine} - {institutionLine} - {email ? ( - {email} - ) : ( - - )} -
+ + {display} + ); } const COLUMNS: SortableColumn[] = [ { key: 'name', label: 'Name', sortable: false }, - { key: 'contact', label: 'Contact', sortable: false }, - { key: 'createdDate', label: 'Created', sortable: false }, + { key: 'details', label: 'Details', sortable: false }, + { key: 'createdBy', label: 'Created By', sortable: false }, + { key: 'createdDate', label: 'Created Date', sortable: false }, { key: 'view', label: '', sortable: false }, ]; @@ -45,7 +58,8 @@ export function TemplatesTable({ templates, onRowClick }: TemplatesTableProps) { const data = templates.map((template) => ({ id: template.id, name: template.name || '—', - contact: , + details: , + createdBy: template.createdBy?.author?.fullName ?? '—', createdDate: formatTimestamp(template.createdDate, false), view: ( ( updatedAt: raw.updated_at ?? '', completedAt: raw.completed_at ?? null, work: raw.work ? transformUnifiedDocument(raw.work) : null, + createdBy: transformCreatedBy(raw.created_by), })); export const transformExpertSearchListItem = createTransformer( @@ -134,6 +150,7 @@ export const transformExpertSearchListItem = createTransformer((r notes: raw.notes ?? '', createdAt: raw.created_at ?? '', updatedAt: raw.updated_at ?? '', + createdBy: transformCreatedBy(raw.created_by), })); // ── Document invited experts (app-level, camelCase) ─────────────────────────── @@ -234,7 +253,7 @@ export const transformInvitedExperts = createTransformer((r export interface SavedTemplate { id: number; - createdBy: number; + createdBy: CreatedByInfo | null; name: string; templateType: SavedTemplateType; contactName: string; @@ -259,7 +278,7 @@ export interface SavedTemplateListResponse { export const transformSavedTemplate = createTransformer((raw) => ({ id: raw.id ?? 0, - createdBy: raw.created_by ?? 0, + createdBy: transformCreatedBy(raw.created_by), name: raw.name ?? '', templateType: raw.template_type ?? 'prompt-context', contactName: raw.contact_name ?? '',