feat: AI chat tone selector + graph node pinning
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m48s
CI / Deploy production (on server) (push) Failing after 17s

- ai-chat: sélecteur tone (Professional/Créatif/Académique/Décontracté)
  passé via noteContext.tone dans le body vers /api/chat
- network-graph: dragended garde fx/fy → nœud épinglé après drag
  double-clic sur nœud pour désépingler (fx=null, fy=null)
- sprint-status: 6-2 et 6-3 passés en done

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Antigravity
2026-05-29 14:20:50 +00:00
parent aff8e688a5
commit cd54a983c3
3 changed files with 37 additions and 4 deletions

View File

@@ -64,8 +64,8 @@ development_status:
# Epic 6 — Croissance & Activation (PLG) — ajouté 2026-05-29
epic-6: in-progress
6-1-onboarding-activation: done # story-onboarding-activation.md
6-2-markdown-roundtrip: review # brief-markdown-roundtrip.md
6-3-brainstorm-canvas-finalize: review # story: 6-3-brainstorm-canvas-finalize.md
6-2-markdown-roundtrip: done # brief-markdown-roundtrip.md
6-3-brainstorm-canvas-finalize: done # story: 6-3-brainstorm-canvas-finalize.md
6-4-chat-with-pdf: done # already implemented: document-qa-overlay.tsx + document-ingestion + document-search tool
6-5-pptx-export-watermark: done # story: 6-5-pptx-export-watermark.md
epic-6-retrospective: optional

View File

@@ -42,6 +42,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
const [input, setInput] = useState('')
const [conversationId, setConversationId] = useState<string | undefined>()
const [isContextualAIVisible, setIsContextualAIVisible] = useState(false)
const [tone, setTone] = useState<'professional' | 'creative' | 'academic' | 'casual'>('professional')
const [history, setHistory] = useState<any[]>([])
const [historyLoading, setHistoryLoading] = useState(false)
@@ -97,6 +98,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
webSearch: webSearch && webSearchAvailable,
conversationId: convId,
language,
noteContext: { title: '', content: '', tone },
}
}
)
@@ -273,6 +275,32 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
/>
</div>
{/* Tone selector */}
<div className="flex items-center gap-1.5 flex-wrap">
{(['professional', 'creative', 'academic', 'casual'] as const).map((t_) => {
const labels: Record<typeof t_, string> = {
professional: '💼 Pro',
creative: '✨ Créatif',
academic: '🎓 Académique',
casual: '😊 Décontracté',
}
return (
<button
key={t_}
onClick={() => setTone(t_)}
className={cn(
'px-2.5 py-1 rounded-full text-[10px] font-bold border transition-all',
tone === t_
? 'bg-brand-accent/10 border-brand-accent/40 text-brand-accent'
: 'border-border/40 text-concrete hover:border-ink/20 hover:text-ink/60'
)}
>
{labels[t_]}
</button>
)
})}
</div>
{/* Messages */}
{messages.length === 0 && (
<div className="h-48 flex flex-col items-center justify-center text-center space-y-3 text-concrete/30">

View File

@@ -241,6 +241,11 @@ export function NetworkGraph({
return String(d.clusterId) === selectedClusterId ? 1 : 0.15
})
.on('click', (event, d) => onNoteSelect(d.id))
.on('dblclick', (event, d) => {
d.fx = null
d.fy = null
simulation.alphaTarget(0.1).restart()
})
.call(d3.drag<SVGGElement, D3Node>()
.on('start', dragstarted)
.on('drag', dragged)
@@ -328,8 +333,8 @@ export function NetworkGraph({
function dragended(event: any, d: D3Node) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
// Garder le nœud épinglé à sa position après drag (fx/fy non remis à null)
// Double-clic pour désépingler → géré via dblclick ci-dessous
}
return () => {