Skip to content

Commit 08a7eef

Browse files
authored
feat(opentrons-ai-client, opentrons-ai-server): content field (#19080)
# Overview Closes AUTH-2159 This PR addresses the handling of file attachments in the multipart upload flow by restructuring how files are managed between the frontend and backend. The primary focus is fixing the `content` field for file attachments. This field is essential for maintaining chat history, enabling users to reference files across multi-turn conversations. Currently, the chat system can only process files on a per-message basis. While users can attach a file and ask a question about it in a single message, the system loses context in subsequent turns. This happens because the frontend only sends files attached to the current message, rather than maintaining file references throughout the conversation history. File attachments persist across the entire conversation, allowing for more natural multi-turn interactions with uploaded files. ## Test Plan and Hands on Testing CI ## Review requests Multi-turn conversations with files: Upload pdf, csv, or python then ask questions. ## Risk assessment Mid
1 parent 7d61119 commit 08a7eef

File tree

11 files changed

+577
-349
lines changed

11 files changed

+577
-349
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export const ANALYTICS = {
2+
// App lifecycle
3+
APP_OPEN: 'appOpen',
4+
5+
// Authentication
6+
USER_LOGIN: 'user-login',
7+
USER_LOGOUT: 'user-logout',
8+
9+
// Chat interactions
10+
CHAT_SUBMITTED: 'chat-submitted',
11+
SUBMIT_PROMPT: 'submit-prompt',
12+
13+
// Protocol actions
14+
GENERATED_PROTOCOL: 'generated-protocol',
15+
REGENERATE_PROTOCOL: 'regenerate-protocol',
16+
DOWNLOAD_PROTOCOL: 'download-protocol',
17+
COPY_PROTOCOL: 'copy-protocol',
18+
19+
// Navigation
20+
CREATE_NEW_PROTOCOL: 'create-new-protocol',
21+
UPDATE_PROTOCOL: 'update-protocol',
22+
GO_TO_CHAT: 'go-to-chat',
23+
24+
// Feedback
25+
FEEDBACK_SENT: 'feedback-sent',
26+
} as const
27+
28+
export type AnalyticsTrackEvent = typeof ANALYTICS[keyof typeof ANALYTICS]

opentrons-ai-client/src/molecules/FeedbackModal/__tests__/FeedbackModal.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'
22
import { beforeEach, describe, expect, it, vi } from 'vitest'
33

44
import { renderWithProviders } from '/ai-client/__testing-utils__'
5+
import { ANALYTICS } from '/ai-client/analytics/constants'
56
import { feedbackModalAtom } from '/ai-client/resources/atoms'
67

78
import { FeedbackModal } from '..'
@@ -21,6 +22,9 @@ vi.mock('/ai-client/hooks/useTrackEvent', () => ({
2122
vi.mock('/ai-client/resources/hooks', () => ({
2223
useApiCall: () => ({
2324
callApi: mockCallApi,
25+
error: null,
26+
isLoading: false,
27+
data: { success: true },
2428
}),
2529
}))
2630

@@ -77,7 +81,7 @@ describe('FeedbackModal', () => {
7781
// Then wait for the tracking event to be triggered
7882
await waitFor(() => {
7983
expect(mockUseTrackEvent).toHaveBeenCalledWith({
80-
name: 'feedback-sent',
84+
name: ANALYTICS.FEEDBACK_SENT,
8185
properties: {
8286
feedback: 'This is a test feedback',
8387
},
Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { useState } from 'react'
1+
import { useEffect, useState } from 'react'
22
import { useTranslation } from 'react-i18next'
33
import { useAtom } from 'jotai'
44

55
import {
66
ALIGN_FLEX_END,
7+
COLORS,
8+
DIRECTION_COLUMN,
79
Flex,
810
InputField,
911
Modal,
@@ -13,6 +15,7 @@ import {
1315
StyledText,
1416
} from '@opentrons/components'
1517

18+
import { ANALYTICS } from '/ai-client/analytics/constants'
1619
import { feedbackModalAtom, tokenAtom } from '/ai-client/resources/atoms'
1720
import {
1821
LOCAL_FEEDBACK_END_POINT,
@@ -31,51 +34,57 @@ export function FeedbackModal(): JSX.Element {
3134
const [feedbackValue, setFeedbackValue] = useState<string>('')
3235
const [, setShowFeedbackModal] = useAtom(feedbackModalAtom)
3336
const [token] = useAtom(tokenAtom)
34-
const { callApi } = useApiCall()
37+
const { callApi, error, isLoading, data } = useApiCall()
38+
const [isSubmitting, setIsSubmitting] = useState<boolean>(false)
3539

3640
const handleSendFeedback = async (): Promise<void> => {
37-
try {
38-
const headers = {
39-
Authorization: `Bearer ${token}`,
40-
'Content-Type': 'application/json',
41-
}
41+
const headers = {
42+
Authorization: `Bearer ${token}`,
43+
'Content-Type': 'application/json',
44+
}
4245

43-
const getEndpoint = (): string => {
44-
switch (process.env.NODE_ENV) {
45-
case 'production':
46-
return PROD_FEEDBACK_END_POINT
47-
case 'development':
48-
return LOCAL_FEEDBACK_END_POINT
49-
default:
50-
return STAGING_FEEDBACK_END_POINT
51-
}
46+
const getEndpoint = (): string => {
47+
switch (process.env.NODE_ENV) {
48+
case 'production':
49+
return PROD_FEEDBACK_END_POINT
50+
case 'development':
51+
return LOCAL_FEEDBACK_END_POINT
52+
default:
53+
return STAGING_FEEDBACK_END_POINT
5254
}
55+
}
5356

54-
const url = getEndpoint()
57+
const url = getEndpoint()
5558

56-
const config = {
57-
url,
58-
method: 'POST',
59-
headers,
60-
data: {
61-
feedbackText: feedbackValue,
62-
fake: false,
63-
},
64-
}
65-
await callApi(config as AxiosRequestConfig)
66-
trackEvent({
67-
name: 'feedback-sent',
68-
properties: {
69-
feedback: feedbackValue,
70-
},
71-
})
72-
setShowFeedbackModal(false)
73-
} catch (err: any) {
74-
console.error(`error: ${err.message}`)
75-
throw err
59+
const config = {
60+
url,
61+
method: 'POST',
62+
headers,
63+
data: {
64+
feedbackText: feedbackValue,
65+
fake: false,
66+
},
7667
}
68+
setIsSubmitting(true)
69+
await callApi(config as AxiosRequestConfig)
7770
}
7871

72+
useEffect(() => {
73+
if (isSubmitting && !isLoading) {
74+
if (!error && data) {
75+
// Success - track event and close modal
76+
trackEvent({
77+
name: ANALYTICS.FEEDBACK_SENT,
78+
properties: {
79+
feedback: feedbackValue,
80+
},
81+
})
82+
setShowFeedbackModal(false)
83+
}
84+
setIsSubmitting(false)
85+
}
86+
}, [isSubmitting, isLoading, error, data, feedbackValue])
87+
7988
return (
8089
<Modal
8190
title={t(`send_feedback_to_opentrons`)}
@@ -98,7 +107,7 @@ export function FeedbackModal(): JSX.Element {
98107
</StyledText>
99108
</SecondaryButton>
100109
<PrimaryButton
101-
disabled={feedbackValue === ''}
110+
disabled={feedbackValue === '' || isLoading}
102111
onClick={async () => {
103112
await handleSendFeedback()
104113
}}
@@ -110,14 +119,21 @@ export function FeedbackModal(): JSX.Element {
110119
</Flex>
111120
}
112121
>
113-
<InputField
114-
title={t(`send_feedback_input_title`)}
115-
size="medium"
116-
value={feedbackValue}
117-
onChange={event => {
118-
setFeedbackValue(event.target.value as string)
119-
}}
120-
></InputField>
122+
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing16}>
123+
{error && (
124+
<StyledText desktopStyle="bodyDefaultRegular" color={COLORS.red50}>
125+
{error}
126+
</StyledText>
127+
)}
128+
<InputField
129+
title={t(`send_feedback_input_title`)}
130+
size="medium"
131+
value={feedbackValue}
132+
onChange={event => {
133+
setFeedbackValue(event.target.value as string)
134+
}}
135+
/>
136+
</Flex>
121137
</Modal>
122138
)
123139
}

0 commit comments

Comments
 (0)