'use client' // IdeaTimeline — chronologische merge van IdeaLog + ClaudeQuestion + UserQuestion entries. // Server-component zou ook kunnen, maar we mounten dit binnen de client-side // detail-layout dus client is simpler (geen rsc-boundary doorbreken). // // Iconen + kleur per log-type voor snelle herkenning. // Open ClaudeQuestions krijgen een inline answer-form (M11). // PBI-33: UserQuestions tonen vraag + (indien beantwoord) Claude's antwoord. // Onderaan: UserChatInput om nieuwe vraag te stellen (alleen als plan_md aanwezig is). import { useState, useTransition } from 'react' import { useRouter } from 'next/navigation' import { ClipboardList, FileText, HelpCircle, Lightbulb, MessageCircle, RefreshCw, StickyNote, Wrench, } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { debugProps } from '@/lib/debug' import { answerQuestion } from '@/actions/questions' import { UserChatInput } from '@/components/ideas/user-chat-input' import type { IdeaLogType } from '@prisma/client' export interface TimelineLog { id: string type: string content: string metadata: unknown created_at: string } export interface TimelineQuestion { id: string question: string options: string[] | null status: 'open' | 'answered' | 'cancelled' | 'expired' answer: string | null created_at: string expires_at: string } export interface TimelineUserQuestion { id: string question: string answer: string | null status: 'pending' | 'answered' created_at: string } interface Props { logs: TimelineLog[] questions: TimelineQuestion[] userQuestions: TimelineUserQuestion[] planMd: string | null ideaId: string isDemo?: boolean } const LOG_ICON: Record = { DECISION: , NOTE: , GRILL_RESULT: , PLAN_RESULT: , STATUS_CHANGE: , JOB_EVENT: , } const LOG_LABEL: Record = { DECISION: 'Beslissing', NOTE: 'Notitie', GRILL_RESULT: 'Grill-resultaat', PLAN_RESULT: 'Plan-resultaat', STATUS_CHANGE: 'Status', JOB_EVENT: 'Job-event', } const QUESTION_STATUS_LABEL: Record = { open: 'Open', answered: 'Beantwoord', cancelled: 'Geannuleerd', expired: 'Verlopen', } const USER_QUESTION_STATUS_LABEL: Record = { pending: 'In behandeling', answered: 'Beantwoord', } export function IdeaTimeline({ logs, questions, userQuestions, planMd, ideaId, isDemo = false, }: Props) { const merged = [ ...logs.map((l) => ({ kind: 'log' as const, created_at: l.created_at, data: l, })), ...questions.map((q) => ({ kind: 'question' as const, created_at: q.created_at, data: q, })), ...userQuestions.map((uq) => ({ kind: 'user_question' as const, created_at: uq.created_at, data: uq, })), ].sort((a, b) => (a.created_at < b.created_at ? 1 : -1)) const showChatInput = planMd !== null return (
{merged.length === 0 ? (

Nog geen activiteit op dit idee.

) : (
    {merged.map((entry, i) => { // Expliciete locale + format om SSR/CSR hydration-mismatch te voorkomen // (server-locale verschilde van browser-locale). const time = new Date(entry.created_at).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short', }) if (entry.kind === 'log') { const type = entry.data.type as IdeaLogType return (
  1. {LOG_ICON[type] ?? }
    {LOG_LABEL[type] ?? type} ·

    {entry.data.content}

    {entry.data.metadata != null && typeof entry.data.metadata === 'object' && Object.keys(entry.data.metadata as object).length > 0 ? (
    metadata
                              {JSON.stringify(entry.data.metadata, null, 2)}
                            
    ) : null}
  2. ) } if (entry.kind === 'question') { const q = entry.data return (
  3. Vraag · {QUESTION_STATUS_LABEL[q.status]} ·

    {q.question}

    {q.status === 'open' ? ( ) : ( <> {q.options && q.options.length > 0 ? (
      {q.options.map((o, ii) => (
    • {o}
    • ))}
    ) : null} {q.answer ? (

    Antwoord {q.answer}

    ) : null} )}
  4. ) } // user_question — gebruiker stelt vraag aan Claude (PBI-33 PLAN_CHAT) const uq = entry.data return (
  5. Jouw vraag · {USER_QUESTION_STATUS_LABEL[uq.status]} ·

    {uq.question}

    {uq.status === 'pending' ? (

    Claude denkt na…

    ) : uq.answer ? (
    Antwoord van Claude

    {uq.answer}

    ) : null}
  6. ) })}
)} {showChatInput && }
) } // --------------------------------------------------------------------------- // AnswerForm — inline antwoord op een open Claude-vraag. // Voor multi-choice (options): knoppen die direct submitten met de gekozen // optie. Anders: textarea + Submit knop. function AnswerForm({ questionId, options, }: { questionId: string options: string[] | null }) { const router = useRouter() const [text, setText] = useState('') const [pending, startTransition] = useTransition() function submit(answer: string) { const trimmed = answer.trim() if (!trimmed) { toast.error('Antwoord mag niet leeg zijn') return } startTransition(async () => { const r = await answerQuestion(questionId, trimmed) if (!r.ok) { toast.error(r.error) return } toast.success('Antwoord verzonden — agent gaat door.') setText('') router.refresh() }) } if (options && options.length > 0) { return (
{options.map((opt, i) => ( ))}
Of typ een eigen antwoord