Skip to content

Commit 92a2b40

Browse files
authored
feat(pages): custom prompts (#10985)
Signed-off-by: Matt Krick <matt.krick@gmail.com>
1 parent 1b04e56 commit 92a2b40

File tree

19 files changed

+376
-216
lines changed

19 files changed

+376
-216
lines changed

.config/kyselyMigrations.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// so kysely.config.ts is not required by the bundle, only in dev
33

44
export const migrations = {
5+
// Uncomment this if you need to fix your local DB migration order!
6+
// allowUnorderedMigrations: true,
57
getMigrationPrefix: () => `${new Date().toISOString()}_`,
68
migrationFolder: './packages/server/postgres/migrations',
79
migrationTableSchema: 'public',

codegen.json

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"AzureDevOpsIntegration": ".types/AzureDevOpsIntegration#AzureDevOpsIntegrationSource",
7474
"AzureDevOpsRemoteProject": "./types/AzureDevOpsRemoteProject#AzureDevOpsRemoteProjectSource",
7575
"AzureDevOpsWorkItem": "../../dataloader/azureDevOpsLoaders#AzureDevOpsWorkItem",
76+
"AIPrompt": "../../postgres/types/index#AIPrompt",
7677
"BatchArchiveTasksSuccess": "./types/BatchArchiveTasksSuccess#BatchArchiveTasksSuccessSource",
7778
"Comment": "../../postgres/types/index#Comment as CommentDB",
7879
"Company": "./types/Company#CompanySource",

docker/images/parabol-ubi/environments/pipeline

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ PGADMIN_DEFAULT_EMAIL=''
3535
PGADMIN_DEFAULT_PASSWORD=''
3636
PGSSLMODE=''
3737
PORT='3000'
38+
HOCUS_POCUS_PORT='3003'
3839
# Database configurations must be the same used in the build.yml Github workflow
3940
POSTGRES_PASSWORD='temppassword'
4041
POSTGRES_USER='tempuser'

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@
107107
"html-webpack-plugin": "^5.5.0",
108108
"husky": "^7.0.4",
109109
"jscodeshift": "^0.14.0",
110-
"kysely": "^0.27.5",
110+
"kysely": "^0.27.6",
111111
"kysely-codegen": "^0.17.0",
112-
"kysely-ctl": "^0.11.0",
112+
"kysely-ctl": "^0.12.1",
113113
"lerna": "^6.4.1",
114114
"marked": "^15.0.7",
115115
"mini-css-extract-plugin": "^2.7.2",

packages/client/tiptap/extensions/insightsBlock/InsightsBlock.ts

+8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface InsightsBlockAttrs {
1616
title: string
1717
id: string
1818
hash: string
19+
prompt: string
1920
}
2021

2122
declare module '@tiptap/core' {
@@ -101,6 +102,13 @@ export const InsightsBlock = InsightsBlockBase.extend<never, {markdown: Markdown
101102
renderHTML: (attributes) => ({
102103
'data-hash': attributes.hash
103104
})
105+
},
106+
prompt: {
107+
default: '',
108+
parseHTML: (element) => element.getAttribute('data-prompt'),
109+
renderHTML: (attributes) => ({
110+
'data-prompt': attributes.prompt
111+
})
104112
}
105113
}
106114
},

packages/client/tiptap/extensions/insightsBlock/InsightsBlockEditing.tsx

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {type NodeViewProps} from '@tiptap/react'
22
import graphql from 'babel-plugin-relay/macro'
33
import type {InsightsBlockEditingQuery} from '../../../__generated__/InsightsBlockEditingQuery.graphql'
4+
import Ellipsis from '../../../components/Ellipsis/Ellipsis'
45
import {MeetingDatePicker} from '../../../components/MeetingDatePicker'
56
import {MeetingTypePickerCombobox} from '../../../components/MeetingTypePickerCombobox'
67
import {SpecificMeetingPickerRoot} from '../../../components/SpecificMeetingPickerRoot'
@@ -10,33 +11,37 @@ import useMutationProps from '../../../hooks/useMutationProps'
1011
import {Button} from '../../../ui/Button/Button'
1112
import {quickHash} from '../../../utils/quickHash'
1213
import type {InsightsBlockAttrs} from './InsightsBlock'
14+
import {InsightsBlockPromptRoot} from './InsightsBlockPromptRoot'
1315

1416
const queryNode = graphql`
15-
query InsightsBlockEditingQuery($meetingIds: [ID!]!) {
17+
query InsightsBlockEditingQuery($meetingIds: [ID!]!, $prompt: String!) {
1618
viewer {
17-
pageInsights(meetingIds: $meetingIds)
19+
pageInsights(meetingIds: $meetingIds, prompt: $prompt)
1820
}
1921
}
2022
`
2123

2224
export const InsightsBlockEditing = (props: NodeViewProps) => {
2325
const {editor, node, updateAttributes} = props
2426
const attrs = node.attrs as InsightsBlockAttrs
25-
const {id, after, before, meetingTypes, teamIds, meetingIds, hash, title} = attrs
27+
const {id, after, before, meetingTypes, teamIds, meetingIds, hash, title, prompt} = attrs
2628
const canQueryMeetings = teamIds.length > 0 && meetingTypes.length > 0 && after && before
2729
const {submitting, submitMutation, onCompleted} = useMutationProps()
2830
const atmosphere = useAtmosphere()
2931
const disabled = submitting || meetingIds.length < 1
3032

3133
const generateInsights = async () => {
3234
if (disabled) return
33-
const resultsHash = await quickHash(meetingIds)
35+
const resultsHash = await quickHash([...meetingIds, prompt])
3436
if (resultsHash === hash) {
3537
updateAttributes({editing: false})
3638
return
3739
}
3840
submitMutation()
39-
const res = await atmosphere.fetchQuery<InsightsBlockEditingQuery>(queryNode, {meetingIds})
41+
const res = await atmosphere.fetchQuery<InsightsBlockEditingQuery>(queryNode, {
42+
meetingIds,
43+
prompt
44+
})
4045
onCompleted()
4146
if (res instanceof Error) {
4247
atmosphere.eventEmitter.emit('addSnackbar', {
@@ -67,7 +72,7 @@ export const InsightsBlockEditing = (props: NodeViewProps) => {
6772
}}
6873
value={title}
6974
/>
70-
<div className='grid grid-cols-[auto_1fr] gap-4 p-4'>
75+
<div className='grid grid-cols-[auto_1fr] gap-4 py-4'>
7176
{/* Row 1 */}
7277
<label className='self-center font-semibold'>Teams</label>
7378
<TeamPickerComboboxRoot updateAttributes={updateAttributes} attrs={attrs} />
@@ -81,6 +86,7 @@ export const InsightsBlockEditing = (props: NodeViewProps) => {
8186
{canQueryMeetings && (
8287
<SpecificMeetingPickerRoot updateAttributes={updateAttributes} attrs={attrs} />
8388
)}
89+
<InsightsBlockPromptRoot updateAttributes={updateAttributes} attrs={attrs} />
8490
<div className='flex justify-end p-4 select-none'>
8591
<Button
8692
variant='secondary'
@@ -90,6 +96,7 @@ export const InsightsBlockEditing = (props: NodeViewProps) => {
9096
disabled={disabled}
9197
>
9298
Generate Insights
99+
{submitting ? <Ellipsis /> : undefined}
93100
</Button>
94101
</div>
95102
</>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import HistoryIcon from '@mui/icons-material/History'
2+
import type {NodeViewProps} from '@tiptap/core'
3+
import graphql from 'babel-plugin-relay/macro'
4+
import dayjs from 'dayjs'
5+
import {useRef} from 'react'
6+
import {usePreloadedQuery, type PreloadedQuery} from 'react-relay'
7+
import {Menu} from '~/ui/Menu/Menu'
8+
import {MenuContent} from '~/ui/Menu/MenuContent'
9+
import {MenuItem} from '~/ui/Menu/MenuItem'
10+
import type {InsightsBlockPromptQuery} from '../../../__generated__/InsightsBlockPromptQuery.graphql'
11+
import type {InsightsBlockAttrs} from './InsightsBlock'
12+
13+
const query = graphql`
14+
query InsightsBlockPromptQuery {
15+
viewer {
16+
aiPrompts {
17+
id
18+
content
19+
isUserDefined
20+
lastUsedAt
21+
title
22+
}
23+
}
24+
}
25+
`
26+
27+
interface Props {
28+
updateAttributes: NodeViewProps['updateAttributes']
29+
attrs: InsightsBlockAttrs
30+
queryRef: PreloadedQuery<InsightsBlockPromptQuery>
31+
}
32+
33+
export const InsightsBlockPrompt = (props: Props) => {
34+
const {attrs, queryRef, updateAttributes} = props
35+
const data = usePreloadedQuery<InsightsBlockPromptQuery>(query, queryRef)
36+
const {viewer} = data
37+
const {aiPrompts} = viewer
38+
const textAreaRef = useRef<HTMLTextAreaElement>(null)
39+
const {prompt} = attrs
40+
const defaultValue = prompt || aiPrompts[0]?.content || ''
41+
return (
42+
<div>
43+
<h3 className='pt-2'>What do you want to know?</h3>
44+
<div className='relative w-full'>
45+
<div className='absolute top-0 right-0'>
46+
<Menu
47+
trigger={
48+
<button className='bg-inherit p-1 outline-none'>
49+
<HistoryIcon className='cursor-pointer' />
50+
</button>
51+
}
52+
>
53+
<MenuContent align='end' sideOffset={4}>
54+
{aiPrompts
55+
.toSorted((a, b) => {
56+
if (a.isUserDefined !== b.isUserDefined) return a.isUserDefined ? -1 : 1
57+
return a.lastUsedAt > b.lastUsedAt ? -1 : 1
58+
})
59+
.map((prompt) => {
60+
const {id, lastUsedAt, content, title, isUserDefined} = prompt
61+
const subtitle = isUserDefined
62+
? dayjs(lastUsedAt).format('ddd MMM D')
63+
: 'Provided by Parabol'
64+
return (
65+
<MenuItem
66+
key={id}
67+
onClick={() => {
68+
textAreaRef.current!.value = content
69+
updateAttributes({prompt: content})
70+
}}
71+
className='w-80 flex-col items-start justify-start'
72+
>
73+
<div className='w-72 overflow-hidden text-ellipsis whitespace-nowrap'>
74+
{title}
75+
</div>
76+
<div className='text-xs font-bold text-slate-600'>{subtitle}</div>
77+
{}
78+
</MenuItem>
79+
)
80+
})}
81+
</MenuContent>
82+
</Menu>
83+
</div>
84+
<textarea
85+
defaultValue={defaultValue}
86+
autoComplete='on'
87+
autoCorrect='on'
88+
spellCheck={true}
89+
minLength={10}
90+
placeholder={'Summarize the meeting data and extract key insights...'}
91+
ref={textAreaRef}
92+
rows={13}
93+
cols={30}
94+
onChange={(e) => {
95+
updateAttributes({prompt: e.target.value})
96+
}}
97+
className='max-h-96 min-h-14 w-full resize-y rounded-md pr-6 outline-hidden focus:ring-2'
98+
></textarea>
99+
</div>
100+
</div>
101+
)
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type {NodeViewProps} from '@tiptap/core'
2+
import {Suspense} from 'react'
3+
import type {InsightsBlockPromptQuery} from '../../../__generated__/InsightsBlockPromptQuery.graphql'
4+
import query from '../../../__generated__/InsightsBlockPromptQuery.graphql'
5+
import useQueryLoaderNow from '../../../hooks/useQueryLoaderNow'
6+
import {Loader} from '../../../utils/relay/renderLoader'
7+
import type {InsightsBlockAttrs} from './InsightsBlock'
8+
import {InsightsBlockPrompt} from './InsightsBlockPrompt'
9+
10+
interface Props {
11+
updateAttributes: NodeViewProps['updateAttributes']
12+
attrs: InsightsBlockAttrs
13+
}
14+
15+
export const InsightsBlockPromptRoot = (props: Props) => {
16+
const {attrs, updateAttributes} = props
17+
const queryRef = useQueryLoaderNow<InsightsBlockPromptQuery>(query)
18+
return (
19+
<Suspense fallback={<Loader />}>
20+
{queryRef && (
21+
<InsightsBlockPrompt
22+
queryRef={queryRef}
23+
attrs={attrs}
24+
updateAttributes={updateAttributes}
25+
/>
26+
)}
27+
</Suspense>
28+
)
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import getKysely from '../../../postgres/getKysely'
2+
import {getUserId} from '../../../utils/authorization'
3+
import {UserResolvers} from '../resolverTypes'
4+
5+
export const aiPrompts: NonNullable<UserResolvers['aiPrompts']> = async (
6+
_source,
7+
_args,
8+
{authToken}
9+
) => {
10+
const viewerId = getUserId(authToken)
11+
const pg = getKysely()
12+
const aiPrompts = await pg
13+
.selectFrom('AIPrompt')
14+
.selectAll()
15+
.where('userId', 'in', [viewerId, 'aGhostUser'])
16+
.orderBy('lastUsedAt', 'desc')
17+
.execute()
18+
return aiPrompts
19+
}

0 commit comments

Comments
 (0)