feat(m2): wire ChatPanel to stream LLM responses — thinking state, token accumulation, error display
This commit is contained in:
parent
0e3bfd24f1
commit
cba4175260
1 changed files with 54 additions and 8 deletions
|
|
@ -26,8 +26,11 @@
|
||||||
class="chat-input"
|
class="chat-input"
|
||||||
placeholder="Ask Robin something..."
|
placeholder="Ask Robin something..."
|
||||||
@keydown.enter="send"
|
@keydown.enter="send"
|
||||||
|
:disabled="thinking"
|
||||||
/>
|
/>
|
||||||
<button class="send-btn" @click="send">→</button>
|
<button class="send-btn" @click="send" :disabled="thinking">
|
||||||
|
{{ thinking ? '…' : '→' }}
|
||||||
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -42,13 +45,12 @@ interface RobinEvent { pattern_id: string; title: string; body: string; severity
|
||||||
|
|
||||||
const messages = ref<Message[]>([])
|
const messages = ref<Message[]>([])
|
||||||
const input = ref('')
|
const input = ref('')
|
||||||
|
const thinking = ref(false)
|
||||||
const messagesEl = ref<HTMLElement | null>(null)
|
const messagesEl = ref<HTMLElement | null>(null)
|
||||||
let unlisten: UnlistenFn | null = null
|
let unlisten: UnlistenFn | null = null
|
||||||
|
let activeCleanup: (() => void) | null = null
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Signal backend: stop queuing events into PENDING while panel is open.
|
|
||||||
// Must happen before drain so new events that arrive during mount are not
|
|
||||||
// double-delivered on the next open cycle.
|
|
||||||
try { await invoke('panel_opened') } catch {}
|
try { await invoke('panel_opened') } catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -60,7 +62,6 @@ onMounted(async () => {
|
||||||
console.warn('Robin: failed to drain pending events:', err)
|
console.warn('Robin: failed to drain pending events:', err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always set up the live listener, even if drain failed
|
|
||||||
unlisten = await listen<RobinEvent>('robin:event', ({ payload }) => {
|
unlisten = await listen<RobinEvent>('robin:event', ({ payload }) => {
|
||||||
pushRobinEvent(payload)
|
pushRobinEvent(payload)
|
||||||
})
|
})
|
||||||
|
|
@ -69,6 +70,7 @@ onMounted(async () => {
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlisten?.()
|
unlisten?.()
|
||||||
invoke('panel_closed').catch(() => {})
|
invoke('panel_closed').catch(() => {})
|
||||||
|
activeCleanup?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
function pushRobinEvent(e: RobinEvent) {
|
function pushRobinEvent(e: RobinEvent) {
|
||||||
|
|
@ -80,13 +82,55 @@ function pushRobinEvent(e: RobinEvent) {
|
||||||
|
|
||||||
async function send() {
|
async function send() {
|
||||||
const text = input.value.trim()
|
const text = input.value.trim()
|
||||||
if (!text) return
|
if (!text || thinking.value) return
|
||||||
input.value = ''
|
input.value = ''
|
||||||
|
thinking.value = true
|
||||||
|
|
||||||
messages.value.push({ role: 'user', content: text })
|
messages.value.push({ role: 'user', content: text })
|
||||||
// M4: invoke('chat', { message: text }) and stream response
|
messages.value.push({ role: 'robin', content: '' })
|
||||||
messages.value.push({ role: 'robin', content: '(LLM chat not yet connected — M4)' })
|
const robinIdx = messages.value.length - 1
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
messagesEl.value?.scrollTo({ top: messagesEl.value.scrollHeight, behavior: 'smooth' })
|
messagesEl.value?.scrollTo({ top: messagesEl.value.scrollHeight, behavior: 'smooth' })
|
||||||
|
|
||||||
|
let unlistenToken: UnlistenFn | null = null
|
||||||
|
let unlistenDone: UnlistenFn | null = null
|
||||||
|
let unlistenError: UnlistenFn | null = null
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
unlistenToken?.()
|
||||||
|
unlistenDone?.()
|
||||||
|
unlistenError?.()
|
||||||
|
thinking.value = false
|
||||||
|
activeCleanup = null
|
||||||
|
}
|
||||||
|
activeCleanup = cleanup
|
||||||
|
|
||||||
|
unlistenToken = await listen<string>('robin:chat-token', ({ payload }) => {
|
||||||
|
messages.value[robinIdx].content += payload
|
||||||
|
nextTick(() => {
|
||||||
|
messagesEl.value?.scrollTo({ top: messagesEl.value.scrollHeight, behavior: 'smooth' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
unlistenDone = await listen('robin:chat-done', () => cleanup())
|
||||||
|
|
||||||
|
unlistenError = await listen<string>('robin:chat-error', ({ payload }) => {
|
||||||
|
if (!messages.value[robinIdx].content) {
|
||||||
|
messages.value[robinIdx].content =
|
||||||
|
`Robin couldn't reach Ollama. Make sure it's running at the configured URL. (${payload})`
|
||||||
|
}
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('chat', { message: text })
|
||||||
|
} catch (err) {
|
||||||
|
if (!messages.value[robinIdx].content) {
|
||||||
|
messages.value[robinIdx].content = `Something went wrong starting the chat. (${err})`
|
||||||
|
}
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -154,4 +198,6 @@ async function send() {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.send-btn:hover { opacity: 0.85; }
|
.send-btn:hover { opacity: 0.85; }
|
||||||
|
.send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.chat-input:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue