website/islands/AiChatBox.tsx

168 lines
3.9 KiB
TypeScript

import { JsonParseStream } from '$std/json/mod.ts'
import { Markdown } from ':components/Markdown.tsx'
import { Signal, signal, useSignal } from '@preact/signals'
import { JSX } from 'preact'
import { useEffect, useRef } from 'preact/hooks'
const systemHistory = signal<BotMessage[]>([{
role: 'system',
content:
`Tu es un assistant spécialisé dans l'open source qui s'appel Coh@bot. Tu répond en français avec des réponse courtes. Tu aide les usagers du Fablab nommé Cohabit en leur proposant des solutions techniques appropriées en français. Tu formule des réponses courtes et précises en français et en markdown si besoin.`,
}])
const currentReader = signal<ReadableStreamDefaultReader<BotResponse> | null>(
null,
)
let currentResponse: string[] = []
type BotMessage = {
role: string
content: string
}
type BotResponse = {
model: string
created_at: string
message: {
role: 'assistant' | 'user' | 'system'
content: string
images?: string
}
done: boolean
}
async function aiRequest(
{ model, messages, stream }: {
model: string
messages: BotMessage[]
stream: boolean
},
) {
systemHistory.value = [...systemHistory.peek(), ...messages]
const body = JSON.stringify({
model,
messages: systemHistory,
stream,
})
console.log('send', JSON.parse(body))
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
body,
})
if (response.ok) {
return response.body?.pipeThrough(new TextDecoderStream()).pipeThrough(
new JsonParseStream(),
) as ReadableStream<BotResponse> ?? new ReadableStream<BotResponse>()
}
throw new Error(response.statusText)
}
export default function AiChatBox() {
const dialog = useRef<HTMLDialogElement>(null)
const form = useRef<HTMLFormElement>(null)
const history = useSignal<JSX.Element[]>([])
useEffect(() => {
dialog.current?.addEventListener('click', (event) => {
if (event.target === dialog.current) {
dialog.current?.close()
}
})
form.current?.addEventListener(
'submit',
(event) => chatListener(event, history),
)
}, [])
return (
<>
<button
class='islands__ai_chat_box__button'
onClick={() => dialog.current?.showModal()}
>
<i class='ri-bard-line'></i>
</button>
<dialog ref={dialog} class='islands__ai_chat_box__dialog'>
<div class='islands__ai_chat_box__dialog__content'>
{history}
</div>
<form ref={form} class='islands__ai_chat_box__dialog__form'>
<input
type='text'
name='query'
placeholder='Saisissez une requête ...'
autoFocus
autoComplete='off'
/>
<button>
<i class='ri-send-plane-2-line'></i>
</button>
</form>
</dialog>
</>
)
}
async function chatListener(event: Event, history: Signal<JSX.Element[]>) {
event.preventDefault()
const form = event.target as HTMLFormElement
const query = new FormData(form).get(
'query',
) as string
form.reset()
const userEntry = (
<span class='islands__ai_chat_box__history__user'>{query}</span>
)
const botMessage = signal('')
const botEntry = (
<span class='islands__ai_chat_box__history_bot'>
<Markdown>{botMessage}</Markdown>
</span>
)
history.value = [...history.peek(), userEntry, botEntry]
if (currentResponse.length !== 0) {
systemHistory.value = [...systemHistory.peek(), {
role: 'assistant',
content: currentResponse.join(''),
}]
currentResponse = []
}
await currentReader.value?.cancel()
const response = await aiRequest({
model: 'mistral',
messages: [{
role: 'user',
content: query,
}],
stream: true,
})
const reader = response.getReader()
currentReader.value = reader
while (true) {
const { value, done } = await reader.read()
currentResponse.push(value?.message.content ?? '')
if (done) break
console.log(value.message.content)
botMessage.value = `${botMessage.peek()}${value.message.content}`
}
}