- ChatbotV2.vue : Vue island, thread chat (input + messages bot/user), persistance sessionStorage, bandeau beta '120 fiches AEP, RAG-PE bientot', gestion erreurs 429/502/504 ; pas de streaming ni markdown V1 - /api/chatbot.ts : endpoint Astro server proxy POST vers CHATBOT_UPSTREAM (default https://aep.trans-former.fr/api/chatbot), timeout 25s, body { question, history } -> upstream classique chatbot AEP Mistral Small - astro.config.mjs : output 'server' + adapter @astrojs/node standalone (Astro 6 a supprime mode hybrid ; on opt-in prerender sur les pages) - Toutes les pages publiques (index, manifeste, manifeste/commander, a-propos, mentions-legales) ont 'export const prerender = true' - ColCentre.astro : remplace ChatbotPlaceholder par ChatbotV2 dans le tab - .env.example : ajoute CHATBOT_UPSTREAM (V1.5 = switch LightRAG-PE 1 ligne) Decision V1 : endpoint AEP /api/chatbot (classique, repond bien) au lieu de /api/chatbot-v2 qui retourne v2_ready=false ('base vectorielle en cours'). Bandeau beta reste valide ; switch v2 quand ready cote AEP via env var. Note PC8 deploy : Coolify doit booter avec 'node ./dist/server/entry.mjs' (SSR Node standalone) au lieu de servir dist/client/ static. Test end-to-end OK : SSR boot port 4399 + curl POST /api/chatbot -> reponse_texte 800+ chars de l'AEP backend.
188 lines
4.9 KiB
Vue
188 lines
4.9 KiB
Vue
<script setup lang="ts">
|
|
// PC7 — Chatbot V1 brancha sur endpoint AEP (proxy Astro server-side).
|
|
// Persistance thread sessionStorage. Bandeau beta info ; pas de streaming, pas de markdown V1.
|
|
|
|
import { ref, onMounted, nextTick, useTemplateRef } from 'vue'
|
|
|
|
interface Msg {
|
|
role: 'user' | 'bot'
|
|
content: string
|
|
ts: number
|
|
}
|
|
|
|
const props = defineProps<{ apiUrl?: string }>()
|
|
|
|
const messages = ref<Msg[]>([])
|
|
const input = ref('')
|
|
const sending = ref(false)
|
|
const errorBanner = ref('')
|
|
const threadEl = useTemplateRef<HTMLElement>('threadEl')
|
|
|
|
const STORAGE_KEY = 'tf-chatbot-v2-thread'
|
|
const ENDPOINT = props.apiUrl || '/api/chatbot'
|
|
|
|
onMounted(() => {
|
|
try {
|
|
const saved = sessionStorage.getItem(STORAGE_KEY)
|
|
if (saved) messages.value = JSON.parse(saved)
|
|
} catch {
|
|
// sessionStorage indisponible ; on ignore.
|
|
}
|
|
})
|
|
|
|
const persist = () => {
|
|
try {
|
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages.value))
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
const scrollToBottom = async () => {
|
|
await nextTick()
|
|
const el = threadEl.value
|
|
if (el) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
|
|
}
|
|
|
|
const send = async () => {
|
|
const q = input.value.trim()
|
|
if (!q || sending.value) return
|
|
|
|
messages.value.push({ role: 'user', content: q, ts: Date.now() })
|
|
input.value = ''
|
|
sending.value = true
|
|
errorBanner.value = ''
|
|
persist()
|
|
scrollToBottom()
|
|
|
|
try {
|
|
const res = await fetch(ENDPOINT, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
question: q,
|
|
history: messages.value.slice(-10).map((m) => ({ role: m.role, content: m.content })),
|
|
}),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 429) {
|
|
errorBanner.value = 'Limite de questions atteinte ; ressaie plus tard.'
|
|
} else if (res.status === 502 || res.status === 504) {
|
|
errorBanner.value = "Le service de chat n'est pas joignable ; ressaie dans un instant."
|
|
} else {
|
|
errorBanner.value = `Erreur ${res.status} ; ressaie ou recharge la page.`
|
|
}
|
|
sending.value = false
|
|
return
|
|
}
|
|
|
|
const data = await res.json()
|
|
const answer =
|
|
data.reponse_texte ||
|
|
data.answer ||
|
|
data.text ||
|
|
data.message ||
|
|
'Pas de reponse.'
|
|
|
|
messages.value.push({ role: 'bot', content: answer, ts: Date.now() })
|
|
} catch (e) {
|
|
errorBanner.value = "Probleme reseau ; verifie ta connexion."
|
|
messages.value.push({
|
|
role: 'bot',
|
|
content: `Erreur : ${(e as Error).message || 'inconnue'}`,
|
|
ts: Date.now(),
|
|
})
|
|
} finally {
|
|
sending.value = false
|
|
persist()
|
|
scrollToBottom()
|
|
}
|
|
}
|
|
|
|
const reset = () => {
|
|
messages.value = []
|
|
errorBanner.value = ''
|
|
persist()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="h-full flex flex-col bg-white">
|
|
<div class="bg-amber-50 border-b border-amber-200 px-3 py-2 text-xs text-amber-900 leading-snug">
|
|
<span class="font-medium">Beta :</span> 120 fiches AEP indexees (Mistral Small) ; bientot RAG-PE multi-corpus.
|
|
</div>
|
|
|
|
<div
|
|
v-if="errorBanner"
|
|
class="bg-rose-50 border-b border-rose-200 px-3 py-2 text-xs text-rose-800"
|
|
role="alert"
|
|
>
|
|
{{ errorBanner }}
|
|
</div>
|
|
|
|
<div
|
|
ref="threadEl"
|
|
class="flex-1 overflow-y-auto px-3 py-3 space-y-3"
|
|
role="log"
|
|
aria-live="polite"
|
|
>
|
|
<p
|
|
v-if="!messages.length"
|
|
class="text-neutral-400 text-sm italic text-center mt-8 px-4"
|
|
>
|
|
Pose une question sur l'AEP ; le manifeste ; les agences locales...
|
|
</p>
|
|
|
|
<div
|
|
v-for="m in messages"
|
|
:key="m.ts"
|
|
:class="[
|
|
'max-w-[85%] rounded-lg px-3 py-2 text-sm leading-relaxed whitespace-pre-wrap break-words',
|
|
m.role === 'user'
|
|
? 'bg-neutral-900 text-white ml-auto'
|
|
: 'bg-neutral-100 text-neutral-800 mr-auto',
|
|
]"
|
|
>
|
|
{{ m.content }}
|
|
</div>
|
|
|
|
<div
|
|
v-if="sending"
|
|
class="bg-neutral-100 max-w-[60%] rounded-lg px-3 py-2 text-sm text-neutral-500 italic mr-auto"
|
|
>
|
|
Le bot reflechit...
|
|
</div>
|
|
</div>
|
|
|
|
<form
|
|
class="border-t border-neutral-200 p-2 flex gap-2 items-stretch"
|
|
@submit.prevent="send"
|
|
>
|
|
<input
|
|
v-model="input"
|
|
type="text"
|
|
placeholder="Ta question..."
|
|
:disabled="sending"
|
|
class="flex-1 px-3 py-2 border border-neutral-300 rounded-lg text-sm focus:outline-none focus:border-neutral-900 disabled:opacity-60"
|
|
aria-label="Saisis ta question"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
:disabled="sending || !input.trim()"
|
|
class="px-3 py-2 bg-neutral-900 text-white rounded-lg text-sm font-medium disabled:opacity-50"
|
|
>
|
|
Envoyer
|
|
</button>
|
|
<button
|
|
v-if="messages.length"
|
|
type="button"
|
|
class="px-2 py-2 text-neutral-500 text-xs hover:text-neutral-900 underline"
|
|
@click="reset"
|
|
>
|
|
Reset
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</template>
|