feat(m2): LLM chat via Ollama — streaming responses with migration context #3
1 changed files with 54 additions and 8 deletions
|
|
@ -26,8 +26,11 @@
|
|||
class="chat-input"
|
||||
placeholder="Ask Robin something..."
|
||||
@keydown.enter="send"
|
||||
:disabled="thinking"
|
||||
/>
|
||||
<button class="send-btn" @click="send">→</button>
|
||||
<button class="send-btn" @click="send" :disabled="thinking">
|
||||
{{ thinking ? '…' : '→' }}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -42,13 +45,12 @@ interface RobinEvent { pattern_id: string; title: string; body: string; severity
|
|||
|
||||
const messages = ref<Message[]>([])
|
||||
const input = ref('')
|
||||
const thinking = ref(false)
|
||||
const messagesEl = ref<HTMLElement | null>(null)
|
||||
let unlisten: UnlistenFn | null = null
|
||||
let activeCleanup: (() => void) | null = null
|
||||
|
||||
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 {
|
||||
|
|
@ -60,7 +62,6 @@ onMounted(async () => {
|
|||
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 }) => {
|
||||
pushRobinEvent(payload)
|
||||
})
|
||||
|
|
@ -69,6 +70,7 @@ onMounted(async () => {
|
|||
onUnmounted(() => {
|
||||
unlisten?.()
|
||||
invoke('panel_closed').catch(() => {})
|
||||
activeCleanup?.()
|
||||
})
|
||||
|
||||
function pushRobinEvent(e: RobinEvent) {
|
||||
|
|
@ -80,13 +82,55 @@ function pushRobinEvent(e: RobinEvent) {
|
|||
|
||||
async function send() {
|
||||
const text = input.value.trim()
|
||||
if (!text) return
|
||||
if (!text || thinking.value) return
|
||||
input.value = ''
|
||||
thinking.value = true
|
||||
|
||||
messages.value.push({ role: 'user', content: text })
|
||||
// M4: invoke('chat', { message: text }) and stream response
|
||||
messages.value.push({ role: 'robin', content: '(LLM chat not yet connected — M4)' })
|
||||
messages.value.push({ role: 'robin', content: '' })
|
||||
const robinIdx = messages.value.length - 1
|
||||
|
||||
await nextTick()
|
||||
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>
|
||||
|
||||
|
|
@ -154,4 +198,6 @@ async function send() {
|
|||
cursor: pointer;
|
||||
}
|
||||
.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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue