Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@use '@styles/Spacings.module';
@use '@styles/Typography.module';
@use '@styles/Radius.module';

.main_container {
display: flex;
Expand All @@ -21,4 +22,46 @@
justify-content: space-between;
gap: Spacings.$spacing03;
}

.feedback_container {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: Spacings.$spacing06;
height: 100%;

.feedback_buttons {
display: flex;
align-items: center;
gap: Spacings.$spacing03;
justify-content: flex-end;
}

.responses_container {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: Spacings.$spacing05;
padding-top: Spacings.$spacing06;

.response_container {
display: flex;
flex-direction: column;
gap: Spacings.$spacing02;

.response_title {
font-weight: 600;
}

.response_content {
background-color: var(--background-3);
padding: Spacings.$spacing05;
font-size: Typography.$tiny;
line-height: 18px;
border-radius: Radius.$normal;
max-height: 350px;
overflow-y: auto;
}
}
}
}
}
147 changes: 114 additions & 33 deletions zendesk_app/src/app/components/AutosendModal/AutosendModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,144 @@ import styles from './AutosendModal.module.scss'
import { ZAFClient } from '../../contexts/ClientProvider'
import { useClient } from '../../hooks/useClient'
import { useQuivrApiContext } from '../../hooks/useQuivrApiContext'
import { ZendeskConversationEntry, ZendeskMessage } from 'src/app/types/zendesk'

