Skip to content
Open
7 changes: 6 additions & 1 deletion frontend/src/components/ExpertButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default {
return this.featuresCheck.isAiFeatureEnabled
},
showExpertButton () {
return this.isAiEnabled && !this.isExpertDrawerOpen
return this.isAiEnabled && !this.isExpertDrawerOpen && !this.rightDrawer.expertSuppressed
},
showMcpToggle () {
return this.isAiEnabled && this.featuresCheck.isMcpThirdPartyFeatureEnabled
Expand Down Expand Up @@ -260,6 +260,11 @@ export default {
color: var(--ff-color-text-default);
}

&:only-child {
border-left: none;
border-radius: 5px;
}

&--waiting {
/* The -dot token, not -text: this is a standalone indicator, not text on a tinted pill */
color: var(--ff-color-status-warning-dot);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/expert/components/ExpertChatInput.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<div ref="resizeTarget" class="ff-expert-input" :style="containerStyle">
<resize-bar
v-if="expertSurface !== 'overview'"
:is-resizing="isInputResizing"
direction="horizontal"
@mousedown="onStartResize"
Expand Down Expand Up @@ -247,6 +248,7 @@ export default {
},
showSuggestions () {
if (this.expertSurface === 'onboarding') return false
if (this.expertSurface === 'overview') return false
if (this.isInsightsAgent) return false
if (this.suggestionUsed || this.suggestions.length === 0) return false
if (this.inputText.length > 0 || this.hasUserTurns) return false
Expand Down
318 changes: 318 additions & 0 deletions frontend/src/pages/team/Home/Expert/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
<template>
<ff-page>
<template #header>
<ff-page-header>
<template #breadcrumbs>
<ff-nav-breadcrumb>Home</ff-nav-breadcrumb>
</template>
</ff-page-header>
</template>

<div
class="ff-expert-home"
data-el="expert-home"
:class="{ 'is-composing': stage !== 'idle', 'is-conversing': stage === 'conversing' }"
:data-stage="stage"
>
<ff-button
kind="tertiary"
size="small"
class="ff-expert-home__back"
data-action="collapse-expert"
@click="stage = 'idle'"
>
<template #icon-left>
<ChevronLeftIcon />
</template>
Back
</ff-button>

<div v-if="canSwitchAgent" class="ff-expert-home__mode" data-el="expert-home-mode">
<ExpertModeSwitcher />
</div>

<div
class="ff-expert-home__expert"
data-el="expert-home-surface"
@input="onComposerInput"
>
<ExpertPanel />
</div>

<ff-button
v-if="canResume"
kind="tertiary"
size="small"
class="ff-expert-home__resume"
data-action="resume-conversation"
@click="stage = 'conversing'"
>
<span v-if="liveTurns > 0">
Continue your conversation
<span class="ff-expert-home__resume-count">
· {{ liveTurns }} {{ liveTurns === 1 ? 'message' : 'messages' }}
</span>
</span>
<span v-else>Open the Expert</span>
</ff-button>
</div>
</ff-page>
</template>

<script setup lang="ts">
import { ChevronLeftIcon } from '@heroicons/vue/20/solid'
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'

import ExpertPanel from '@/components/expert/Expert.vue'
import ExpertModeSwitcher from '@/components/expert/components/ExpertModeSwitcher.vue'
import { useAccountSettingsStore } from '@/stores/account-settings.js'
import { useContextStore } from '@/stores/context.js'
import { SUPPORT_AGENT } from '@/stores/product-expert-agents.js'
import { useProductExpertStore } from '@/stores/product-expert.js'
import { useUxDrawersStore } from '@/stores/ux-drawers.js'

defineOptions({ name: 'TeamHomeExpert' })

provide('expert-surface', 'overview')

const contextStore = useContextStore()
const settingsStore = useAccountSettingsStore()
const expertStore = useProductExpertStore() as ReturnType<typeof useProductExpertStore> & {
messages: { _type: string }[]
isWaitingForResponse: boolean
isSessionExpired: boolean
isInsightsAgent: boolean
hasSelectedCapabilities: boolean
}
const drawersStore = useUxDrawersStore()

const canSwitchAgent = computed<boolean>(() => {
const features = settingsStore.featuresCheck
return !!features.isExpertAssistantFeatureEnabled && !!features.isExpertInsightsFeatureEnabled
})

type Stage = 'idle' | 'composing' | 'conversing'

const stage = ref<Stage>('idle')
const liveTurns = computed<number>(() => expertStore.messages.filter(message => message._type === 'human').length)

const canResume = computed<boolean>(() =>
liveTurns.value > 0 ||
expertStore.isWaitingForResponse ||
expertStore.isSessionExpired ||
(expertStore.isInsightsAgent && !expertStore.hasSelectedCapabilities)
)

function onComposerInput () {
if (stage.value === 'idle') {
stage.value = 'composing'
}
}

watch(liveTurns, (now: number, before: number) => {
if (now > before) {
stage.value = 'conversing'
}
})

onMounted(() => {
const features = settingsStore.featuresCheck
if (features.isExpertAssistantFeatureEnabled && !features.isExpertInsightsFeatureEnabled) {
expertStore.setAgentMode(SUPPORT_AGENT)
}
expertStore.resumeSessionTimer()
drawersStore.suppressExpertDrawer()
if (drawersStore.rightDrawer.state) {
drawersStore.closeRightDrawer({ preserveExpertState: true })
}
})

onBeforeUnmount(() => {
drawersStore.releaseExpertDrawer()
if (!contextStore.isImmersiveEditor && drawersStore.rightDrawer.expertState.open) {
expertStore.openAssistantDrawer({ openPinned: drawersStore.rightDrawer.expertState.pinned })
}
})
</script>

<style scoped lang="scss">

$ff-expand: 480ms;
$ff-ease: cubic-bezier(0.4, 0, 0.2, 1);
$ff-column: 820px;

.ff-expert-home {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
padding: 24px 22px 28px;
position: relative;

&__back {
position: absolute;
top: 0;
left: 0;
z-index: 1;
transition: opacity 220ms ease, visibility 220ms;
}

&:not(.is-composing) &__back {
opacity: 0;
visibility: hidden;
pointer-events: none;
}

&__resume {
align-self: center;
min-height: 0;
margin-top: 14px;
max-height: 6rem;
overflow: hidden;
transition: max-height $ff-expand $ff-ease, opacity 220ms ease,
margin-top $ff-expand $ff-ease, padding $ff-expand $ff-ease,
visibility $ff-expand;
}

&__resume-count {
color: var(--ff-color-text-subtle);
}

&.is-conversing &__resume {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
opacity: 0;
visibility: hidden;
pointer-events: none;
}

&__mode {
align-self: center;
display: flex;
justify-content: center;
width: 100%;
max-width: $ff-column;
max-height: 0;
margin-bottom: 0;
opacity: 0;
visibility: hidden;
overflow: hidden;
pointer-events: none;
transition: max-height $ff-expand $ff-ease, opacity 220ms ease,
margin-bottom $ff-expand $ff-ease, visibility $ff-expand;
}

&.is-conversing &__mode {
max-height: 6rem;
margin-bottom: 10px;
opacity: 1;
visibility: visible;
pointer-events: auto;
}

&__expert {
display: flex;
flex-direction: column;
flex: 0 1 auto;
min-height: 0;
margin-top: 28px;
transition: flex-grow $ff-expand $ff-ease, margin-top $ff-expand $ff-ease;
}

&.is-conversing &__expert {
flex-grow: 1;
margin-top: 0;
}

&__expert :deep(.ff-expert) {
height: auto;
flex: 1;
min-height: 0;
background: transparent;
}

&__expert :deep(.messages-container),
&__expert :deep(.ff-expert-input) {
width: 100%;
max-width: $ff-column;
margin-left: auto;
margin-right: auto;
}

&__expert :deep(.messages-container) {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
opacity: 0;
transition: max-height $ff-expand $ff-ease, opacity 300ms ease;
}

&.is-conversing &__expert :deep(.messages-container) {
max-height: 70vh;
padding-top: 1rem;
padding-bottom: 1rem;
opacity: 1;
}

&__expert :deep(.ff-expert-input) {
border-top: none;
background: transparent;
min-height: 0;
padding: 0;
}

&__expert {
:deep(.input-wrapper) {
flex-direction: row;
align-items: stretch;
border-width: 1px;
border-color: var(--ff-color-border);
border-radius: 12px;
background: var(--ff-color-bg-app);
}

:deep(.chat-input) {
padding: 1.15rem 0.5rem 1.15rem 1.25rem;
font-size: 0.9375rem;
background: transparent;
}

:deep(.actions) {
display: flex;
align-items: center;
padding: 0 0.6rem 0 0;
flex: 0 0 auto;
}

:deep(.action-buttons) {
max-height: 0;
opacity: 0;
visibility: hidden;
margin-bottom: 0;
overflow: hidden;
transition: max-height $ff-expand $ff-ease, opacity 260ms ease,
margin-bottom $ff-expand $ff-ease, visibility $ff-expand;
}
}

&.is-conversing &__expert :deep(.action-buttons) {
max-height: 6rem;
opacity: 1;
visibility: visible;
margin-bottom: 0.5rem;
}
}

@media (prefers-reduced-motion: reduce) {
.ff-expert-home__back,
.ff-expert-home__resume,
.ff-expert-home__mode,
.ff-expert-home__expert,
.ff-expert-home__expert :deep(.messages-container),
.ff-expert-home__expert :deep(.action-buttons) {
transition: none;
}
}
</style>
Loading
Loading