177 lines
4.1 KiB
TypeScript
177 lines
4.1 KiB
TypeScript
import { Signal, signal, useSignal } from '@preact/signals'
|
|
import { useEffect, useRef } from 'preact/hooks'
|
|
import { JSX } from 'preact'
|
|
import { JsonParseStream } from '$std/json/mod.ts'
|
|
import { CSS, render as renderMd } from 'gfm'
|
|
|
|
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[] = []
|
|
|
|
function MdCell({ children }: { children: Signal<string> }) {
|
|
return (
|
|
<div
|
|
class='markdown-body'
|
|
dangerouslySetInnerHTML={{ __html: renderMd(children.value) }}
|
|
>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<>
|
|
<style dangerouslySetInnerHTML={{ __html: CSS }}></style>
|
|
<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'>
|
|
<MdCell>{botMessage}</MdCell>
|
|
</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}`
|
|
}
|
|
}
|