export interface AutosendModalProps {
export interface AutosendModalProps {
ticketAnswerId: string
predictionId: string
askForFeedback: boolean
response: string
endUserMessage: ZendeskConversationEntry
}

export const AutosendModal = ({ ticketAnswerId, predictionId }: AutosendModalProps): JSX.Element => {
export const AutosendModal = ({
ticketAnswerId,
predictionId,
askForFeedback,
response,
endUserMessage
}: AutosendModalProps): JSX.Element => {
const client = useClient() as ZAFClient
const { quivrService } = useQuivrApiContext()

useEffect(() => {
const updatePredictionAcceptance = async () => {
if (!quivrService) return
if (!askForFeedback) {
updatePredictionAcceptance(true)
}
}, [quivrService])

const updatePredictionAcceptance = async (isAccepted: boolean) => {
if (!quivrService) return

try {
await quivrService.updatePredictionAcceptance(predictionId, ticketAnswerId, true)
} catch (error) {
console.error('Error sending prediction acceptance', error)
try {
if (!isAccepted) {
client.trigger('modal.reject_draft')
client.invoke('destroy')
}
}

updatePredictionAcceptance()
}, [quivrService])
await quivrService.updatePredictionAcceptance(predictionId, ticketAnswerId, isAccepted)
} catch (error) {
console.error('Error sending prediction acceptance', error)
}
}

const handleSend = async () => {
client.trigger('modal.send_draft')
client.invoke('destroy')
client.trigger('modal.send_draft')
client.invoke('destroy')
}

const handleEdit = async () => {
client.trigger('modal.copy_draft')
client.invoke('destroy')
}

const getEndUserContentHtml = (conversationEntry: ZendeskConversationEntry) => {
const messageContent = (conversationEntry.message.content || '').replace(
/<a\b(?![^>]*\btarget=)[^>]*>/gi,
(match: string) => {
// Add target and rel attributes to open links in a new tab
return match.replace(/<a/, '<a target="_blank" rel="noopener noreferrer"');
}
);

const attachmentLinks = conversationEntry.attachments
?.map(
(att: any) => `
<div style="margin-top: 1em;">
<a href="${att.contentUrl}" target="_blank" rel="noopener noreferrer">
📎 ${att.filename}
</a>
</div>
`
)
.join('') || '';

return `${messageContent}${attachmentLinks}`;
};

return (
<div className={styles.main_container}>
<div className={styles.text}>
<div>
<p>Thank you for your feedback!</p>
<p>Quivr has marked this message as ready to send.</p>
{askForFeedback ? (
<div className={styles.feedback_container}>
<div>
<p>This answer has been marked as ready to send.</p>
<p>Would you like to review it and confirm if it’s sendable or not?</p>

<div className={styles.responses_container}>
<div className={styles.response_container}>
<p className={styles.response_title}>Customer message</p>
<div
className={styles.response_content}
contentEditable={false}
dangerouslySetInnerHTML={{ __html: getEndUserContentHtml(endUserMessage) }}
/>
</div>
<div className={styles.response_container}>
<p className={styles.response_title}>Quivr's generated answer</p>
<div
className={styles.response_content}
contentEditable={false}
dangerouslySetInnerHTML={{ __html: response }}
/>
</div>
</div>
</div>
<div className={styles.buttons}>
<QuivrButton label="Close modal" onClick={() => client.invoke('destroy')} color="dangerous" />
<div className={styles.feedback_buttons}>
<QuivrButton
label="No, it is not sendable"
onClick={() => updatePredictionAcceptance(false)}
color="zendesk-secondary"
iconName="close"
/>
<QuivrButton
label="Yes, it is sendable"
onClick={() => {
updatePredictionAcceptance(true)
handleEdit()
}}
color="zendesk"
iconName="check"
/>
</div>
</div>
</div>
<p>Would you like to send it like this, or make a quick edit first?</p>
</div>
<div className={styles.buttons}>
<QuivrButton
label="Continue editing"
onClick={handleEdit}
color="zendesk-secondary"
iconName="edit"
/>
<QuivrButton
label="Send"
onClick={handleSend}
color="zendesk"
iconName="send"
/>
</div>
) : (
<>
<div className={styles.text}>
<div>
<p>Thank you for your feedback!</p>
<p>Quivr has marked this message as ready to send.</p>
</div>
<p>Would you like to send it like this, or make a quick edit first?</p>
</div>
<div className={styles.buttons}>
<QuivrButton label="Continue editing" onClick={handleEdit} color="zendesk-secondary" iconName="edit" />
<QuivrButton label="Send" onClick={handleSend} color="zendesk" iconName="send" />
</div>
</>
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Autodraft } from '../../../../types/zendesk'
import { Icon } from '../../../../shared/components/Icon/Icon'
import Tooltip from '../../../../shared/components/Tooltip/Tooltip'

const subdomainsEligibleToAutosend = ['getquivr', 'd3v-quivr', 'trusk']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

y a pas un autre moyen que d'ecrire ca en dur ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tu voudrais le mettre où ? pas trop envie de le mettre en base vu que c'est très expérimentale. L'idéal ça serait de le mettre dans un feature flag, mais pour ça faut intégrer Posthog dans Zendesk (je vais regarder ça prochainement)


interface ResponseContainerProps {
responseContent: string
setResponseContent: (content: string) => void
Expand All @@ -31,7 +33,7 @@ export const ResponseContainer = ({
const [manualEditing, setManualEditing] = useState(false)
const [rating, setRating] = useState(0)
const client = useClient() as ZAFClient
const { sendMessage } = useZendesk()
const { sendMessage, getLatestEndUserMessage, getSubdomain } = useZendesk()
const { quivrService } = useQuivrApiContext()
const [isAutosendableFeedbackOpen, setIsAutosendableFeedbackOpen] = useState(true)

Expand All @@ -54,6 +56,21 @@ export const ResponseContainer = ({
}
}, [ongoingTask])

useEffect(() => {
const checkAutosendModal = async () => {
const subdomain = await getSubdomain(client)
if (
autoDraft?.prediction?.is_autosendable &&
autoDraft?.prediction?.is_accepted === null &&
htmlContent !== '' &&
subdomainsEligibleToAutosend.includes(subdomain)
) {
openFeedbackModal({ autosendable: true, askForFeedback: true })
}
}
void checkAutosendModal()
}, [autoDraft, htmlContent, client])

const handleInput = (event: React.FormEvent<HTMLDivElement>) => {
if (autoDraft?.prediction?.is_autosendable) {
return
Expand All @@ -77,22 +94,36 @@ export const ResponseContainer = ({
}
}

const openFeedbackModal = ({ autosendable = false }: { autosendable?: boolean }) => {
const openFeedbackModal = async ({
autosendable = false,
askForFeedback = false
}: {
autosendable?: boolean
askForFeedback?: boolean
}) => {
client
.invoke('instances.create', {
location: 'modal',
url: 'http://localhost:3000/modal',
size: { width: '280px', height: autosendable ? '150px' : '300px' }
size: {
width: askForFeedback ? '600px' : '280px',
height: autosendable ? (askForFeedback ? '500px' : '150px') : '300px'
}
})
.then(async (modalContext) => {
const modalGuid = modalContext['instances.create'][0].instanceGuid
const modalClient = client.instance(modalGuid)

if (autosendable) {
const latestEndUserMessage = await getLatestEndUserMessage(client)

modalClient.on('modal.ready', function () {
modalClient.trigger('modal.data_autosend', {
ticketAnswerId: autoDraft?.ticket_answer_id,
predictionId: autoDraft?.prediction?.prediction_id
predictionId: autoDraft?.prediction?.prediction_id,
askForFeedback: askForFeedback,
response: htmlContent,
endUserMessage: latestEndUserMessage
})
})

Expand All @@ -107,6 +138,10 @@ export const ResponseContainer = ({
await sendMessage(client, autoDraft?.generated_answer)
}
})

modalClient.on('modal.reject_draft', function () {
setIsAutosendableFeedbackOpen(false)
})
} else {
modalClient.on('modal.ready', function () {
modalClient.trigger('modal.data', {
Expand Down Expand Up @@ -162,7 +197,7 @@ export const ResponseContainer = ({
</Tooltip>
<div className={styles.autosend_buttons_container}>
<button className={styles.autosend_button} onClick={approveDraftResponse}>
<Icon name="check" color="success" size="small" />
<Icon name="check" color="success" size="normal" />
</button>
<button className={styles.autosend_button} onClick={rejectDraftResponse}>
<Icon name="close" color="dangerous" size="normal" />
Expand Down
18 changes: 16 additions & 2 deletions zendesk_app/src/app/hooks/useZendesk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ZAFClient } from '../contexts/ClientProvider'
import { ZendeskUser } from '../types/zendesk'
import { ZendeskConversationEntry, ZendeskUser } from '../types/zendesk'

export const useZendesk = () => {
async function getHistoric(client: ZAFClient): Promise<string[]> {
Expand All @@ -24,6 +24,19 @@ export const useZendesk = () => {
return client.get('ticket.requester').then((data) => data['ticket.requester'].name)
}

async function getLatestEndUserMessage(client: ZAFClient): Promise<ZendeskConversationEntry | null> {
const data = await client.get('ticket.conversation');
const conversation = data['ticket.conversation'];

if (!Array.isArray(conversation)) return null;

const latestMessage = conversation
.filter(msg => msg.author.role === 'end-user' && msg.message?.content)
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0];

return latestMessage || null;
}

async function getTicketId(client: ZAFClient): Promise<string> {
return client.get('ticket.id').then((data) => data['ticket.id'])
}
Expand Down Expand Up @@ -74,6 +87,7 @@ export const useZendesk = () => {
getRequesterEmail,
getSubdomain,
pasteInEditor,
sendMessage
sendMessage,
getLatestEndUserMessage
}
}
3 changes: 2 additions & 1 deletion zendesk_app/src/app/shared/helpers/iconList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import { RiDeleteBackLine, RiDeleteBin6Line, RiHashtag, RiNotification2Line } fr
import { SlOptionsVertical } from 'react-icons/sl'
import { TbNetwork, TbProgress, TbRobot } from 'react-icons/tb'
import { VscGraph } from 'react-icons/vsc'
import { IoMdCheckmark } from "react-icons/io";

export const iconList: { [name: string]: IconType } = {
addWithoutCircle: IoIosAdd,
Expand All @@ -131,7 +132,7 @@ export const iconList: { [name: string]: IconType } = {
calendar: FaCalendar,
chair: PiOfficeChairFill,
chat: IoChatbubbleEllipsesOutline,
check: FaCheck,
check: IoMdCheckmark,
checkCircle: FaCheckCircle,
chevronDown: LuChevronDown,
chevronLeft: LuChevronLeft,
Expand Down
Loading