Compare commits
1 commit
main
...
feat/story
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82736fd051 |
2 changed files with 112 additions and 4 deletions
|
|
@ -44,7 +44,7 @@ const TABS: { key: TabKey; label: string }[] = [
|
||||||
{ key: 'idee', label: 'Idee' },
|
{ key: 'idee', label: 'Idee' },
|
||||||
{ key: 'grill', label: 'Grill' },
|
{ key: 'grill', label: 'Grill' },
|
||||||
{ key: 'plan', label: 'Plan' },
|
{ key: 'plan', label: 'Plan' },
|
||||||
{ key: 'timeline', label: 'Timeline' },
|
{ key: 'timeline', label: 'Chat & Timeline' },
|
||||||
]
|
]
|
||||||
|
|
||||||
interface IdeaLog {
|
interface IdeaLog {
|
||||||
|
|
@ -221,7 +221,15 @@ export function IdeaDetailLayout({
|
||||||
ideaId={idea.id}
|
ideaId={idea.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === 'timeline' && <IdeaTimeline logs={logs} questions={questions} />}
|
{tab === 'timeline' && (
|
||||||
|
<IdeaTimeline
|
||||||
|
logs={logs}
|
||||||
|
questions={questions}
|
||||||
|
userQuestions={userQuestions}
|
||||||
|
planMd={plan_md}
|
||||||
|
ideaId={idea.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
FileText,
|
FileText,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
|
MessageCircle,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
StickyNote,
|
StickyNote,
|
||||||
Wrench,
|
Wrench,
|
||||||
|
|
@ -24,6 +25,7 @@ import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { answerQuestion } from '@/actions/questions'
|
import { answerQuestion } from '@/actions/questions'
|
||||||
|
import { createUserQuestionAction } from '@/actions/user-questions'
|
||||||
|
|
||||||
import type { IdeaLogType } from '@prisma/client'
|
import type { IdeaLogType } from '@prisma/client'
|
||||||
|
|
||||||
|
|
@ -45,9 +47,20 @@ export interface TimelineQuestion {
|
||||||
expires_at: string
|
expires_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TimelineUserQuestion {
|
||||||
|
id: string
|
||||||
|
question: string
|
||||||
|
answer: string | null
|
||||||
|
status: 'pending' | 'answered'
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logs: TimelineLog[]
|
logs: TimelineLog[]
|
||||||
questions: TimelineQuestion[]
|
questions: TimelineQuestion[]
|
||||||
|
userQuestions: TimelineUserQuestion[]
|
||||||
|
planMd: string | null
|
||||||
|
ideaId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOG_ICON: Record<IdeaLogType, React.ReactNode> = {
|
const LOG_ICON: Record<IdeaLogType, React.ReactNode> = {
|
||||||
|
|
@ -75,7 +88,7 @@ const QUESTION_STATUS_LABEL: Record<TimelineQuestion['status'], string> = {
|
||||||
expired: 'Verlopen',
|
expired: 'Verlopen',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IdeaTimeline({ logs, questions }: Props) {
|
export function IdeaTimeline({ logs, questions, userQuestions, planMd, ideaId }: Props) {
|
||||||
const merged = [
|
const merged = [
|
||||||
...logs.map((l) => ({
|
...logs.map((l) => ({
|
||||||
kind: 'log' as const,
|
kind: 'log' as const,
|
||||||
|
|
@ -87,9 +100,14 @@ export function IdeaTimeline({ logs, questions }: Props) {
|
||||||
created_at: q.created_at,
|
created_at: q.created_at,
|
||||||
data: q,
|
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))
|
].sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
|
||||||
|
|
||||||
if (merged.length === 0) {
|
if (merged.length === 0 && !planMd) {
|
||||||
return (
|
return (
|
||||||
<p className="text-sm text-muted-foreground py-8 text-center italic">
|
<p className="text-sm text-muted-foreground py-8 text-center italic">
|
||||||
Nog geen activiteit op dit idee.
|
Nog geen activiteit op dit idee.
|
||||||
|
|
@ -98,6 +116,7 @@ export function IdeaTimeline({ logs, questions }: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
<ol className="border-l-2 border-input pl-4 space-y-3 ml-2">
|
<ol className="border-l-2 border-input pl-4 space-y-3 ml-2">
|
||||||
{merged.map((entry, i) => {
|
{merged.map((entry, i) => {
|
||||||
// Expliciete locale + format om SSR/CSR hydration-mismatch te voorkomen
|
// Expliciete locale + format om SSR/CSR hydration-mismatch te voorkomen
|
||||||
|
|
@ -138,6 +157,37 @@ export function IdeaTimeline({ logs, questions }: Props) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.kind === 'user_question') {
|
||||||
|
const uq = entry.data
|
||||||
|
return (
|
||||||
|
<li key={`uq-${uq.id}-${i}`} className="relative">
|
||||||
|
<span className="absolute -left-[26px] top-1 flex size-5 items-center justify-center rounded-full bg-surface-container text-primary">
|
||||||
|
<MessageCircle className="size-4" />
|
||||||
|
</span>
|
||||||
|
<div className="rounded-md border border-input bg-surface-container p-3 space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span className="font-medium uppercase tracking-wide">Jouw vraag</span>
|
||||||
|
<span>·</span>
|
||||||
|
<time>{time}</time>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm">{uq.question}</p>
|
||||||
|
{uq.status === 'pending' ? (
|
||||||
|
<p className="text-xs text-muted-foreground italic">
|
||||||
|
Wacht op antwoord van Claude...
|
||||||
|
</p>
|
||||||
|
) : uq.answer ? (
|
||||||
|
<div className="mt-2 rounded bg-muted/50 p-3 text-sm">
|
||||||
|
<span className="text-xs font-medium uppercase tracking-wide text-primary mr-2">
|
||||||
|
Claude
|
||||||
|
</span>
|
||||||
|
{uq.answer}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const q = entry.data
|
const q = entry.data
|
||||||
return (
|
return (
|
||||||
<li key={`q-${q.id}-${i}`} className="relative">
|
<li key={`q-${q.id}-${i}`} className="relative">
|
||||||
|
|
@ -179,6 +229,12 @@ export function IdeaTimeline({ logs, questions }: Props) {
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
|
{planMd && (
|
||||||
|
<div className="mt-4 border-t pt-4">
|
||||||
|
<UserChatInput ideaId={ideaId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,3 +325,47 @@ function AnswerForm({
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// UserChatInput — stel een vraag aan Claude over het plan van dit idee.
|
||||||
|
|
||||||
|
function UserChatInput({ ideaId }: { ideaId: string }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const [question, setQuestion] = useState('')
|
||||||
|
const [isPending, startTransition] = useTransition()
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
const trimmed = question.trim()
|
||||||
|
if (!trimmed) return
|
||||||
|
startTransition(async () => {
|
||||||
|
const res = await createUserQuestionAction(ideaId, trimmed)
|
||||||
|
if ('success' in res && res.success) {
|
||||||
|
setQuestion('')
|
||||||
|
toast.success('Vraag verstuurd — Claude antwoordt zodra mogelijk.')
|
||||||
|
router.refresh()
|
||||||
|
} else if ('error' in res) {
|
||||||
|
toast.error(res.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||||
|
Stel een vraag aan Claude
|
||||||
|
</p>
|
||||||
|
<Textarea
|
||||||
|
value={question}
|
||||||
|
onChange={(e) => setQuestion(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Vraag iets over het plan…"
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button size="sm" disabled={isPending || !question.trim()} onClick={submit}>
|
||||||
|
Vraag stellen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue