370 lines
11 KiB
TypeScript
370 lines
11 KiB
TypeScript
import {
|
|
BaseBoxShapeUtil,
|
|
HTMLContainer,
|
|
TLBaseShape,
|
|
} from "tldraw"
|
|
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"
|
|
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
|
|
|
|
type IFathomTranscript = TLBaseShape<
|
|
"FathomTranscript",
|
|
{
|
|
w: number
|
|
h: number
|
|
meetingId: string
|
|
meetingTitle: string
|
|
meetingUrl: string
|
|
summary: string
|
|
transcript: Array<{
|
|
speaker: string
|
|
text: string
|
|
timestamp: string
|
|
}>
|
|
actionItems: Array<{
|
|
text: string
|
|
assignee?: string
|
|
dueDate?: string
|
|
}>
|
|
isExpanded: boolean
|
|
showTranscript: boolean
|
|
showActionItems: boolean
|
|
}
|
|
>
|
|
|
|
export class FathomTranscriptShape extends BaseBoxShapeUtil<IFathomTranscript> {
|
|
static override type = "FathomTranscript" as const
|
|
|
|
// Fathom Transcript theme color: Blue (same as FathomMeetings)
|
|
static readonly PRIMARY_COLOR = "#3b82f6"
|
|
|
|
getDefaultProps(): IFathomTranscript["props"] {
|
|
return {
|
|
w: 600,
|
|
h: 400,
|
|
meetingId: "",
|
|
meetingTitle: "",
|
|
meetingUrl: "",
|
|
summary: "",
|
|
transcript: [],
|
|
actionItems: [],
|
|
isExpanded: false,
|
|
showTranscript: true,
|
|
showActionItems: true,
|
|
}
|
|
}
|
|
|
|
component(shape: IFathomTranscript) {
|
|
const {
|
|
w,
|
|
h,
|
|
meetingId,
|
|
meetingTitle,
|
|
meetingUrl,
|
|
summary,
|
|
transcript,
|
|
actionItems,
|
|
isExpanded,
|
|
showTranscript,
|
|
showActionItems
|
|
} = shape.props
|
|
|
|
const [isHovering, setIsHovering] = useState(false)
|
|
const [isMinimized, setIsMinimized] = useState(false)
|
|
const isSelected = this.editor.getSelectedShapeIds().includes(shape.id)
|
|
|
|
const toggleExpanded = useCallback(() => {
|
|
this.editor.updateShape<IFathomTranscript>({
|
|
id: shape.id,
|
|
type: 'FathomTranscript',
|
|
props: {
|
|
...shape.props,
|
|
isExpanded: !isExpanded
|
|
}
|
|
})
|
|
}, [shape.id, shape.props, isExpanded])
|
|
|
|
const toggleTranscript = useCallback(() => {
|
|
this.editor.updateShape<IFathomTranscript>({
|
|
id: shape.id,
|
|
type: 'FathomTranscript',
|
|
props: {
|
|
...shape.props,
|
|
showTranscript: !showTranscript
|
|
}
|
|
})
|
|
}, [shape.id, shape.props, showTranscript])
|
|
|
|
const toggleActionItems = useCallback(() => {
|
|
this.editor.updateShape<IFathomTranscript>({
|
|
id: shape.id,
|
|
type: 'FathomTranscript',
|
|
props: {
|
|
...shape.props,
|
|
showActionItems: !showActionItems
|
|
}
|
|
})
|
|
}, [shape.id, shape.props, showActionItems])
|
|
|
|
const formatTimestamp = (timestamp: string) => {
|
|
// Convert timestamp to readable format
|
|
const seconds = parseInt(timestamp)
|
|
const minutes = Math.floor(seconds / 60)
|
|
const remainingSeconds = seconds % 60
|
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
|
|
}
|
|
|
|
const buttonStyle: React.CSSProperties = {
|
|
padding: '4px 8px',
|
|
fontSize: '10px',
|
|
border: '1px solid #ccc',
|
|
borderRadius: '4px',
|
|
backgroundColor: 'white',
|
|
cursor: 'pointer',
|
|
}
|
|
|
|
// Custom header content with meeting info and toggle buttons
|
|
const headerContent = (
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', gap: '8px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<span>🎥 Fathom Meeting</span>
|
|
{meetingId && <span style={{ fontSize: '10px', color: '#666' }}>#{meetingId}</span>}
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
|
<button
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
toggleTranscript()
|
|
}}
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
style={{
|
|
...buttonStyle,
|
|
backgroundColor: showTranscript ? '#007bff' : '#6c757d',
|
|
color: 'white',
|
|
}}
|
|
>
|
|
📝 Transcript
|
|
</button>
|
|
<button
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
toggleActionItems()
|
|
}}
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
style={{
|
|
...buttonStyle,
|
|
backgroundColor: showActionItems ? '#28a745' : '#6c757d',
|
|
color: 'white',
|
|
}}
|
|
>
|
|
✅ Actions
|
|
</button>
|
|
<button
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
toggleExpanded()
|
|
}}
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
style={{
|
|
...buttonStyle,
|
|
backgroundColor: isExpanded ? '#ffc107' : '#6c757d',
|
|
color: 'white',
|
|
}}
|
|
>
|
|
{isExpanded ? '📄 Expanded' : '📄 Compact'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
const handleMinimize = () => {
|
|
setIsMinimized(!isMinimized)
|
|
}
|
|
|
|
const handleClose = () => {
|
|
this.editor.deleteShape(shape.id)
|
|
}
|
|
|
|
const contentStyle: React.CSSProperties = {
|
|
padding: '16px',
|
|
flex: 1,
|
|
overflow: 'auto',
|
|
color: 'black',
|
|
fontSize: '12px',
|
|
lineHeight: '1.4',
|
|
cursor: 'pointer',
|
|
transition: 'background-color 0.2s ease',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '12px',
|
|
}
|
|
|
|
const transcriptEntryStyle: React.CSSProperties = {
|
|
marginBottom: '8px',
|
|
padding: '8px',
|
|
backgroundColor: '#f8f9fa',
|
|
borderRadius: '4px',
|
|
borderLeft: '3px solid #007bff',
|
|
}
|
|
|
|
const actionItemStyle: React.CSSProperties = {
|
|
marginBottom: '6px',
|
|
padding: '6px',
|
|
backgroundColor: '#fff3cd',
|
|
borderRadius: '4px',
|
|
borderLeft: '3px solid #ffc107',
|
|
}
|
|
|
|
return (
|
|
<HTMLContainer style={{ width: w, height: h }}>
|
|
<StandardizedToolWrapper
|
|
title="Fathom Transcript"
|
|
primaryColor={FathomTranscriptShape.PRIMARY_COLOR}
|
|
isSelected={isSelected}
|
|
width={w}
|
|
height={h}
|
|
onClose={handleClose}
|
|
onMinimize={handleMinimize}
|
|
isMinimized={isMinimized}
|
|
headerContent={headerContent}
|
|
editor={this.editor}
|
|
shapeId={shape.id}
|
|
>
|
|
|
|
<div style={contentStyle}>
|
|
{/* Meeting Title */}
|
|
<div style={{ marginBottom: '8px' }}>
|
|
<h3 style={{ margin: '0 0 4px 0', fontSize: '14px', fontWeight: 'bold' }}>
|
|
{meetingTitle || 'Untitled Meeting'}
|
|
</h3>
|
|
{meetingUrl && (
|
|
<a
|
|
href={meetingUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{
|
|
fontSize: '10px',
|
|
color: '#007bff',
|
|
textDecoration: 'none'
|
|
}}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
View in Fathom →
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{/* Summary */}
|
|
{summary && (
|
|
<div style={{ marginBottom: '12px' }}>
|
|
<h4 style={{ margin: '0 0 6px 0', fontSize: '12px', fontWeight: 'bold', color: '#333' }}>
|
|
📋 Summary
|
|
</h4>
|
|
<div style={{
|
|
padding: '8px',
|
|
backgroundColor: '#e7f3ff',
|
|
borderRadius: '4px',
|
|
fontSize: '11px',
|
|
lineHeight: '1.4'
|
|
}}>
|
|
{summary}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Items */}
|
|
{showActionItems && actionItems.length > 0 && (
|
|
<div style={{ marginBottom: '12px' }}>
|
|
<h4 style={{ margin: '0 0 6px 0', fontSize: '12px', fontWeight: 'bold', color: '#333' }}>
|
|
✅ Action Items ({actionItems.length})
|
|
</h4>
|
|
<div style={{ maxHeight: isExpanded ? 'none' : '120px', overflow: 'auto' }}>
|
|
{actionItems.map((item, index) => (
|
|
<div key={index} style={actionItemStyle}>
|
|
<div style={{ fontSize: '11px', fontWeight: 'bold' }}>
|
|
{item.text}
|
|
</div>
|
|
{item.assignee && (
|
|
<div style={{ fontSize: '10px', color: '#666', marginTop: '2px' }}>
|
|
👤 {item.assignee}
|
|
</div>
|
|
)}
|
|
{item.dueDate && (
|
|
<div style={{ fontSize: '10px', color: '#666' }}>
|
|
📅 {item.dueDate}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Transcript */}
|
|
{showTranscript && transcript.length > 0 && (
|
|
<div>
|
|
<h4 style={{ margin: '0 0 6px 0', fontSize: '12px', fontWeight: 'bold', color: '#333' }}>
|
|
💬 Transcript ({transcript.length} entries)
|
|
</h4>
|
|
<div style={{ maxHeight: isExpanded ? 'none' : '200px', overflow: 'auto' }}>
|
|
{transcript.map((entry, index) => (
|
|
<div key={index} style={transcriptEntryStyle}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
|
|
<span style={{ fontSize: '11px', fontWeight: 'bold', color: '#007bff' }}>
|
|
{entry.speaker}
|
|
</span>
|
|
<span style={{ fontSize: '10px', color: '#666' }}>
|
|
{formatTimestamp(entry.timestamp)}
|
|
</span>
|
|
</div>
|
|
<div style={{ fontSize: '11px', lineHeight: '1.4' }}>
|
|
{entry.text}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty state */}
|
|
{!summary && transcript.length === 0 && actionItems.length === 0 && (
|
|
<div style={{
|
|
textAlign: 'center',
|
|
color: '#666',
|
|
fontSize: '12px',
|
|
padding: '20px',
|
|
fontStyle: 'italic'
|
|
}}>
|
|
No meeting data available
|
|
</div>
|
|
)}
|
|
</div>
|
|
</StandardizedToolWrapper>
|
|
</HTMLContainer>
|
|
)
|
|
}
|
|
|
|
indicator(shape: IFathomTranscript) {
|
|
return <rect width={shape.props.w} height={shape.props.h} />
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|