feat(assistant): chat moderne — saisie en bas, fil qui défile en bulles
- OrchestratorDialog restructuré en vraie fenêtre de chat : carte à hauteur fixe (76vh), en-tête + conversation qui défile + composer épinglé en bas. On écrit SOUS la discussion (comme tout chat), l'input se vide à l'envoi, auto-défilement vers le bas à chaque nouveau message. Confirmation du plan déplacée en ligne dans le fil (plus de pied de page). État initial « Comment puis-je aider ? » + exemples. Recherche client/adresse intégrée au composer. - MonAujourdhui : « Allouer un technicien » → « Assistant » (icône auto_awesome), ouvre le chat vierge ; visible aussi pour les rôles Boîte de réception. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
17e85d88a9
commit
19e2c1fea0
|
|
@ -100,7 +100,7 @@ onMounted(() => { load(); refreshTimer = setInterval(load, 60000); document.addE
|
|||
onUnmounted(() => { if (refreshTimer) clearInterval(refreshTimer); document.removeEventListener('visibilitychange', onVisible) })
|
||||
|
||||
const dash = (v) => (loading.value && v == null) ? '…' : (v == null ? '—' : v)
|
||||
function allocate () { window.dispatchEvent(new CustomEvent('ops:assistant', { detail: { text: 'Allouer un technicien : ' } })) }
|
||||
function allocate () { window.dispatchEvent(new CustomEvent('ops:assistant', { detail: { text: '' } })) } // ouvre l'Assistant (chat) vierge
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -111,8 +111,8 @@ function allocate () { window.dispatchEvent(new CustomEvent('ops:assistant', { d
|
|||
<div class="mon-auj-day">{{ dateLabel }}</div>
|
||||
</div>
|
||||
<div class="mon-auj-acts">
|
||||
<button v-if="canAssign" class="act act-pri" @click="allocate">
|
||||
<q-icon name="engineering" size="17px" /> Allouer un technicien
|
||||
<button v-if="canAssign || showInbox" class="act act-pri" @click="allocate">
|
||||
<q-icon name="auto_awesome" size="17px" /> Assistant
|
||||
</button>
|
||||
<button v-if="showInbox" class="act" @click="router.push('/pipeline')">
|
||||
<q-icon name="sell" size="16px" /> Nouvelle vente
|
||||
|
|
|
|||
|
|
@ -1,47 +1,26 @@
|
|||
<template>
|
||||
<q-dialog v-model="open" @hide="reset">
|
||||
<q-card style="min-width:540px;max-width:96vw">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<q-icon name="bolt" color="deep-purple-6" size="22px" class="q-mr-sm" />
|
||||
<q-card class="orch-card column no-wrap">
|
||||
<q-card-section class="row items-center q-py-sm">
|
||||
<q-icon name="auto_awesome" color="deep-purple-6" size="22px" class="q-mr-sm" />
|
||||
<div class="text-subtitle1 text-weight-bold">{{ title }}</div>
|
||||
<q-space />
|
||||
<q-btn flat round dense icon="close" v-close-popup />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<!-- Dictée / texte -->
|
||||
<div class="row items-end q-gutter-sm">
|
||||
<q-input v-model="text" type="textarea" autogrow :rows="2" outlined class="col" autofocus
|
||||
label="Dicte ou écris (ex. « Crée un ticket de facture non payée pour Louis-Paul, texto au client qu'on attend le paiement »)"
|
||||
@keyup.enter.exact.prevent="plan" />
|
||||
<q-btn round :color="listening ? 'red-6' : 'deep-purple-5'" :icon="listening ? 'mic' : 'mic_none'" :loading="planning" @click="toggleMic">
|
||||
<q-tooltip>{{ micSupported ? (listening ? 'Arrêter la dictée' : 'Dicter à voix') : 'Dictée non supportée ici' }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<!-- Typeahead client / adresse : tape → suggestions (nom, adresse, tél., courriel) → insère l'entité dans la demande -->
|
||||
<q-select v-if="entitySearch" dense outlined use-input hide-dropdown-icon input-debounce="200"
|
||||
v-model="entityPick" :options="entityOptions" @filter="filterEntities" @update:model-value="onPickEntity"
|
||||
option-label="label" clearable class="q-mt-sm"
|
||||
label="Ajouter un client / une adresse — recherche (nom · adresse · téléphone · courriel)">
|
||||
<template #prepend><q-icon name="person_search" color="deep-purple-5" /></template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar><q-icon :name="scope.opt.icon" size="18px" color="deep-purple-5" /></q-item-section>
|
||||
<q-item-section><q-item-label>{{ scope.opt.label }}</q-item-label><q-item-label caption>{{ scope.opt.caption }}</q-item-label></q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<template #no-option><q-item><q-item-section class="text-grey-6">Tape au moins 2 lettres…</q-item-section></q-item></template>
|
||||
</q-select>
|
||||
|
||||
<div class="row justify-end q-mt-xs">
|
||||
<q-btn unelevated color="deep-purple-6" icon="auto_awesome" :label="thread.length ? 'Envoyer' : 'Préparer le plan'" no-caps :disable="!text.trim() || planning" :loading="planning" @click="plan" />
|
||||
<!-- CONVERSATION (défile) — l'input est en bas, comme tout chat -->
|
||||
<q-card-section ref="scrollEl" class="col orch-body">
|
||||
<!-- État initial -->
|
||||
<div v-if="!thread.length && !diagnoses.length && !writeActions.length && !results.length" class="orch-empty">
|
||||
<q-icon name="auto_awesome" size="38px" color="deep-purple-3" />
|
||||
<div class="text-subtitle2 q-mt-sm text-grey-8">Comment puis-je aider ?</div>
|
||||
<div class="text-caption text-grey-6 q-mt-xs">« Vérifier le signal chez Louis-Paul » · « Allouer un technicien pour une panne fibre » · « Créer un ticket… »</div>
|
||||
</div>
|
||||
|
||||
<!-- Fil de conversation (chat MULTI-TOURS) : l'assistant peut poser une question → réponds dans le champ ci-dessus -->
|
||||
<div v-if="thread.length" class="orch-thread q-mt-md">
|
||||
<div v-for="(m, i) in thread" :key="i" :class="['orch-msg', 'orch-msg-' + m.role]">
|
||||
<q-icon v-if="m.role === 'assistant'" name="bolt" size="14px" color="deep-purple-6" class="q-mr-xs" />{{ m.text }}
|
||||
</div>
|
||||
<!-- Fil de conversation (bulles) -->
|
||||
<div v-for="(m, i) in thread" :key="'m' + i" :class="['orch-msg', 'orch-msg-' + m.role]">
|
||||
<q-icon v-if="m.role === 'assistant'" name="auto_awesome" size="14px" color="deep-purple-6" class="q-mr-xs" />{{ m.text }}
|
||||
</div>
|
||||
|
||||
<!-- Vérifications (lecture immédiate — pas de confirmation) -->
|
||||
|
|
@ -125,6 +104,11 @@
|
|||
<template v-if="a.type === 'send_email'"><q-input v-model="a.subject" dense outlined label="Sujet" class="q-mb-xs" /><q-input v-model="a.body" type="textarea" autogrow :rows="2" dense outlined label="Courriel" /></template>
|
||||
<q-input v-if="a.type === 'note'" v-model="a.text" type="textarea" autogrow :rows="2" dense outlined label="Note" />
|
||||
</div>
|
||||
<!-- Confirmation INLINE dans le fil (pas de pied de page) -->
|
||||
<div v-if="!results.length" class="row justify-end q-gutter-sm q-mt-sm">
|
||||
<q-btn flat dense no-caps label="Annuler" color="grey-7" @click="actions = []" />
|
||||
<q-btn unelevated dense no-caps color="green-7" icon="send" label="Confirmer et exécuter" :loading="running" @click="run" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Résultats -->
|
||||
|
|
@ -135,13 +119,33 @@
|
|||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions v-if="writeActions.length && !results.length" align="right">
|
||||
<q-btn flat label="Annuler" @click="actions = []" />
|
||||
<q-btn unelevated color="green-7" icon="send" label="Confirmer et exécuter" no-caps :loading="running" @click="run" />
|
||||
</q-card-actions>
|
||||
<q-card-actions v-else-if="results.length" align="right">
|
||||
<q-btn unelevated color="grey-7" label="Fermer" v-close-popup />
|
||||
</q-card-actions>
|
||||
<q-separator />
|
||||
<!-- COMPOSER (bas) : écris ta demande / réponds à l'assistant — comme tout chat -->
|
||||
<div class="orch-composer">
|
||||
<q-select v-if="entitySearch" dense outlined use-input hide-dropdown-icon input-debounce="200"
|
||||
v-model="entityPick" :options="entityOptions" @filter="filterEntities" @update:model-value="onPickEntity"
|
||||
option-label="label" clearable class="q-mb-xs"
|
||||
label="+ Ajouter un client / une adresse (nom · adresse · tél. · courriel)">
|
||||
<template #prepend><q-icon name="person_search" color="deep-purple-5" size="18px" /></template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar><q-icon :name="scope.opt.icon" size="18px" color="deep-purple-5" /></q-item-section>
|
||||
<q-item-section><q-item-label>{{ scope.opt.label }}</q-item-label><q-item-label caption>{{ scope.opt.caption }}</q-item-label></q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<template #no-option><q-item><q-item-section class="text-grey-6">Tape au moins 2 lettres…</q-item-section></q-item></template>
|
||||
</q-select>
|
||||
<div class="row items-end no-wrap q-gutter-xs">
|
||||
<q-input v-model="text" type="textarea" autogrow :rows="1" outlined dense class="col orch-input" autofocus
|
||||
placeholder="Écris ta demande… (Entrée pour envoyer)" @keyup.enter.exact.prevent="plan" />
|
||||
<q-btn round dense flat :color="listening ? 'red-6' : 'grey-7'" :icon="listening ? 'mic' : 'mic_none'" @click="toggleMic">
|
||||
<q-tooltip>{{ micSupported ? (listening ? 'Arrêter la dictée' : 'Dicter à voix') : 'Dictée non supportée ici' }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn round dense unelevated color="deep-purple-6" icon="send" :disable="!text.trim() || planning" :loading="planning" @click="plan">
|
||||
<q-tooltip>Envoyer</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -201,6 +205,8 @@ const actions = ref([])
|
|||
const results = ref([])
|
||||
const reply = ref('')
|
||||
const thread = ref([]) // fil de conversation (chat multi-tours) : [{ role:'user'|'assistant', text }]
|
||||
const scrollEl = ref(null)
|
||||
function scrollDown () { nextTick(() => { const el = scrollEl.value && scrollEl.value.$el; if (el) el.scrollTop = el.scrollHeight + 200 }) }
|
||||
|
||||
// En-têtes de requête : identité de l'agent connecté pour les routes STAFF (attribution + suivi + permissions).
|
||||
function reqHeaders () {
|
||||
|
|
@ -224,6 +230,7 @@ async function pickDiag (a, m) {
|
|||
}
|
||||
|
||||
watch(() => props.modelValue, v => { open.value = v; if (v) { reset(); if (props.initialText) { text.value = props.initialText; nextTick(() => plan()) } } })
|
||||
watch(() => thread.value.length + actions.value.length + results.value.length, scrollDown) // défile vers le bas à chaque nouveau contenu (comme un chat)
|
||||
watch(open, v => emit('update:modelValue', v))
|
||||
function reset () { text.value = ''; actions.value = []; results.value = []; reply.value = ''; thread.value = []; if (recog) { try { recog.stop() } catch (e) {} } listening.value = false }
|
||||
|
||||
|
|
@ -297,8 +304,13 @@ async function run () {
|
|||
.orch-diag { border: 1px solid #e0e7ff; border-radius: 8px; padding: 10px; margin-bottom: 8px; background: #f5f7ff; }
|
||||
.diag-dot { width: 11px; height: 11px; border-radius: 50%; margin-right: 10px; flex: 0 0 auto; }
|
||||
.orch-reply { border-left: 3px solid #7c6cf6; background: #f6f5ff; padding: 8px 12px; border-radius: 0 8px 8px 0; color: #4a4a5a; font-size: 0.92rem; white-space: pre-wrap; }
|
||||
.orch-thread { display: flex; flex-direction: column; gap: 8px; max-height: 42vh; overflow-y: auto; }
|
||||
.orch-msg { padding: 8px 12px; border-radius: 10px; font-size: 0.92rem; white-space: pre-wrap; max-width: 88%; line-height: 1.4; }
|
||||
.orch-msg-user { align-self: flex-end; background: #ede9fe; color: #3a3a4a; border-bottom-right-radius: 3px; }
|
||||
.orch-msg-assistant { align-self: flex-start; background: #f6f5ff; border-left: 3px solid #7c6cf6; color: #4a4a5a; border-bottom-left-radius: 3px; }
|
||||
/* Chat moderne : carte à hauteur fixe, conversation qui défile, composer en bas */
|
||||
.orch-card { width: 560px; max-width: 96vw; height: 76vh; max-height: 780px; }
|
||||
.orch-body { flex: 1 1 auto; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; background: #fafafe; }
|
||||
.orch-empty { margin: auto; text-align: center; padding: 24px 16px; max-width: 360px; }
|
||||
.orch-composer { flex: 0 0 auto; padding: 10px 14px 12px; background: #fff; }
|
||||
.orch-composer :deep(textarea) { max-height: 110px; }
|
||||
.orch-msg { padding: 8px 12px; border-radius: 12px; font-size: 0.92rem; white-space: pre-wrap; max-width: 86%; line-height: 1.45; box-shadow: 0 1px 1px rgba(24,32,46,.05); }
|
||||
.orch-msg-user { align-self: flex-end; background: #7c6cf6; color: #fff; border-bottom-right-radius: 3px; }
|
||||
.orch-msg-assistant { align-self: flex-start; background: #fff; border: 1px solid #ece9fb; color: #3a3a4a; border-bottom-left-radius: 3px; }
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user