Skip to content

Commit 2cd664d

Browse files
committed
Merge remote-tracking branch 'origin/main' into expert/fix-website-context-switching-hydration-race-condition
2 parents 660f739 + f46a819 commit 2cd664d

14 files changed

Lines changed: 842 additions & 101 deletions

File tree

frontend/src/components/expert/Expert.vue

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@
9494
<div ref="scrollAnchor" class="scroll-anchor" />
9595
</div>
9696

97+
<!-- Updates Available Banner -->
98+
<update-banner v-if="isEditorContext && isInstanceRunning" />
99+
97100
<!-- Input Area -->
98101
<expert-chat-input
99102
:is-generating="isGenerating"
@@ -121,6 +124,7 @@ import ExpertLoadingDots from './ExpertLoadingDots.vue'
121124
import ExpertRichGuide from './ExpertRichGuide.vue'
122125
import ExpertRichResources from './ExpertRichResources.vue'
123126
import ExpertToolCall from './ExpertToolCall.vue'
127+
import UpdateBanner from './components/UpdateBanner.vue'
124128
125129
export default {
126130
name: 'ExpertPanel',
@@ -131,6 +135,7 @@ export default {
131135
ExpertRichGuide,
132136
ExpertRichResources,
133137
ExpertToolCall,
138+
'update-banner': UpdateBanner,
134139
ToggleButtonGroup
135140
},
136141
inject: {
@@ -139,6 +144,18 @@ export default {
139144
default: () => () => {} // No-op function when not provided
140145
}
141146
},
147+
props: {
148+
instance: {
149+
type: Object,
150+
required: false,
151+
default: null
152+
},
153+
device: {
154+
type: Object,
155+
required: false,
156+
default: null
157+
}
158+
},
142159
data () {
143160
return {
144161
scrollCheckDebounce: null,
@@ -188,6 +205,11 @@ export default {
188205
{ title: 'Support', value: 'ff-agent' },
189206
{ title: 'Insights', value: 'operator-agent' }
190207
]
208+
},
209+
isInstanceRunning () {
210+
const instanceRunning = this.instance?.meta?.state === 'running'
211+
const deviceRunning = this.device?.status === 'running'
212+
return instanceRunning || deviceRunning
191213
}
192214
},
193215
watch: {
@@ -214,6 +236,20 @@ export default {
214236
'product/expert/addWelcomeMessageIfNeeded'
215237
)
216238
}
239+
},
240+
'instance.meta.state': {
241+
handler (newState) {
242+
if (this.isEditorContext && newState !== 'running') {
243+
this.reset() // reset assistant state
244+
}
245+
}
246+
},
247+
'device.status': {
248+
handler (newState) {
249+
if (this.isEditorContext && newState !== 'running') {
250+
this.reset() // reset assistant state
251+
}
252+
}
217253
}
218254
},
219255
mounted () {
@@ -254,6 +290,7 @@ export default {
254290
'setAbortController',
255291
'resetSessionTimer'
256292
]),
293+
...mapActions('product/assistant', ['reset']),
257294
258295
async handleSendMessage (query) {
259296
if (!query.trim()) return

frontend/src/components/expert/ExpertChatInput.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
<div class="left">
3737
<context-selector v-if="!isOperatorAgent" />
3838
<div class="context-items-container" @wheel="horizontalScrolling">
39-
<include-context-item v-for="(context, index) in selectedContext" :key="index" :contextItem="context" />
39+
<include-context-item v-for="(context, index) in selectedContextFiltered" :key="index" :contextItem="context" />
40+
<include-debug-context-button v-if="hasDebugLogsSelected && !isOperatorAgent" />
4041
<include-selection-button v-if="hasUserSelection && !isOperatorAgent" />
4142
</div>
4243
</div>
@@ -75,6 +76,7 @@ import ResizeBar from '../ResizeBar.vue'
7576
import CapabilitiesSelector from './components/CapabilitiesSelector.vue'
7677
import ContextSelector from './components/ContextSelector.vue'
7778
import IncludeContextItem from './components/IncludeContextItem.vue'
79+
import IncludeDebugContextButton from './components/IncludeDebugContextButton.vue'
7880
import IncludeSelectionButton from './components/IncludeSelectionButton.vue'
7981
8082
export default {
@@ -83,6 +85,7 @@ export default {
8385
CapabilitiesSelector,
8486
ContextSelector,
8587
IncludeContextItem,
88+
IncludeDebugContextButton,
8689
IncludeSelectionButton,
8790
ResizeBar
8891
},
@@ -131,7 +134,7 @@ export default {
131134
}
132135
},
133136
computed: {
134-
...mapGetters('product/assistant', ['hasUserSelection', 'getSelectedContext']),
137+
...mapGetters('product/assistant', ['getSelectedContext', 'hasDebugLogsSelected', 'hasUserSelection']),
135138
isInputDisabled () {
136139
if (this.isSessionExpired) return true
137140
if (this.isGenerating) return true
@@ -155,6 +158,9 @@ export default {
155158
return []
156159
}
157160
return this.getSelectedContext
161+
},
162+
selectedContextFiltered () {
163+
return this.selectedContext.filter(c => c.showAsChip !== false)
158164
}
159165
},
160166
mounted () {

frontend/src/components/expert/components/ContextSelector.vue

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
:hide-chevron="true"
1010
:icon-only="true"
1111
return-model
12-
label-key="name"
12+
label-key="label"
1313
placeholder="Context"
1414
open-above
15-
:min-options-width="220"
15+
:options-min-width="250"
1616
align-right
1717
@option-selected="selectItem"
1818
>
@@ -76,19 +76,23 @@ export default {
7676
pluralize,
7777
...mapActions('product/assistant', ['setSelectedContext']),
7878
selectItem (option) {
79-
const cleanOption = (option) => ({
80-
value: option.value,
81-
name: option.name,
82-
title: option.title,
83-
icon: option.icon
84-
})
85-
const cleanedOption = cleanOption(option)
86-
const currentSelection = this.selectedContext || []
87-
const exists = currentSelection.some(c => c.value === cleanedOption.value)
88-
if (exists) {
89-
return // already selected, do nothing
79+
if (option.onSelectAction) {
80+
this.$store.dispatch(`product/assistant/${option.onSelectAction}`)
81+
} else {
82+
const cleanOption = (option) => ({
83+
value: option.value,
84+
name: option.name,
85+
label: option.label,
86+
icon: option.icon
87+
})
88+
const cleanedOption = cleanOption(option)
89+
const currentSelection = this.selectedContext || []
90+
const exists = currentSelection.some(c => c.value === cleanedOption.value)
91+
if (exists) {
92+
return // already selected, do nothing
93+
}
94+
this.setSelectedContext([...currentSelection, cleanedOption].filter(Boolean))
9095
}
91-
this.setSelectedContext([...currentSelection, cleanedOption].filter(Boolean))
9296
}
9397
}
9498
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<template>
2+
<ContextChip
3+
class="flow-selection-button"
4+
:modelValue="modelValue"
5+
@toggle="toggleSelection"
6+
>
7+
<template #text>
8+
<span>Debug</span>
9+
<span class="counter italic" :title="selectionTitle">( {{ selectedCounter }} {{ pluralize('log', selectedCounter) }} )</span>
10+
</template>
11+
</ContextChip>
12+
</template>
13+
14+
<script>
15+
import { mapActions, mapState } from 'vuex'
16+
17+
import { pluralize } from '../../../composables/String.js'
18+
19+
import ContextChip from './ContextChip/index.vue'
20+
21+
export default {
22+
name: 'IncludeDebugContextButton',
23+
components: {
24+
ContextChip
25+
},
26+
props: {
27+
modelValue: {
28+
type: Boolean,
29+
required: false,
30+
default: true
31+
}
32+
},
33+
emits: ['update:modelValue'],
34+
computed: {
35+
...mapState('product/assistant', ['debugLog']),
36+
selectedCounter () {
37+
return this.debugLog.length
38+
},
39+
selectionTitle () {
40+
const map = {}
41+
this.debugLog.forEach(n => {
42+
map[n.type] = (map[n.type] ?? 0) + 1
43+
})
44+
45+
const tipBuilder = (logEntry, index) => {
46+
logEntry = logEntry || {}
47+
const level = logEntry.level || ''
48+
const nonDebugLevel = level !== 'debug' ? level : ''
49+
const topic = logEntry.metadata?.topic || ''
50+
const name = logEntry.source?.name || logEntry.source?.id || logEntry.metadata?.path || ''
51+
const format = logEntry.metadata?.format || ''
52+
const type = logEntry.source?.type || ''
53+
const property = logEntry.metadata?.property || ''
54+
const strBuilder = []
55+
if (name) strBuilder.push(`node: ${name}`)
56+
if (topic) strBuilder.push(`topic: ${topic}`)
57+
if (nonDebugLevel) {
58+
if (type) {
59+
strBuilder.push(`${type} : (${nonDebugLevel})`)
60+
}
61+
}
62+
if (property) {
63+
if (format) {
64+
strBuilder.push(`property: ${property} ${format}`)
65+
} else {
66+
strBuilder.push(`property: ${property}`)
67+
}
68+
}
69+
return `${index + 1}: ${strBuilder.join(', ')}`
70+
}
71+
72+
const list = this.debugLog.map(tipBuilder).join('\n')
73+
74+
return `Selected Logs: \n${list}`
75+
}
76+
},
77+
methods: {
78+
...mapActions('product/assistant', ['resetDebugLogContext']),
79+
pluralize,
80+
toggleSelection () {
81+
this.$emit('update:modelValue', !this.modelValue)
82+
this.resetDebugLogContext()
83+
}
84+
}
85+
}
86+
</script>
87+
88+
<style lang="scss">
89+
.flow-selection-button {
90+
.text {
91+
.counter {
92+
color: $ff-grey-500;
93+
margin-left: 4px;
94+
font-size: $ff-funit-xs;
95+
}
96+
}
97+
}
98+
</style>

0 commit comments

Comments
 (0)