'use client'
import { submitSimilarThreadFeedback, updateSimilarThreadFeedback } from '~/app/contribute/actions'
import type {
SimilarSolvedThread,
SimilarThreadFeedbackReaction,
ThreadSource,
} from '~/types/contribute'
import { ChevronDown, ThumbsDown, ThumbsUp } from 'lucide-react'
import { useRef, useState } from 'react'
import {
Badge,
Button,
Card,
CardContent,
CardFooter,
CardHeader,
cn,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
Label,
TextArea,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from 'ui'
import { ChannelIcon } from './Icons'
function getChannelFromUrl(url: string): ThreadSource {
const u = url.toLowerCase()
if (u.includes('discord')) return 'discord'
if (u.includes('reddit')) return 'reddit'
if (u.includes('github')) return 'github'
// TODO: handle unknown URL patterns more explicitly, don’t just default to GitHub
return 'github'
}
interface SimilarSolvedThreadsProps {
threads: SimilarSolvedThread[]
parentThreadId: string
}
const SimilarThreadCard = ({
thread,
className,
}: {
thread: SimilarSolvedThread
className?: string
}) => {
const channel = getChannelFromUrl(thread.external_activity_url || '')
const filteredStack = thread.stack?.filter((s) => s !== 'Other') ?? []
const hasStack = filteredStack.length > 0
const url = thread.external_activity_url || null
const linkClassName = cn(
'border-b border-border px-6 py-4 flex items-center gap-3 overflow-hidden hover:bg-surface-200 transition-colors',
className
)
const content = (
<>
{thread.subject}
{thread.problem_description ? (
{thread.problem_description}
) : null}
{hasStack ? (
{filteredStack.map((tech) => (
{tech}
))}
) : null}
>
)
if (url) {
return (
{content}
)
}
return {content}
}
export const SimilarSolvedThreads = ({ threads, parentThreadId }: SimilarSolvedThreadsProps) => {
const [isExpanded, setIsExpanded] = useState(true)
const [submittedReaction, setSubmittedReaction] = useState(
null
)
const [feedbackId, setFeedbackId] = useState(null)
const [dialogOpen, setDialogOpen] = useState(false)
const [dialogReaction, setDialogReaction] = useState('positive')
const [dialogFeedback, setDialogFeedback] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const isClosingProgrammatically = useRef(false)
const handleThumbClick = async (reaction: SimilarThreadFeedbackReaction) => {
if (submittedReaction) return
setIsSubmitting(true)
const result = await submitSimilarThreadFeedback({
parentThreadId,
reaction,
similarThreadKey: null,
})
setIsSubmitting(false)
if (result.success) {
setFeedbackId(result.id ?? null)
setDialogReaction(reaction)
setDialogFeedback('')
setDialogOpen(true)
}
}
const persistAndCloseDialog = async () => {
if (!feedbackId) return
setIsSubmitting(true)
const result = await updateSimilarThreadFeedback(
feedbackId,
dialogReaction,
dialogFeedback.trim() || null
)
setIsSubmitting(false)
if (result.success) {
setSubmittedReaction(dialogReaction)
isClosingProgrammatically.current = true
setDialogOpen(false)
queueMicrotask(() => {
isClosingProgrammatically.current = false
})
}
}
const handleDialogOpenChange = (open: boolean) => {
setDialogOpen(open)
if (!open && feedbackId && !isClosingProgrammatically.current) {
persistAndCloseDialog()
}
}
return (
{isExpanded && (
<>
{threads.map((thread, idx) => (
))}
{submittedReaction ? (
Thanks for helping improve related threads
) : (
Relevant
Irrelevant
)}
>
)}
)
}