feat: AI chat tone selector + graph node pinning
- 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:
@@ -64,8 +64,8 @@ development_status:
|
|||||||
# Epic 6 — Croissance & Activation (PLG) — ajouté 2026-05-29
|
# Epic 6 — Croissance & Activation (PLG) — ajouté 2026-05-29
|
||||||
epic-6: in-progress
|
epic-6: in-progress
|
||||||
6-1-onboarding-activation: done # story-onboarding-activation.md
|
6-1-onboarding-activation: done # story-onboarding-activation.md
|
||||||
6-2-markdown-roundtrip: review # brief-markdown-roundtrip.md
|
6-2-markdown-roundtrip: done # brief-markdown-roundtrip.md
|
||||||
6-3-brainstorm-canvas-finalize: review # story: 6-3-brainstorm-canvas-finalize.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-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
|
6-5-pptx-export-watermark: done # story: 6-5-pptx-export-watermark.md
|
||||||
epic-6-retrospective: optional
|
epic-6-retrospective: optional
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
|||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [conversationId, setConversationId] = useState<string | undefined>()
|
const [conversationId, setConversationId] = useState<string | undefined>()
|
||||||
const [isContextualAIVisible, setIsContextualAIVisible] = useState(false)
|
const [isContextualAIVisible, setIsContextualAIVisible] = useState(false)
|
||||||
|
const [tone, setTone] = useState<'professional' | 'creative' | 'academic' | 'casual'>('professional')
|
||||||
|
|
||||||
const [history, setHistory] = useState<any[]>([])
|
const [history, setHistory] = useState<any[]>([])
|
||||||
const [historyLoading, setHistoryLoading] = useState(false)
|
const [historyLoading, setHistoryLoading] = useState(false)
|
||||||
@@ -97,6 +98,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
|||||||
webSearch: webSearch && webSearchAvailable,
|
webSearch: webSearch && webSearchAvailable,
|
||||||
conversationId: convId,
|
conversationId: convId,
|
||||||
language,
|
language,
|
||||||
|
noteContext: { title: '', content: '', tone },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -273,6 +275,32 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div className="h-48 flex flex-col items-center justify-center text-center space-y-3 text-concrete/30">
|
<div className="h-48 flex flex-col items-center justify-center text-center space-y-3 text-concrete/30">
|
||||||
|
|||||||
@@ -241,6 +241,11 @@ export function NetworkGraph({
|
|||||||
return String(d.clusterId) === selectedClusterId ? 1 : 0.15
|
return String(d.clusterId) === selectedClusterId ? 1 : 0.15
|
||||||
})
|
})
|
||||||
.on('click', (event, d) => onNoteSelect(d.id))
|
.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>()
|
.call(d3.drag<SVGGElement, D3Node>()
|
||||||
.on('start', dragstarted)
|
.on('start', dragstarted)
|
||||||
.on('drag', dragged)
|
.on('drag', dragged)
|
||||||
@@ -328,8 +333,8 @@ export function NetworkGraph({
|
|||||||
|
|
||||||
function dragended(event: any, d: D3Node) {
|
function dragended(event: any, d: D3Node) {
|
||||||
if (!event.active) simulation.alphaTarget(0)
|
if (!event.active) simulation.alphaTarget(0)
|
||||||
d.fx = null
|
// Garder le nœud épinglé à sa position après drag (fx/fy non remis à null)
|
||||||
d.fy = null
|
// Double-clic pour désépingler → géré via dblclick ci-dessous
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user