Add coach session review workflow
This commit is contained in:
parent
227ec4cb9a
commit
0e65518e41
@ -11,13 +11,21 @@ function isClientUser(req) {
|
|||||||
|
|
||||||
function portalClientIncludes() {
|
function portalClientIncludes() {
|
||||||
return [
|
return [
|
||||||
{ model: db.sessions, as: "sessions", order: [["session_at", "DESC"]] },
|
{ model: db.sessions, as: "sessions", where: { status: "shared" }, required: false, order: [["session_at", "DESC"]] },
|
||||||
{ model: db.action_items, as: "action_items", order: [["due_at", "ASC"]] },
|
{ model: db.action_items, as: "action_items", order: [["due_at", "ASC"]] },
|
||||||
{ model: db.resources, as: "resources", where: { is_shared: true }, required: false },
|
{ model: db.resources, as: "resources", where: { is_shared: true }, required: false },
|
||||||
{ model: db.prep_briefs, as: "prep_briefs", order: [["updatedAt", "DESC"]] },
|
{ model: db.prep_briefs, as: "prep_briefs", order: [["updatedAt", "DESC"]] },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitActionItems(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.split(/\n|;/)
|
||||||
|
.map((item) => item.replace(/^[-*]\s*/, "").trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/summary",
|
"/summary",
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
@ -137,6 +145,107 @@ router.post(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/session-memory/save",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const data = req.body.data || req.body;
|
||||||
|
|
||||||
|
if (!data.clientId) {
|
||||||
|
res.status(400).send({ error: "client_id_required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await db.clients.findByPk(data.clientId);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
res.status(404).send({ error: "client_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = data.shareWithClient ? "shared" : "draft_review";
|
||||||
|
const session = await db.sessions.create({
|
||||||
|
clientId: data.clientId,
|
||||||
|
title: data.title,
|
||||||
|
session_at: data.session_at || new Date(),
|
||||||
|
status,
|
||||||
|
transcript_notes: data.transcript_notes,
|
||||||
|
ai_summary: data.ai_summary,
|
||||||
|
key_topics: data.key_topics,
|
||||||
|
goals_discussed: data.goals_discussed,
|
||||||
|
blockers: data.blockers,
|
||||||
|
commitments: data.commitments,
|
||||||
|
homework: data.homework,
|
||||||
|
emotional_themes: data.emotional_themes,
|
||||||
|
important_quotes: data.important_quotes,
|
||||||
|
follow_up_email: data.follow_up_email,
|
||||||
|
next_session_prep: data.next_session_prep,
|
||||||
|
private_coach_notes: data.private_coach_notes,
|
||||||
|
shared_client_notes: data.shared_client_notes,
|
||||||
|
createdById: req.currentUser.id,
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionTitles = splitActionItems(data.commitments || data.homework);
|
||||||
|
|
||||||
|
for (const title of actionTitles) {
|
||||||
|
await db.action_items.create({
|
||||||
|
clientId: data.clientId,
|
||||||
|
sessionId: session.id,
|
||||||
|
title,
|
||||||
|
status: "not_started",
|
||||||
|
createdById: req.currentUser.id,
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.prep_briefs.create({
|
||||||
|
clientId: data.clientId,
|
||||||
|
sessionId: session.id,
|
||||||
|
next_session_at: client.next_session_at,
|
||||||
|
previous_summary: data.ai_summary,
|
||||||
|
open_commitments: actionTitles.join("; "),
|
||||||
|
suggested_questions: data.next_session_prep,
|
||||||
|
sensitive_topics: data.blockers,
|
||||||
|
status: "ready",
|
||||||
|
createdById: req.currentUser.id,
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedSession = await db.sessions.findByPk(session.id, {
|
||||||
|
include: [
|
||||||
|
{ model: db.clients, as: "client" },
|
||||||
|
{ model: db.action_items, as: "action_items", order: [["createdAt", "ASC"]] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(savedSession);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/sessions/:id/share",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const session = await db.sessions.findByPk(req.params.id);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
res.status(404).send({ error: "session_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await session.update({
|
||||||
|
shared_client_notes: req.body.shared_client_notes || session.shared_client_notes,
|
||||||
|
status: "shared",
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedSession = await db.sessions.findByPk(session.id, {
|
||||||
|
include: [{ model: db.clients, as: "client" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(savedSession);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/session-memory/generate",
|
"/session-memory/generate",
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
|
|||||||
@ -20,13 +20,25 @@ type Client = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Session = {
|
type MemoryDraft = {
|
||||||
id: string;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
ai_summary?: string;
|
ai_summary?: string;
|
||||||
key_topics?: string;
|
key_topics?: string;
|
||||||
|
goals_discussed?: string;
|
||||||
|
blockers?: string;
|
||||||
|
commitments?: string;
|
||||||
homework?: string;
|
homework?: string;
|
||||||
|
emotional_themes?: string;
|
||||||
|
important_quotes?: string;
|
||||||
follow_up_email?: string;
|
follow_up_email?: string;
|
||||||
|
next_session_prep?: string;
|
||||||
|
private_coach_notes?: string;
|
||||||
|
shared_client_notes?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Session = MemoryDraft & {
|
||||||
|
id: string;
|
||||||
|
status?: string;
|
||||||
client?: Client;
|
client?: Client;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,32 +58,73 @@ function Panel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OutputBlock({
|
function TextField({
|
||||||
title,
|
label,
|
||||||
children,
|
value,
|
||||||
|
onChange,
|
||||||
|
rows = 3,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
label: string;
|
||||||
children: React.ReactNode;
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
rows?: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className='rounded-lg border border-[#19192d]/10 bg-[#fffdf9] p-5'>
|
<label className='block'>
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.2em] text-[#b17a1e]'>
|
<span className='text-xs font-semibold uppercase tracking-[0.18em] text-[#b17a1e]'>
|
||||||
{title}
|
{label}
|
||||||
</p>
|
</span>
|
||||||
<div className='mt-3 leading-6 text-[#72798a]'>{children}</div>
|
<textarea
|
||||||
</div>
|
value={value}
|
||||||
|
rows={rows}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
className='mt-2 w-full rounded-lg border border-[#19192d]/10 bg-[#fffdf9] px-3 py-2 text-sm leading-6 text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatusPill({ status }: { status?: string }) {
|
||||||
|
const isShared = status === 'shared';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3 py-1 text-xs font-semibold ${
|
||||||
|
isShared
|
||||||
|
? 'bg-[#f3fbf8] text-[#35b7a5]'
|
||||||
|
: 'bg-[#fbf8f1] text-[#b17a1e]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isShared ? 'Shared' : 'Coach draft'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyDraft: MemoryDraft = {
|
||||||
|
title: '',
|
||||||
|
ai_summary: '',
|
||||||
|
key_topics: '',
|
||||||
|
goals_discussed: '',
|
||||||
|
blockers: '',
|
||||||
|
commitments: '',
|
||||||
|
homework: '',
|
||||||
|
emotional_themes: '',
|
||||||
|
important_quotes: '',
|
||||||
|
follow_up_email: '',
|
||||||
|
next_session_prep: '',
|
||||||
|
private_coach_notes: '',
|
||||||
|
shared_client_notes: '',
|
||||||
|
};
|
||||||
|
|
||||||
const SessionMemory = () => {
|
const SessionMemory = () => {
|
||||||
const [clients, setClients] = React.useState<Client[]>([]);
|
const [clients, setClients] = React.useState<Client[]>([]);
|
||||||
const [sessions, setSessions] = React.useState<Session[]>([]);
|
const [sessions, setSessions] = React.useState<Session[]>([]);
|
||||||
const [clientId, setClientId] = React.useState('');
|
const [clientId, setClientId] = React.useState('');
|
||||||
const [transcript, setTranscript] = React.useState('');
|
const [transcript, setTranscript] = React.useState('');
|
||||||
const [generatedMemory, setGeneratedMemory] = React.useState<Session | null>(
|
const [draft, setDraft] = React.useState<MemoryDraft>(emptyDraft);
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [isGenerating, setIsGenerating] = React.useState(false);
|
const [isGenerating, setIsGenerating] = React.useState(false);
|
||||||
|
const [isSaving, setIsSaving] = React.useState(false);
|
||||||
|
const [notice, setNotice] = React.useState('');
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const [clientsResponse, sessionsResponse] = await Promise.all([
|
const [clientsResponse, sessionsResponse] = await Promise.all([
|
||||||
@ -90,16 +143,79 @@ const SessionMemory = () => {
|
|||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
function updateDraft(field: keyof MemoryDraft, value: string) {
|
||||||
|
setDraft((current) => {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setNotice('');
|
||||||
|
}
|
||||||
|
|
||||||
async function generateMemory() {
|
async function generateMemory() {
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
const response = await axios.post('/coaching/session-memory/generate', {
|
const response = await axios.post('/coaching/session-memory/generate', {
|
||||||
clientId,
|
clientId,
|
||||||
transcript,
|
transcript,
|
||||||
});
|
});
|
||||||
setGeneratedMemory(response.data);
|
setDraft({
|
||||||
|
...emptyDraft,
|
||||||
|
...response.data,
|
||||||
|
});
|
||||||
|
setNotice('Draft generated. Review and edit before saving.');
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveMemory(shareWithClient: boolean) {
|
||||||
|
setIsSaving(true);
|
||||||
|
const response = await axios.post('/coaching/session-memory/save', {
|
||||||
|
...draft,
|
||||||
|
clientId,
|
||||||
|
transcript_notes: transcript,
|
||||||
|
shareWithClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSessions((current) => [response.data, ...current]);
|
||||||
|
setDraft(emptyDraft);
|
||||||
|
setTranscript('');
|
||||||
|
setNotice(
|
||||||
|
shareWithClient
|
||||||
|
? 'Session saved and shared with the client.'
|
||||||
|
: 'Session saved as a coach draft.',
|
||||||
|
);
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shareSession(session: Session) {
|
||||||
|
const response = await axios.patch(`/coaching/sessions/${session.id}/share`, {
|
||||||
|
shared_client_notes: session.shared_client_notes,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSessions((current) =>
|
||||||
|
current.map((currentSession) => {
|
||||||
|
if (currentSession.id === session.id) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentSession;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setNotice('Session shared with the client.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyFollowUp() {
|
||||||
|
await navigator.clipboard.writeText(draft.follow_up_email || '');
|
||||||
|
setNotice('Follow-up copied.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDraft = Boolean(
|
||||||
|
draft.title ||
|
||||||
|
draft.ai_summary ||
|
||||||
|
draft.follow_up_email ||
|
||||||
|
draft.shared_client_notes,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@ -119,13 +235,18 @@ const SessionMemory = () => {
|
|||||||
prep.
|
prep.
|
||||||
</h1>
|
</h1>
|
||||||
<p className='mt-2 max-w-3xl text-sm leading-6 text-[#fffdf9]'>
|
<p className='mt-2 max-w-3xl text-sm leading-6 text-[#fffdf9]'>
|
||||||
Paste session notes or a transcript, generate structured output,
|
Generate a structured draft, edit it as the coach, save it as a
|
||||||
then review and save the result before sharing anything with a
|
private draft, or share approved notes with the client portal.
|
||||||
client.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid gap-4 xl:grid-cols-[0.85fr_1.15fr]'>
|
{notice && (
|
||||||
|
<div className='mb-4 rounded-lg border border-[#35b7a5]/20 bg-[#f3fbf8] px-4 py-3 text-sm font-semibold text-[#257f73]'>
|
||||||
|
{notice}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='grid gap-4 xl:grid-cols-[0.8fr_1.2fr]'>
|
||||||
<Panel className='p-4'>
|
<Panel className='p-4'>
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
|
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
|
||||||
Raw session input
|
Raw session input
|
||||||
@ -168,46 +289,105 @@ const SessionMemory = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<div className='space-y-6'>
|
<div className='space-y-4'>
|
||||||
<Panel className='p-4'>
|
<Panel className='p-4'>
|
||||||
<div className='flex items-start justify-between gap-4'>
|
<div className='flex items-start justify-between gap-4'>
|
||||||
<div>
|
<div>
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
|
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
|
||||||
AI draft
|
Coach review
|
||||||
</p>
|
</p>
|
||||||
<h2 className='mt-2 text-lg font-semibold text-[#19192d]'>
|
<h2 className='mt-2 text-lg font-semibold text-[#19192d]'>
|
||||||
Review before sharing
|
Edit the final memory
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<span className='rounded-full bg-[#f3fbf8] px-3 py-1 text-xs font-semibold text-[#35b7a5]'>
|
<span className='rounded-full bg-[#fbf8f1] px-3 py-1 text-xs font-semibold text-[#b17a1e]'>
|
||||||
Coach approved
|
Not shared until approved
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{generatedMemory ? (
|
{hasDraft ? (
|
||||||
<div className='mt-4 grid gap-4'>
|
<div className='mt-4 grid gap-4'>
|
||||||
<OutputBlock title='Summary'>
|
<label className='block'>
|
||||||
{generatedMemory.ai_summary}
|
<span className='text-xs font-semibold uppercase tracking-[0.18em] text-[#b17a1e]'>
|
||||||
</OutputBlock>
|
Title
|
||||||
<OutputBlock title='Commitments and homework'>
|
</span>
|
||||||
{generatedMemory.homework}
|
<input
|
||||||
</OutputBlock>
|
value={draft.title || ''}
|
||||||
<OutputBlock title='Follow-up email'>
|
onChange={(event) =>
|
||||||
<p className='whitespace-pre-line'>
|
updateDraft('title', event.target.value)
|
||||||
{generatedMemory.follow_up_email}
|
}
|
||||||
</p>
|
className='mt-2 w-full rounded-lg border border-[#19192d]/10 bg-[#fffdf9] px-3 py-2 text-sm text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15'
|
||||||
</OutputBlock>
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label='Summary'
|
||||||
|
value={draft.ai_summary || ''}
|
||||||
|
onChange={(value) => updateDraft('ai_summary', value)}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label='Commitments'
|
||||||
|
value={draft.commitments || ''}
|
||||||
|
onChange={(value) => updateDraft('commitments', value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label='Homework'
|
||||||
|
value={draft.homework || ''}
|
||||||
|
onChange={(value) => updateDraft('homework', value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label='Shared client notes'
|
||||||
|
value={draft.shared_client_notes || ''}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateDraft('shared_client_notes', value)
|
||||||
|
}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label='Follow-up email'
|
||||||
|
value={draft.follow_up_email || ''}
|
||||||
|
onChange={(value) => updateDraft('follow_up_email', value)}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label='Next-session prep'
|
||||||
|
value={draft.next_session_prep || ''}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateDraft('next_session_prep', value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label='Private coach notes'
|
||||||
|
value={draft.private_coach_notes || ''}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateDraft('private_coach_notes', value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='flex flex-wrap gap-3'>
|
<div className='flex flex-wrap gap-3'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='inline-flex items-center gap-2 rounded-full bg-[#35b7a5] px-3 py-1.5 text-sm font-semibold text-white'
|
className='inline-flex items-center gap-2 rounded-full bg-[#19192d] px-3 py-1.5 text-sm font-semibold text-white disabled:opacity-50'
|
||||||
|
disabled={isSaving}
|
||||||
|
onClick={() => saveMemory(false)}
|
||||||
>
|
>
|
||||||
<BaseIcon path={mdiCheckCircleOutline} size={18} />
|
<BaseIcon path={mdiCheckCircleOutline} size={18} />
|
||||||
Save final memory
|
Save coach draft
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='inline-flex items-center gap-2 rounded-full bg-[#35b7a5] px-3 py-1.5 text-sm font-semibold text-white disabled:opacity-50'
|
||||||
|
disabled={isSaving}
|
||||||
|
onClick={() => saveMemory(true)}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiSendOutline} size={18} />
|
||||||
|
Save and share
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='inline-flex items-center gap-2 rounded-full border border-[#19192d]/10 bg-white px-3 py-1.5 text-sm font-semibold text-[#19192d]'
|
className='inline-flex items-center gap-2 rounded-full border border-[#19192d]/10 bg-white px-3 py-1.5 text-sm font-semibold text-[#19192d]'
|
||||||
|
onClick={copyFollowUp}
|
||||||
>
|
>
|
||||||
<BaseIcon path={mdiContentCopy} size={18} />
|
<BaseIcon path={mdiContentCopy} size={18} />
|
||||||
Copy follow-up
|
Copy follow-up
|
||||||
@ -222,10 +402,8 @@ const SessionMemory = () => {
|
|||||||
className='text-[#b17a1e]'
|
className='text-[#b17a1e]'
|
||||||
/>
|
/>
|
||||||
<p className='mt-4 leading-6 text-[#72798a]'>
|
<p className='mt-4 leading-6 text-[#72798a]'>
|
||||||
Generate a structured memory through the AppWizzy AI
|
Generate memory from raw notes. The editable draft will
|
||||||
proxy. The result should stay reviewable: summary,
|
appear here before anything is saved or shared.
|
||||||
commitments, blockers, homework, follow-up, and prep for
|
|
||||||
the next conversation.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -242,18 +420,26 @@ const SessionMemory = () => {
|
|||||||
<div key={session.id} className='p-5'>
|
<div key={session.id} className='p-5'>
|
||||||
<div className='flex items-start justify-between gap-4'>
|
<div className='flex items-start justify-between gap-4'>
|
||||||
<div>
|
<div>
|
||||||
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
<p className='font-semibold text-[#19192d]'>
|
<p className='font-semibold text-[#19192d]'>
|
||||||
{session.title}
|
{session.title}
|
||||||
</p>
|
</p>
|
||||||
|
<StatusPill status={session.status} />
|
||||||
|
</div>
|
||||||
<p className='mt-1 text-sm text-[#72798a]'>
|
<p className='mt-1 text-sm text-[#72798a]'>
|
||||||
{session.client?.name}
|
{session.client?.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<BaseIcon
|
{session.status !== 'shared' && (
|
||||||
path={mdiSendOutline}
|
<button
|
||||||
size={18}
|
type='button'
|
||||||
className='text-[#35b7a5]'
|
className='inline-flex items-center gap-2 rounded-full bg-[#35b7a5] px-3 py-1.5 text-sm font-semibold text-white'
|
||||||
/>
|
onClick={() => shareSession(session)}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiSendOutline} size={18} />
|
||||||
|
Share
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className='mt-3 leading-6 text-[#72798a]'>
|
<p className='mt-3 leading-6 text-[#72798a]'>
|
||||||
{session.ai_summary}
|
{session.ai_summary}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user