Skip to content

Commit

Permalink
feat: copy insights to markdown (#10963)
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Krick <[email protected]>
  • Loading branch information
mattkrick authored Mar 7, 2025
1 parent c711382 commit be3b3d1
Show file tree
Hide file tree
Showing 15 changed files with 108 additions and 102 deletions.
2 changes: 1 addition & 1 deletion packages/client/components/SpecificMeetingPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const SpecificMeetingPicker = (props: Props) => {
const columns = allColumns.filter((column) => !ignoredColumns.includes(column))
const allChecked = meetingIds.length === edges.length
return (
<div className='flex max-h-52 overflow-auto'>
<div className='flex max-h-52 overflow-auto' contentEditable={false}>
<table className='w-full border-collapse'>
<thead className='sticky top-0 z-10 bg-slate-200'>
<tr className='border-b-[1px] border-slate-400'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
export const StandardBubbleMenu = (props: Props) => {
const {editor} = props
const shouldShowBubbleMenu = () => {
if (!editor || editor.isActive('link') || editor.isActive('insightsBlock')) return false
if (!editor || editor.isActive('link')) return false
return isTextSelected(editor)
}

Expand Down
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
"linkify-it": "^2.0.3",
"mousetrap": "^1.6.3",
"ms": "^2.0.0",
"node-html-markdown-cloudflare": "^1.3.0",
"react": "^17.0.2",
"react-beautiful-dnd": "13.1.1",
"react-chartjs-2": "^4.2.0",
Expand Down
8 changes: 0 additions & 8 deletions packages/client/styles/theme/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -446,14 +446,6 @@
@apply overflow-hidden rounded-md;
}
}

.node-insightsBlock {
@apply relative;
&.has-focus > div::after {
content: '';
@apply pointer-events-none absolute inset-0 h-full w-full bg-[#2383e247] select-none;
}
}
}

.ProseMirror .search-result {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface InsightsBlockAttrs {
meetingIds: string[]
title: string
id: string
hash: string
}

declare module '@tiptap/core' {
Expand Down Expand Up @@ -75,6 +76,13 @@ export const InsightsBlock = InsightsBlockBase.extend({
renderHTML: (attributes) => ({
'data-title': attributes.title
})
},
hash: {
default: '',
parseHTML: (element) => element.getAttribute('data-hash'),
renderHTML: (attributes) => ({
'data-hash': attributes.hash
})
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {TeamPickerComboboxRoot} from '../../../components/TeamPickerComboboxRoot
import useAtmosphere from '../../../hooks/useAtmosphere'
import useMutationProps from '../../../hooks/useMutationProps'
import {Button} from '../../../ui/Button/Button'
import {quickHash} from '../../../utils/quickHash'
import type {InsightsBlockAttrs} from './InsightsBlock'

const queryNode = graphql`
Expand All @@ -21,14 +22,19 @@ const queryNode = graphql`
export const InsightsBlockEditing = (props: NodeViewProps) => {
const {editor, node, updateAttributes} = props
const attrs = node.attrs as InsightsBlockAttrs
const {id, after, before, meetingTypes, teamIds, meetingIds} = attrs
const {id, after, before, meetingTypes, teamIds, meetingIds, hash, title} = attrs
const canQueryMeetings = teamIds.length > 0 && meetingTypes.length > 0 && after && before
const {submitting, submitMutation, onCompleted} = useMutationProps()
const atmosphere = useAtmosphere()
const disabled = submitting || meetingIds.length < 1

const generateInsights = async () => {
if (disabled) return
const resultsHash = await quickHash(meetingIds)
if (resultsHash === hash) {
updateAttributes({editing: false})
return
}
submitMutation()
const res = await atmosphere.fetchQuery<InsightsBlockEditingQuery>(queryNode, {meetingIds})
onCompleted()
Expand All @@ -42,19 +48,25 @@ export const InsightsBlockEditing = (props: NodeViewProps) => {
}
const {viewer} = res
const {pageInsights} = viewer

const insightsNode = editor.$node('insightsBlock', {id})!
editor.commands.insertContentAt(
{
from: insightsNode.from,
to: insightsNode.to - 1
},
pageInsights
`<h1>${title}</h1>` + pageInsights
)
updateAttributes({editing: false})
updateAttributes({editing: false, hash: resultsHash})
}
return (
<>
<input
className='bg-inherit p-4 text-lg ring-0 outline-0'
onChange={(e) => {
updateAttributes({title: e.target.value})
}}
value={title}
/>
<div className='grid grid-cols-[auto_1fr] gap-4 p-4'>
{/* Row 1 */}
<label className='self-center font-semibold'>Teams</label>
Expand All @@ -69,7 +81,7 @@ export const InsightsBlockEditing = (props: NodeViewProps) => {
{canQueryMeetings && (
<SpecificMeetingPickerRoot updateAttributes={updateAttributes} attrs={attrs} />
)}
<div className='flex justify-end p-4'>
<div className='flex justify-end p-4 select-none'>
<Button
variant='secondary'
shape='pill'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,53 @@
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import ModelTrainingIcon from '@mui/icons-material/ModelTraining'
import {NodeViewContent, type NodeViewProps} from '@tiptap/react'
import {Button} from '../../../ui/Button/Button'
import {NodeHtmlMarkdown} from 'node-html-markdown-cloudflare'
import {Tooltip} from '../../../ui/Tooltip/Tooltip'
import {TooltipContent} from '../../../ui/Tooltip/TooltipContent'
import {TooltipTrigger} from '../../../ui/Tooltip/TooltipTrigger'
import type {InsightsBlockAttrs} from './InsightsBlock'

interface Props {
updateAttributes: NodeViewProps['updateAttributes']
}
export const InsightsBlockResult = (props: Props) => {
const {updateAttributes} = props
export const InsightsBlockResult = (props: NodeViewProps) => {
const {editor, node, updateAttributes} = props
const attrs = node.attrs as InsightsBlockAttrs
const {id} = attrs
return (
<>
<NodeViewContent className='outline-hidden' contentEditable />
<div className='flex justify-end p-4'>
<Button
variant='secondary'
shape='pill'
size='md'
onClick={() => {
updateAttributes({editing: true})
}}
>
New Query
</Button>
<NodeViewContent className='px-4 outline-hidden' />
<div className='absolute top-0 right-0 flex justify-end space-x-2 p-4'>
<Tooltip>
<TooltipTrigger asChild>
<button
className='cursor-pointer text-slate-600'
onClick={async () => {
const nodePos = editor.$node('insightsBlock', {id})!
const nodeEl = editor.view.domAtPos(nodePos.pos).node as HTMLDivElement
const markdown = NodeHtmlMarkdown.translate(nodeEl.outerHTML)
await navigator.clipboard.writeText(markdown)
}}
>
<ContentCopyIcon />
</button>
</TooltipTrigger>
<TooltipContent side='bottom' align='center'>
{'Copy'}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
className='cursor-pointer text-slate-600'
onClick={() => {
updateAttributes({editing: true})
}}
>
<ModelTrainingIcon />
</button>
</TooltipTrigger>
<TooltipContent side='bottom' align='center'>
{'Start over'}
</TooltipContent>
</Tooltip>
</div>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,15 @@ import type {InsightsBlockAttrs} from './InsightsBlock'
import {InsightsBlockEditing} from './InsightsBlockEditing'
import {InsightsBlockResult} from './InsightsBlockResult'
export const InsightsBlockView = (props: NodeViewProps) => {
const {node, updateAttributes} = props
const {node} = props
const attrs = node.attrs as InsightsBlockAttrs
const {editing, title} = attrs
const {editing} = attrs

return (
<NodeViewWrapper>
<div className='m-0 w-full p-0 text-slate-900' contentEditable={false}>
<NodeViewWrapper contentEditable={!editing}>
<div className='relative m-0 w-full p-0 text-slate-900'>
<div className='flex flex-col rounded-sm bg-slate-200 p-4'>
<input
className='bg-inherit p-4 text-lg ring-0 outline-0'
onChange={(e) => {
updateAttributes({title: e.target.value})
}}
value={title}
/>
{editing ? (
<InsightsBlockEditing {...props} />
) : (
<InsightsBlockResult updateAttributes={updateAttributes} />
)}
{editing ? <InsightsBlockEditing {...props} /> : <InsightsBlockResult {...props} />}
</div>
</div>
</NodeViewWrapper>
Expand Down
9 changes: 9 additions & 0 deletions packages/client/utils/quickHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const quickHash = async (ids: string[]) => {
const data = new TextEncoder().encode(ids.join(',')) // Convert array to byte array
const hashBuffer = await crypto.subtle.digest('SHA-256', data) // Hash using SHA-256
const hashArray = Array.from(new Uint8Array(hashBuffer)) // Convert buffer to array
return hashArray
.map((b) => b.toString(36))
.join('')
.substring(0, 8) // Base36 encoding + shorten
}
5 changes: 3 additions & 2 deletions packages/server/graphql/public/fields/pageInsights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Analyze this data and provide key insights on:
- Any **trends** in the conversations (e.g., recurring blockers, common frustrations, or successful strategies).
- Suggestions for improving efficiency based on the data.
Use a structured response format with **Wins**, **Challenges**, and **Recommendations**. No yapping.
Use a structured response format with **Wins**, **Challenges**, and **Recommendations**. No yapping. No introductory sentence. No horizontal rules to separate the sections. Use markdown formatting.
`

const systemContent = prompt || defaultPrompt
Expand All @@ -86,7 +86,6 @@ Use a structured response format with **Wins**, **Challenges**, and **Recommenda
}
]
})
console.log('got response', rawInsightResponse)
const rawInsight = rawInsightResponse?.choices[0]?.message?.content
if (!rawInsight) throw new Error('Could not fetch insights from provider')
const tokenCost = rawInsightResponse?.usage?.total_tokens ?? 10_000
Expand All @@ -98,5 +97,7 @@ Use a structured response format with **Wins**, **Challenges**, and **Recommenda
gfm: true,
breaks: true
})
console.log(rawInsight)
console.log(htmlInsight)
return htmlInsight
}
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
"ms": "^2.0.0",
"nest-graphql-endpoint": "0.8.1",
"node-env-flag": "0.1.0",
"node-html-markdown": "^1.3.0",
"node-html-markdown-cloudflare": "^1.3.0",
"nodemailer": "^6.9.9",
"oauth-1.0a": "^2.2.6",
"openai": "^4.86.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute()
}

export async function down(db: Kysely<any>): Promise<void> {}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('AIRequest').execute()
}
2 changes: 1 addition & 1 deletion packages/server/utils/convertTipTapToMarkdown.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {JSONContent} from '@tiptap/core'
import {generateHTML} from '@tiptap/html'
import {NodeHtmlMarkdown} from 'node-html-markdown'
import {NodeHtmlMarkdown} from 'node-html-markdown-cloudflare'
import {serverTipTapExtensions} from 'parabol-client/shared/tiptap/serverTipTapExtensions'
export const convertTipTapToMarkdown = (content: JSONContent) => {
const html = generateHTML(content, serverTipTapExtensions)
Expand Down
11 changes: 0 additions & 11 deletions static/css/Draft.css

This file was deleted.

Loading

0 comments on commit be3b3d1

Please sign in to comment.