aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorMaheshtheDev <[email protected]>2026-01-25 02:15:00 +0000
committerMaheshtheDev <[email protected]>2026-01-25 02:15:00 +0000
commit3ecbc9b43576fe453b42383e40045be67866e6ef (patch)
tree2a4b786e4c768ccb41e339996c1532dc368a1c85 /apps
parentfeat: onboarding config, reset onboarding, xai agentic migration (#701) (diff)
downloadsupermemory-3ecbc9b43576fe453b42383e40045be67866e6ef.tar.xz
supermemory-3ecbc9b43576fe453b42383e40045be67866e6ef.zip
feat: feedback modal for nova users (#703)01-24-feat_feedback_modal_for_nova_users
Diffstat (limited to 'apps')
-rw-r--r--apps/web/components/new/feedback-modal.tsx179
-rw-r--r--apps/web/components/new/header.tsx25
2 files changed, 204 insertions, 0 deletions
diff --git a/apps/web/components/new/feedback-modal.tsx b/apps/web/components/new/feedback-modal.tsx
new file mode 100644
index 00000000..82f4623c
--- /dev/null
+++ b/apps/web/components/new/feedback-modal.tsx
@@ -0,0 +1,179 @@
+"use client"
+
+import { useState } from "react"
+import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@ui/components/dialog"
+import { Textarea } from "@ui/components/textarea"
+import { Button } from "@ui/components/button"
+import { cn } from "@lib/utils"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon, Loader2 } from "lucide-react"
+import { usePostHog } from "@lib/posthog"
+import { toast } from "sonner"
+
+interface FeedbackModalProps {
+ isOpen: boolean
+ onClose: () => void
+}
+
+const FEEDBACK_SURVEY_ID = "019bf2dd-f002-0000-d819-8a914cb23999"
+const FEEDBACK_QUESTION_ID = "0af81ccd-cb43-43a3-a61b-3a74c08a922a"
+const FEEDBACK_QUESTION = "What can we do to improve our product?"
+
+export function FeedbackModal({ isOpen, onClose }: FeedbackModalProps) {
+ const [feedback, setFeedback] = useState("")
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const posthog = usePostHog()
+
+ const handleSubmit = async () => {
+ if (!feedback.trim() || isSubmitting) return
+
+ setIsSubmitting(true)
+
+ try {
+ if (posthog.__loaded) {
+ const responseKey = FEEDBACK_QUESTION_ID.replace(/-/g, "_")
+ posthog.capture("survey sent", {
+ $survey_id: FEEDBACK_SURVEY_ID,
+ $survey_questions: [
+ {
+ id: FEEDBACK_QUESTION_ID,
+ question: FEEDBACK_QUESTION,
+ },
+ ],
+ [`$survey_response_${responseKey}`]: feedback.trim(),
+ $survey_name: "Nova feedback",
+ $survey_completed: true,
+ })
+ }
+
+ setFeedback("")
+ toast.success("Thank you for your feedback!")
+ onClose()
+ } catch (error) {
+ console.error("Failed to submit feedback:", error)
+ toast.error("Failed to submit feedback. Please try again.")
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const handleClose = () => {
+ if (!isSubmitting) {
+ setFeedback("")
+ onClose()
+ }
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+ if (
+ e.key === "Enter" &&
+ (e.metaKey || e.ctrlKey) &&
+ feedback.trim() &&
+ !isSubmitting
+ ) {
+ e.preventDefault()
+ handleSubmit()
+ }
+ }
+
+ return (
+ <Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
+ <DialogContent
+ className={cn(
+ "w-[90%]! max-w-[500px]! border-none bg-[#1B1F24] flex flex-col p-4 gap-4 rounded-[22px]",
+ dmSansClassName(),
+ )}
+ style={{
+ boxShadow:
+ "0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
+ }}
+ showCloseButton={false}
+ >
+ <div className="flex flex-col gap-4">
+ <div className="flex justify-between items-start gap-4">
+ <DialogHeader className="pl-1 space-y-1 flex-1">
+ <DialogTitle
+ className={cn(
+ "font-semibold text-[#fafafa]",
+ dmSans125ClassName(),
+ )}
+ >
+ Feedback
+ </DialogTitle>
+ <p
+ className={cn(
+ "text-[#737373] font-medium text-[16px] leading-[1.35]",
+ )}
+ >
+ {FEEDBACK_QUESTION}
+ </p>
+ </DialogHeader>
+ <DialogPrimitive.Close
+ onClick={handleClose}
+ disabled={isSubmitting}
+ className="bg-[#0D121A] w-7 h-7 flex items-center justify-center focus:ring-ring rounded-full transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 border border-[rgba(115,115,115,0.2)] shrink-0"
+ style={{
+ boxShadow:
+ "0 0.711px 2.842px 0 rgba(0, 0, 0, 0.25), 0.178px 0.178px 0.178px 0 rgba(255, 255, 255, 0.10) inset",
+ }}
+ >
+ <XIcon className="size-4 text-[#737373]" />
+ <span className="sr-only">Close</span>
+ </DialogPrimitive.Close>
+ </div>
+
+ <div className="flex flex-col gap-3">
+ <Textarea
+ value={feedback}
+ onChange={(e) => setFeedback(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Share your thoughts..."
+ className={cn(
+ "min-h-[120px] bg-[#0D121A] border border-[rgba(115,115,115,0.2)] text-[#fafafa] placeholder:text-[#737373] focus-visible:border-[#2261CA] focus-visible:ring-[#2261CA33] resize-none",
+ dmSansClassName(),
+ )}
+ disabled={isSubmitting}
+ />
+
+ <div className="flex justify-end gap-2">
+ <Button
+ variant="ghost"
+ onClick={handleClose}
+ disabled={isSubmitting}
+ className={cn(
+ "text-[#737373] hover:text-[#fafafa] hover:bg-[#293952]/40",
+ dmSansClassName(),
+ )}
+ >
+ Cancel
+ </Button>
+ <Button
+ onClick={handleSubmit}
+ disabled={!feedback.trim() || isSubmitting}
+ className={cn(
+ "bg-[#2261CA] hover:bg-[#1a4fa0] text-white",
+ dmSansClassName(),
+ )}
+ >
+ {isSubmitting ? (
+ <>
+ <Loader2 className="size-4 animate-spin mr-2" />
+ Submitting...
+ </>
+ ) : (
+ "Submit"
+ )}
+ </Button>
+ </div>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/apps/web/components/new/header.tsx b/apps/web/components/new/header.tsx
index 4275e5f8..c244bf88 100644
--- a/apps/web/components/new/header.tsx
+++ b/apps/web/components/new/header.tsx
@@ -36,6 +36,8 @@ import Link from "next/link"
import { SpaceSelector } from "./space-selector"
import { useIsMobile } from "@hooks/use-mobile"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
+import { useState } from "react"
+import { FeedbackModal } from "./feedback-modal"
interface HeaderProps {
onAddMemory?: () => void
@@ -56,12 +58,17 @@ export function Header({
const router = useRouter()
const isMobile = useIsMobile()
const { resetOrgOnboarded } = useOrgOnboarding()
+ const [isFeedbackOpen, setIsFeedbackOpen] = useState(false)
const handleTryOnboarding = () => {
resetOrgOnboarded()
router.push("/new/onboarding?step=input&flow=welcome")
}
+ const handleFeedback = () => {
+ setIsFeedbackOpen(true)
+ }
+
const displayName =
user?.displayUsername ||
localStorage.getItem("username") ||
@@ -222,6 +229,13 @@ export function Header({
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-[#2E3033]" />
<DropdownMenuItem
+ onClick={handleFeedback}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+ <MessageCircleIcon className="h-4 w-4 text-[#737373]" />
+ Feedback
+ </DropdownMenuItem>
+ <DropdownMenuItem
onClick={() => router.push("/new/settings")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
@@ -283,6 +297,16 @@ export function Header({
<span className={cn(dmSansClassName())}>K</span>
</span>
</Button>
+ <Button
+ variant="headers"
+ className="rounded-full text-base gap-2 h-10!"
+ onClick={handleFeedback}
+ >
+ <div className="flex items-center gap-2">
+ <MessageCircleIcon className="size-4" />
+ Feedback
+ </div>
+ </Button>
</>
)}
{user && (
@@ -376,6 +400,7 @@ export function Header({
</DropdownMenu>
)}
</div>
+ <FeedbackModal isOpen={isFeedbackOpen} onClose={() => setIsFeedbackOpen(false)} />
</div>
)
}