Skip to content

Commit 75b8a06

Browse files
committed
[8381] Add the onboarding surface variant to the expert components
1 parent 55f2cc7 commit 75b8a06

7 files changed

Lines changed: 512 additions & 7 deletions

File tree

frontend/src/components/expert/Expert.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
:class="{ 'has-mode-switcher': isInsightsModeEnabled && isEditorContext }"
99
@scroll="handleScroll"
1010
>
11-
<info-banner />
11+
<info-banner v-if="!isOnboardingSurface" />
1212

1313
<expert-messages @resizing="scrollToBottom" />
1414

@@ -50,6 +50,10 @@ export default {
5050
togglePinWithWidth: {
5151
from: 'togglePinWithWidth',
5252
default: () => () => {} // No-op function when not provided
53+
},
54+
expertSurface: {
55+
from: 'expert-surface',
56+
default: 'drawer'
5357
}
5458
},
5559
props: {
@@ -85,6 +89,11 @@ export default {
8589
// In editor context, the route name includes 'editor'
8690
return this.$route?.name?.includes('editor') || false
8791
},
92+
isOnboardingSurface () {
93+
// In onboarding, the Expert opens the conversation itself, so the
94+
// canned welcome message and the support banner stay out of it.
95+
return this.expertSurface === 'onboarding'
96+
},
8897
isInsightsModeEnabled () {
8998
return !!this.featuresCheck?.isExpertInsightsFeatureEnabled
9099
},
@@ -109,7 +118,9 @@ export default {
109118
if (this.isInsightsAgent) {
110119
await this.getCapabilities()
111120
}
112-
this.addWelcomeMessageIfNeeded()
121+
if (!this.isOnboardingSurface) {
122+
this.addWelcomeMessageIfNeeded()
123+
}
113124
}
114125
},
115126
'instance.meta.state': {

frontend/src/components/expert/components/ExpertChatInput.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
direction="horizontal"
66
@mousedown="onStartResize"
77
/>
8-
<!-- Action buttons row -->
9-
<div class="action-buttons">
8+
<!-- Action buttons row. Hidden during onboarding: the conversation is
9+
the whole flow there, so Start over, Plan mode and settings only
10+
offer ways to derail it. -->
11+
<div v-if="expertSurface !== 'onboarding'" class="action-buttons">
1012
<button
1113
type="button"
1214
class="btn-start-over"
@@ -146,6 +148,10 @@ export default {
146148
togglePinWithWidth: {
147149
from: 'togglePinWithWidth',
148150
default: () => () => {} // No-op function when not provided
151+
},
152+
expertSurface: {
153+
from: 'expert-surface',
154+
default: 'drawer'
149155
}
150156
},
151157
emits: ['send', 'stop'],
@@ -228,6 +234,9 @@ export default {
228234
if (this.requestingPlanChange) {
229235
return 'Describe a change to the plan, or paste an edited version'
230236
}
237+
if (this.expertSurface === 'onboarding') {
238+
return 'Or just tell me in your own words'
239+
}
231240
return this.isInsightsAgent
232241
? 'Tell us what you want to know about'
233242
: 'Tell us what you need help with'

frontend/src/components/expert/components/ExpertMessages.vue

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<template>
22
<div ref="messagesWrapper" class="messages-wrapper">
33
<ul class="flex flex-col gap-3">
4-
<li v-for="message in messages" :key="message._uuid" class="flex flex-col gap-3">
5-
<component :is="messageTypes[message._type]" v-if="messageTypes[message._type]" v-bind="{...message}" />
4+
<li v-for="entry in renderList" :key="entryKey(entry)" class="flex flex-col gap-3">
5+
<collapsed-question-turn v-if="entry.kind === 'folded-turn'" :turn="entry" />
6+
<component :is="messageTypes[entry.message._type]" v-else-if="messageTypes[entry.message._type]" v-bind="{...entry.message}" />
67
</li>
78
<li v-if="isWaitingForResponse">
89
<expert-loading-indicator />
@@ -16,9 +17,12 @@
1617
import { mapState } from 'pinia'
1718
import { markRaw } from 'vue'
1819
20+
import { buildCollapsedTranscript } from '../composables/collapseTranscript.js'
21+
1922
import ExpertLoadingIndicator from './ExpertLoadingIndicator.vue'
2023
2124
import AiMessage from './messages/AiMessage.vue'
25+
import CollapsedQuestionTurn from './messages/CollapsedQuestionTurn.vue'
2226
import HumanMessage from './messages/HumanMessage.vue'
2327
import SystemMessage from './messages/SystemMessage.vue'
2428
@@ -27,7 +31,13 @@ import { useProductExpertStore } from '@/stores/product-expert.js'
2731
2832
export default {
2933
name: 'ExpertMessages',
30-
components: { ExpertLoadingIndicator },
34+
components: { CollapsedQuestionTurn, ExpertLoadingIndicator },
35+
inject: {
36+
expertSurface: {
37+
from: 'expert-surface',
38+
default: 'drawer'
39+
}
40+
},
3141
emits: ['resizing'],
3242
data () {
3343
return {
@@ -43,6 +53,15 @@ export default {
4353
human: markRaw(HumanMessage),
4454
system: markRaw(SystemMessage)
4555
}
56+
},
57+
renderList () {
58+
// The onboarding surface folds answered question turns into quiet
59+
// lines (the collapsing-transcript treatment); the drawer renders
60+
// the transcript as-is.
61+
if (this.expertSurface === 'onboarding') {
62+
return buildCollapsedTranscript(this.messages)
63+
}
64+
return this.messages.map(message => ({ kind: 'message', message }))
4665
}
4766
},
4867
mounted () {
@@ -54,6 +73,9 @@ export default {
5473
window.removeEventListener('keydown', this.onKeyDown)
5574
},
5675
methods: {
76+
entryKey (entry) {
77+
return entry.kind === 'folded-turn' ? entry.questionsMessage._uuid : entry.message._uuid
78+
},
5779
onKeyDown (e) {
5880
if (e.altKey && e.shiftKey && e.key.toLowerCase() === 'd') {
5981
e.preventDefault()
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<template>
2+
<div class="collapsed-question-turn" :class="{ expanded }">
3+
<template v-if="!expanded">
4+
<div
5+
v-for="(entry, index) in turn.entries"
6+
:key="index"
7+
class="folded-question"
8+
data-el="folded-question"
9+
>
10+
<button
11+
type="button"
12+
class="question-text"
13+
data-action="toggle-turn"
14+
title="Show the full question card"
15+
@click="expanded = true"
16+
>
17+
{{ entry.question }}
18+
</button>
19+
<div class="folded-answers">
20+
<button
21+
v-for="(chip, chipIndex) in chipsFor(entry)"
22+
:key="chipIndex"
23+
type="button"
24+
class="answer-chip"
25+
data-action="edit-answer"
26+
title="Load this answer into the composer to correct it"
27+
@click="editAnswer(entry)"
28+
>
29+
<span class="chip-label">{{ chip }}</span>
30+
<span class="chip-remove" aria-hidden="true">&times;</span>
31+
</button>
32+
<span v-if="chipsFor(entry).length === 0" class="skipped-marker">Skipped</span>
33+
</div>
34+
</div>
35+
</template>
36+
<div v-else class="expanded-messages">
37+
<button
38+
type="button"
39+
class="question-text"
40+
data-action="toggle-turn"
41+
title="Collapse this step"
42+
@click="expanded = false"
43+
>
44+
Collapse this step
45+
</button>
46+
<AiMessage v-bind="{ ...turn.questionsMessage }" />
47+
<HumanMessage v-if="turn.replyMessage" v-bind="{ ...turn.replyMessage }" />
48+
</div>
49+
</div>
50+
</template>
51+
52+
<script>
53+
import { mapActions } from 'pinia'
54+
55+
import AiMessage from './AiMessage.vue'
56+
import HumanMessage from './HumanMessage.vue'
57+
58+
import { useProductExpertStore } from '@/stores/product-expert.js'
59+
60+
export default {
61+
name: 'CollapsedQuestionTurn',
62+
components: {
63+
AiMessage,
64+
HumanMessage
65+
},
66+
props: {
67+
turn: {
68+
type: Object,
69+
required: true
70+
}
71+
},
72+
data () {
73+
return {
74+
expanded: false
75+
}
76+
},
77+
computed: {
78+
// A hand-typed or edited reply doesn't match the question lines, so
79+
// every entry resolves to null; fall back to the raw reply text once.
80+
hasMatchedAnswers () {
81+
return this.turn.entries.some(entry => entry.answer !== null)
82+
}
83+
},
84+
methods: {
85+
...mapActions(useProductExpertStore, ['setPendingInput']),
86+
// One chip per pick: QuestionsList composes multi-select answers as a
87+
// comma-separated list, so split for display only. Editing any chip
88+
// loads the whole answer line, since a resend replaces the full turn.
89+
chipsFor (entry) {
90+
if (entry.answer) {
91+
return entry.answer.split(', ')
92+
}
93+
if (!this.hasMatchedAnswers && this.turn.replyMessage?.content) {
94+
return [this.turn.replyMessage.content]
95+
}
96+
return []
97+
},
98+
editAnswer (entry) {
99+
if (entry.answer) {
100+
this.setPendingInput(`${entry.question} ${entry.answer}`)
101+
} else if (this.turn.replyMessage?.content) {
102+
this.setPendingInput(this.turn.replyMessage.content)
103+
}
104+
}
105+
}
106+
}
107+
</script>
108+
109+
<style scoped lang="scss">
110+
.collapsed-question-turn {
111+
display: flex;
112+
flex-direction: column;
113+
gap: 1rem;
114+
}
115+
116+
.folded-question {
117+
display: flex;
118+
flex-direction: column;
119+
align-items: flex-start;
120+
gap: 0.5rem;
121+
padding: 0.125rem 0 0.125rem 0.875rem;
122+
border-left: 2px solid var(--ff-color-border);
123+
}
124+
125+
.question-text {
126+
font-size: 0.8125rem;
127+
color: var(--ff-color-text-subtle);
128+
text-align: left;
129+
cursor: pointer;
130+
131+
&:hover {
132+
color: var(--ff-color-text-strong);
133+
}
134+
}
135+
136+
.folded-answers {
137+
display: flex;
138+
flex-wrap: wrap;
139+
gap: 0.5rem;
140+
}
141+
142+
.answer-chip {
143+
display: inline-flex;
144+
align-items: center;
145+
gap: 0.5rem;
146+
font-size: 0.875rem;
147+
font-weight: 600;
148+
color: var(--ff-color-text-strong);
149+
background: var(--ff-color-bg-surface);
150+
border: 1px solid var(--ff-color-border);
151+
border-radius: 9999px;
152+
padding: 0.25rem 0.75rem;
153+
cursor: pointer;
154+
max-width: 100%;
155+
156+
&:hover {
157+
border-color: var(--ff-color-accent);
158+
}
159+
}
160+
161+
.chip-label {
162+
overflow: hidden;
163+
text-overflow: ellipsis;
164+
white-space: nowrap;
165+
max-width: 22rem;
166+
}
167+
168+
.chip-remove {
169+
font-weight: 400;
170+
color: var(--ff-color-text-subtle);
171+
line-height: 1;
172+
}
173+
174+
.skipped-marker {
175+
font-size: 0.875rem;
176+
font-style: italic;
177+
color: var(--ff-color-text-subtle);
178+
}
179+
180+
.expanded-messages {
181+
display: flex;
182+
flex-direction: column;
183+
align-items: flex-start;
184+
gap: 0.75rem;
185+
padding-left: 0.875rem;
186+
border-left: 2px solid var(--ff-color-accent);
187+
}
188+
</style>

0 commit comments

Comments
 (0)