feat(ST-qcmqlv2i): SSE + idea-store — user_question events verwerken
- IdeaUserQuestion type + UserQuestionEvent type in idea-store - userQuestions state + initUserQuestions + handleUserQuestionEvent - clearForIdea filtert ook userQuestions voor het idea - IdeaJobKind uitgebreid met PLAN_CHAT - notifications/route.ts: UserQuestionPayload, isUserQuestionPayload, user_question events forwarden naar SSE, userQuestionsInit in state-event - use-notifications-realtime.ts: UserQuestionPayload handler + initUserQuestions aanroepen bij state-event Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
226bd05594
commit
c0e8270e5f
3 changed files with 106 additions and 9 deletions
|
|
@ -49,23 +49,37 @@ interface IdeaJobPayload {
|
|||
idea_id: string
|
||||
user_id: string
|
||||
product_id?: string | null
|
||||
kind: 'IDEA_GRILL' | 'IDEA_MAKE_PLAN'
|
||||
kind: 'IDEA_GRILL' | 'IDEA_MAKE_PLAN' | 'PLAN_CHAT'
|
||||
status: string
|
||||
}
|
||||
|
||||
type NotifyPayload = QuestionPayload | IdeaJobPayload
|
||||
// UserQuestion-payloads: emitted by app/api/user-questions/[id]/answer and
|
||||
// actions/user-questions.ts via prisma.$executeRaw pg_notify.
|
||||
interface UserQuestionPayload {
|
||||
op: 'I' | 'U'
|
||||
entity: 'user_question'
|
||||
id: string
|
||||
idea_id: string
|
||||
status: 'pending' | 'answered'
|
||||
}
|
||||
|
||||
type NotifyPayload = QuestionPayload | IdeaJobPayload | UserQuestionPayload
|
||||
|
||||
function isQuestionPayload(p: NotifyPayload): p is QuestionPayload {
|
||||
return 'entity' in p && p.entity === 'question'
|
||||
}
|
||||
|
||||
function isUserQuestionPayload(p: NotifyPayload): p is UserQuestionPayload {
|
||||
return 'entity' in p && p.entity === 'user_question'
|
||||
}
|
||||
|
||||
function isIdeaJobPayload(p: NotifyPayload): p is IdeaJobPayload {
|
||||
return (
|
||||
'type' in p &&
|
||||
(p.type === 'claude_job_enqueued' || p.type === 'claude_job_status') &&
|
||||
'idea_id' in p &&
|
||||
'kind' in p &&
|
||||
(p.kind === 'IDEA_GRILL' || p.kind === 'IDEA_MAKE_PLAN')
|
||||
(p.kind === 'IDEA_GRILL' || p.kind === 'IDEA_MAKE_PLAN' || p.kind === 'PLAN_CHAT')
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +178,13 @@ export async function GET(request: NextRequest) {
|
|||
return
|
||||
}
|
||||
|
||||
// UserQuestion (PLAN_CHAT answer-event): user-scoped via idea ownership.
|
||||
if (isUserQuestionPayload(payload)) {
|
||||
if (!accessibleIdeaIds.has(payload.idea_id)) return
|
||||
enqueue(`data: ${msg.payload}\n\n`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isQuestionPayload(payload)) return
|
||||
|
||||
// Idea-question: alleen voor de eigenaar van het idee.
|
||||
|
|
@ -264,7 +285,14 @@ export async function GET(request: NextRequest) {
|
|||
}),
|
||||
].sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
|
||||
|
||||
enqueue(`event: state\ndata: ${JSON.stringify({ questions: stateQuestions })}\n\n`)
|
||||
const userQuestionsInit = await prisma.userQuestion.findMany({
|
||||
where: { idea: { user_id: userId } },
|
||||
orderBy: { created_at: 'desc' },
|
||||
take: 100,
|
||||
select: { id: true, idea_id: true, question: true, answer: true, status: true, created_at: true },
|
||||
})
|
||||
|
||||
enqueue(`event: state\ndata: ${JSON.stringify({ questions: stateQuestions, userQuestions: userQuestionsInit.map(uq => ({ ...uq, created_at: uq.created_at.toISOString() })) })}\n\n`)
|
||||
|
||||
heartbeatTimer = setInterval(() => {
|
||||
enqueue(`: heartbeat\n\n`)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue