aboutsummaryrefslogtreecommitdiff
path: root/apps/web/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components')
-rw-r--r--apps/web/components/new/document-modal/index.tsx125
-rw-r--r--apps/web/components/new/document-modal/title.tsx6
-rw-r--r--apps/web/components/new/onboarding/setup/chat-sidebar.tsx549
-rw-r--r--apps/web/components/new/onboarding/setup/integrations-step.tsx2
-rw-r--r--apps/web/components/new/onboarding/setup/relatable-question.tsx2
-rw-r--r--apps/web/components/new/onboarding/welcome/continue-step.tsx200
-rw-r--r--apps/web/components/new/onboarding/welcome/features-step.tsx98
-rw-r--r--apps/web/components/new/onboarding/welcome/profile-step.tsx2
-rw-r--r--apps/web/components/views/chat/chat-messages.tsx3
9 files changed, 709 insertions, 278 deletions
diff --git a/apps/web/components/new/document-modal/index.tsx b/apps/web/components/new/document-modal/index.tsx
index bd8ec05d..8d4aac87 100644
--- a/apps/web/components/new/document-modal/index.tsx
+++ b/apps/web/components/new/document-modal/index.tsx
@@ -2,7 +2,7 @@
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
-import { ArrowUpRightIcon, XIcon, Loader2 } from "lucide-react"
+import { ArrowUpRightIcon, XIcon, Loader2, Trash2Icon, CheckIcon } from "lucide-react"
import type { z } from "zod"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cn } from "@lib/utils"
@@ -20,6 +20,8 @@ import { useState, useEffect, useCallback, useMemo } from "react"
import { motion, AnimatePresence } from "motion/react"
import { Button } from "@repo/ui/components/button"
import { useDocumentMutations } from "@/hooks/use-document-mutations"
+import type { UseMutationResult } from "@tanstack/react-query"
+import { toast } from "sonner"
// Dynamically importing to prevent DOMMatrix error
const PdfViewer = dynamic(
@@ -43,12 +45,102 @@ interface DocumentModalProps {
onClose: () => void
}
+interface DeleteButtonProps {
+ documentId: string | null | undefined
+ customId: string | null | undefined
+ deleteMutation: UseMutationResult<
+ unknown,
+ Error,
+ { documentId: string },
+ unknown
+ >
+}
+
+function isTemporaryId(id: string | null | undefined): boolean {
+ if (!id) return false
+ return id.startsWith("temp-") || id.startsWith("temp-file-")
+}
+
+function DeleteButton({ documentId, customId, deleteMutation }: DeleteButtonProps) {
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
+
+ const handleDelete = useCallback(() => {
+ const id = documentId ?? customId
+ if (!id) return
+
+ // Check both IDs to ensure we catch temporary documents regardless of which ID is used
+ if (isTemporaryId(documentId) || isTemporaryId(customId)) {
+ // this is when user added document immediately and trying to delete
+ toast.error("Cannot delete document", {
+ description: "This document is still being processed. Please wait.",
+ })
+ return
+ }
+
+ deleteMutation.mutate({ documentId: id as string })
+ }, [documentId, customId, deleteMutation])
+
+ return (
+ <AnimatePresence mode="wait">
+ {!deleteConfirmOpen ? (
+ <motion.button
+ key="trash"
+ type="button"
+ initial={{ opacity: 0, scale: 0.8 }}
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.8 }}
+ transition={{ duration: 0.15 }}
+ onClick={() => setDeleteConfirmOpen(true)}
+ tabIndex={-1}
+ className="bg-[#0D121A] w-7 h-7 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus:outline-none cursor-pointer shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
+ disabled={deleteMutation.isPending}
+ >
+ <Trash2Icon className="w-4 h-4 text-red-500" />
+ <span className="sr-only">Delete document</span>
+ </motion.button>
+ ) : (
+ <motion.div
+ key="confirm"
+ initial={{ opacity: 0, scale: 0.8 }}
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.8 }}
+ transition={{ duration: 0.15 }}
+ className="flex items-center gap-1 px-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
+ >
+ <button
+ type="button"
+ onClick={() => setDeleteConfirmOpen(false)}
+ disabled={deleteMutation.isPending}
+ className="w-6 h-6 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus:outline-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ <XIcon className="w-4 h-4 text-[#737373]" />
+ <span className="sr-only">Cancel delete</span>
+ </button>
+ <button
+ type="button"
+ onClick={handleDelete}
+ disabled={deleteMutation.isPending}
+ className="w-6 h-6 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus:outline-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {deleteMutation.isPending ? (
+ <Loader2 className="w-4 h-4 text-green-500 animate-spin" />
+ ) : (
+ <CheckIcon className="w-4 h-4 text-green-500" />
+ )}
+ <span className="sr-only">Confirm delete</span>
+ </button>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ )
+}
+
export function DocumentModal({
document: _document,
isOpen,
onClose,
}: DocumentModalProps) {
- const { updateMutation } = useDocumentMutations()
+ const { updateMutation, deleteMutation } = useDocumentMutations({ onClose })
const { initialEditorContent, initialEditorString } = useMemo(() => {
const content = _document?.content as string | null | undefined
@@ -76,7 +168,9 @@ export function DocumentModal({
}, [initialEditorString])
useEffect(() => {
- if (!isOpen) resetEditor()
+ if (!isOpen) {
+ resetEditor()
+ }
}, [isOpen, resetEditor])
const hasUnsavedChanges =
@@ -107,13 +201,20 @@ export function DocumentModal({
<DialogTitle className="sr-only">
{_document?.title} - Document
</DialogTitle>
- <div className="flex justify-between h-fit">
- <Title
- title={_document?.title}
- documentType={_document?.type ?? "text"}
- url={_document?.url}
- />
- <div className="flex items-center gap-2">
+ <div className="flex items-center justify-between h-fit gap-4">
+ <div className="flex-1 min-w-0">
+ <Title
+ title={_document?.title}
+ documentType={_document?.type ?? "text"}
+ url={_document?.url}
+ />
+ </div>
+ <div className="flex items-center gap-2 shrink-0">
+ <DeleteButton
+ documentId={_document?.id}
+ customId={_document?.customId}
+ deleteMutation={deleteMutation}
+ />
{_document?.url && (
<a
href={_document.url}
@@ -126,8 +227,10 @@ export function DocumentModal({
</a>
)}
<DialogPrimitive.Close
- 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 cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
+ className="bg-[#0D121A] w-7 h-7 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus:outline-none disabled:pointer-events-none cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
data-slot="dialog-close"
+ type="button"
+ tabIndex={-1}
>
<XIcon stroke="#737373" />
<span className="sr-only">Close</span>
diff --git a/apps/web/components/new/document-modal/title.tsx b/apps/web/components/new/document-modal/title.tsx
index 066c437b..59eb285e 100644
--- a/apps/web/components/new/document-modal/title.tsx
+++ b/apps/web/components/new/document-modal/title.tsx
@@ -30,10 +30,10 @@ export function Title({
<div
className={cn(
dmSansClassName(),
- "text-[16px] font-semibold text-[#FAFAFA] line-clamp-1 leading-[125%] flex items-center gap-3",
+ "text-[16px] font-semibold text-[#FAFAFA] leading-[125%] flex items-center gap-3 min-w-0",
)}
>
- <div className="pl-1 flex items-center gap-1 w-5 h-5">
+ <div className="pl-1 flex items-center gap-1 w-5 h-5 shrink-0">
{getDocumentIcon(
documentType as DocumentType,
"w-5 h-5",
@@ -49,7 +49,7 @@ export function Title({
</p>
)}
</div>
- {title || "Untitled Document"}
+ <span className="truncate">{title || "Untitled Document"}</span>
</div>
)
}
diff --git a/apps/web/components/new/onboarding/setup/chat-sidebar.tsx b/apps/web/components/new/onboarding/setup/chat-sidebar.tsx
index 59742271..dd6eeb23 100644
--- a/apps/web/components/new/onboarding/setup/chat-sidebar.tsx
+++ b/apps/web/components/new/onboarding/setup/chat-sidebar.tsx
@@ -2,14 +2,18 @@
import { useState, useEffect, useCallback, useRef } from "react"
import { motion, AnimatePresence } from "motion/react"
+import { useChat } from "@ai-sdk/react"
+import { DefaultChatTransport } from "ai"
import NovaOrb from "@/components/nova/nova-orb"
import { Button } from "@ui/components/button"
-import { PanelRightCloseIcon, SendIcon } from "lucide-react"
+import { PanelRightCloseIcon, SendIcon, CheckIcon, XIcon } from "lucide-react"
import { collectValidUrls } from "@/lib/url-helpers"
import { $fetch } from "@lib/api"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { useAuth } from "@lib/auth-context"
+import { useProject } from "@/stores"
+import { Streamdown } from "streamdown"
interface ChatSidebarProps {
formData: {
@@ -20,11 +24,20 @@ interface ChatSidebarProps {
} | null
}
+interface DraftDoc {
+ kind: "likes" | "link" | "x_research"
+ content: string
+ metadata: Record<string, string>
+ title?: string
+ url?: string
+}
+
export function ChatSidebar({ formData }: ChatSidebarProps) {
const { user } = useAuth()
+ const { selectedProject } = useProject()
const [message, setMessage] = useState("")
const [isChatOpen, setIsChatOpen] = useState(true)
- const [messages, setMessages] = useState<
+ const [timelineMessages, setTimelineMessages] = useState<
{
message: string
type?: "formData" | "exa" | "memory" | "waiting"
@@ -40,10 +53,82 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
}[]
>([])
const [isLoading, setIsLoading] = useState(false)
+ const [isFetchingDrafts, setIsFetchingDrafts] = useState(false)
+ const [draftDocs, setDraftDocs] = useState<DraftDoc[]>([])
+ const [xResearchStatus, setXResearchStatus] = useState<
+ "correct" | "incorrect" | null
+ >(null)
+ const [isConfirmed, setIsConfirmed] = useState(false)
const displayedMemoriesRef = useRef<Set<string>>(new Set())
+ const contextInjectedRef = useRef(false)
+ const draftsBuiltRef = useRef(false)
+ const isProcessingRef = useRef(false)
+
+ const {
+ messages: chatMessages,
+ sendMessage,
+ status,
+ } = useChat({
+ transport: new DefaultChatTransport({
+ api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
+ credentials: "include",
+ body: {
+ metadata: {
+ projectId: selectedProject,
+ model: "gemini-2.5-pro",
+ },
+ },
+ }),
+ })
+
+ const buildOnboardingContext = useCallback(() => {
+ if (!formData) return ""
+
+ const contextParts: string[] = []
+
+ if (formData.description?.trim()) {
+ contextParts.push(`User's interests/likes: ${formData.description}`)
+ }
+
+ if (formData.twitter) {
+ contextParts.push(`X/Twitter profile: ${formData.twitter}`)
+ }
+
+ if (formData.linkedin) {
+ contextParts.push(`LinkedIn profile: ${formData.linkedin}`)
+ }
+
+ if (formData.otherLinks.length > 0) {
+ contextParts.push(`Other links: ${formData.otherLinks.join(", ")}`)
+ }
+
+ const memoryTexts = timelineMessages
+ .filter((msg) => msg.type === "memory" && msg.memories)
+ .flatMap(
+ (msg) => msg.memories?.map((m) => `${m.title}: ${m.description}`) || [],
+ )
+
+ if (memoryTexts.length > 0) {
+ contextParts.push(`Extracted memories:\n${memoryTexts.join("\n")}`)
+ }
+
+ return contextParts.join("\n\n")
+ }, [formData, timelineMessages])
const handleSend = () => {
- console.log("Message:", message)
+ if (!message.trim() || status === "submitted" || status === "streaming")
+ return
+
+ let messageToSend = message
+
+ const context = buildOnboardingContext()
+
+ if (context && !contextInjectedRef.current && chatMessages.length === 0) {
+ messageToSend = `${context}\n\nUser question: ${message}`
+ contextInjectedRef.current = true
+ }
+
+ sendMessage({ text: messageToSend })
setMessage("")
}
@@ -97,8 +182,8 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
},
)
- if (newMemories.length > 0 && messages.length < 10) {
- setMessages((prev) => [
+ if (newMemories.length > 0 && timelineMessages.length < 10) {
+ setTimelineMessages((prev) => [
...prev,
{
message: newMemories
@@ -123,13 +208,151 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
}
}
},
- [messages.length],
+ [timelineMessages.length],
)
+ const buildDraftDocs = useCallback(async () => {
+ if (!formData || draftsBuiltRef.current) return
+ draftsBuiltRef.current = true
+
+ const hasContent =
+ formData.twitter ||
+ formData.linkedin ||
+ formData.otherLinks.length > 0 ||
+ formData.description?.trim()
+
+ if (!hasContent) return
+
+ setIsFetchingDrafts(true)
+ const drafts: DraftDoc[] = []
+
+ try {
+ if (formData.description?.trim()) {
+ drafts.push({
+ kind: "likes",
+ content: formData.description,
+ metadata: {
+ sm_source: "consumer",
+ description_source: "user_input",
+ },
+ title: "Your Interests",
+ })
+ }
+
+ const urls = collectValidUrls(formData.linkedin, formData.otherLinks)
+
+ const [exaResults, xResearchResult] = await Promise.all([
+ urls.length > 0
+ ? fetch("/api/onboarding/extract-content", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ urls }),
+ })
+ .then((r) => r.json())
+ .then((data) => data.results || [])
+ .catch(() => [])
+ : Promise.resolve([]),
+ formData.twitter
+ ? fetch("/api/onboarding/research", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ xUrl: formData.twitter,
+ name: user?.name,
+ email: user?.email,
+ }),
+ })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((data) => data?.text?.trim() || null)
+ .catch(() => null)
+ : Promise.resolve(null),
+ ])
+
+ for (const result of exaResults) {
+ if (result.text || result.description) {
+ drafts.push({
+ kind: "link",
+ content: result.text || result.description || "",
+ metadata: {
+ sm_source: "consumer",
+ exa_url: result.url,
+ exa_title: result.title,
+ },
+ title: result.title || "Extracted Content",
+ url: result.url,
+ })
+ }
+ }
+
+ if (xResearchResult) {
+ drafts.push({
+ kind: "x_research",
+ content: xResearchResult,
+ metadata: {
+ sm_source: "consumer",
+ onboarding_source: "x_research",
+ x_url: formData.twitter,
+ },
+ title: "X/Twitter Profile Research",
+ url: formData.twitter,
+ })
+ }
+
+ setDraftDocs(drafts)
+ } catch (error) {
+ console.warn("Error building draft docs:", error)
+ } finally {
+ setIsFetchingDrafts(false)
+ }
+ }, [formData, user])
+
+ const handleConfirmDocs = useCallback(async () => {
+ if (isConfirmed || isProcessingRef.current) return
+ isProcessingRef.current = true
+ setIsConfirmed(true)
+ setIsLoading(true)
+
+ try {
+ const documentIds: string[] = []
+
+ for (const draft of draftDocs) {
+ if (draft.kind === "x_research" && xResearchStatus !== "correct") {
+ continue
+ }
+
+ try {
+ const docResponse = await $fetch("@post/documents", {
+ body: {
+ content: draft.content,
+ containerTags: ["sm_project_default"],
+ metadata: draft.metadata,
+ },
+ })
+
+ if (docResponse.data?.id) {
+ documentIds.push(docResponse.data.id)
+ }
+ } catch (error) {
+ console.warn("Error creating document:", error)
+ }
+ }
+
+ if (documentIds.length > 0) {
+ await pollForMemories(documentIds)
+ }
+ } catch (error) {
+ console.warn("Error confirming documents:", error)
+ setIsConfirmed(false)
+ } finally {
+ setIsLoading(false)
+ isProcessingRef.current = false
+ }
+ }, [draftDocs, xResearchStatus, isConfirmed, pollForMemories])
+
useEffect(() => {
if (!formData) return
- const formDataMessages: typeof messages = []
+ const formDataMessages: typeof timelineMessages = []
if (formData.twitter) {
formDataMessages.push({
@@ -172,125 +395,9 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
})
}
- setMessages(formDataMessages)
-
- const hasContent =
- formData.twitter ||
- formData.linkedin ||
- formData.otherLinks.length > 0 ||
- formData.description?.trim()
-
- if (!hasContent) return
-
- const urls = collectValidUrls(formData.linkedin, formData.otherLinks)
-
- const processContent = async () => {
- setIsLoading(true)
-
- try {
- const documentIds: string[] = []
-
- if (formData.description?.trim()) {
- try {
- const descDocResponse = await $fetch("@post/documents", {
- body: {
- content: formData.description,
- containerTags: ["sm_project_default"],
- metadata: {
- sm_source: "consumer",
- description_source: "user_input",
- },
- },
- })
-
- if (descDocResponse.data?.id) {
- documentIds.push(descDocResponse.data.id)
- }
- } catch (error) {
- console.warn("Error creating description document:", error)
- }
- }
-
- if (formData.twitter) {
- try {
- const researchResponse = await fetch("/api/onboarding/research", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- xUrl: formData.twitter,
- name: user?.name,
- email: user?.email,
- }),
- })
-
- if (researchResponse.ok) {
- const { text } = await researchResponse.json()
-
- if (text?.trim()) {
- const xDocResponse = await $fetch("@post/documents", {
- body: {
- content: text,
- containerTags: ["sm_project_default"],
- metadata: {
- sm_source: "consumer",
- onboarding_source: "x_research",
- x_url: formData.twitter,
- },
- },
- })
-
- if (xDocResponse.data?.id) {
- documentIds.push(xDocResponse.data.id)
- }
- }
- }
- } catch (error) {
- console.warn("Error fetching X research:", error)
- }
- }
-
- if (urls.length > 0) {
- const response = await fetch("/api/onboarding/extract-content", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ urls }),
- })
- const { results } = await response.json()
-
- for (const result of results) {
- try {
- const docResponse = await $fetch("@post/documents", {
- body: {
- content: result.text || result.description || "",
- containerTags: ["sm_project_default"],
- metadata: {
- sm_source: "consumer",
- exa_url: result.url,
- exa_title: result.title,
- },
- },
- })
-
- if (docResponse.data?.id) {
- documentIds.push(docResponse.data.id)
- }
- } catch (error) {
- console.warn("Error creating document:", error)
- }
- }
- }
-
- if (documentIds.length > 0) {
- await pollForMemories(documentIds)
- }
- } catch (error) {
- console.warn("Error processing content:", error)
- }
- setIsLoading(false)
- }
-
- processContent()
- }, [formData, pollForMemories, user])
+ setTimelineMessages(formDataMessages)
+ buildDraftDocs()
+ }, [formData, buildDraftDocs])
return (
<AnimatePresence mode="wait">
@@ -337,8 +444,8 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
<PanelRightCloseIcon className="size-4" />
Close chat
</motion.button>
- <div className="flex-1 flex flex-col px-4 space-y-3 pb-4 justify-end">
- {messages.map((msg, i) => (
+ <div className="flex-1 flex flex-col px-4 space-y-3 pb-4 justify-end overflow-y-auto scrollbar-thin">
+ {timelineMessages.map((msg, i) => (
<div
key={`message-${i}-${msg.message}`}
className="flex items-start gap-2"
@@ -438,12 +545,78 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
)}
</div>
))}
- {messages.length === 0 && !isLoading && !formData && (
- <div className="flex items-center gap-2 text-white/50">
- <NovaOrb size={28} className="blur-none!" />
- <span className="text-sm">Waiting for your input</span>
- </div>
- )}
+ {chatMessages.map((msg) => {
+ if (msg.role === "user") {
+ const text = msg.parts
+ .filter((part) => part.type === "text")
+ .map((part) => part.text)
+ .join(" ")
+ return (
+ <div
+ key={msg.id}
+ className="flex items-start gap-2 justify-end"
+ >
+ <div className="bg-[#1B1F24] rounded-[12px] p-3 px-[14px] max-w-[80%]">
+ <p className="text-sm text-white">{text}</p>
+ </div>
+ </div>
+ )
+ }
+ if (msg.role === "assistant") {
+ return (
+ <div key={msg.id} className="flex items-start gap-2">
+ <NovaOrb size={30} className="blur-none!" />
+ <div className="flex-1">
+ {msg.parts.map((part, partIndex) => {
+ if (part.type === "text") {
+ return (
+ <div
+ key={`${msg.id}-${partIndex}`}
+ className="text-sm text-white/90 chat-markdown-content"
+ >
+ <Streamdown>{part.text}</Streamdown>
+ </div>
+ )
+ }
+ if (part.type === "tool-searchMemories") {
+ if (
+ part.state === "input-available" ||
+ part.state === "input-streaming"
+ ) {
+ return (
+ <div
+ key={`${msg.id}-${partIndex}`}
+ className="text-xs text-white/50 italic"
+ >
+ Searching memories...
+ </div>
+ )
+ }
+ }
+ return null
+ })}
+ </div>
+ </div>
+ )
+ }
+ return null
+ })}
+ {(status === "submitted" || status === "streaming") &&
+ chatMessages[chatMessages.length - 1]?.role === "user" && (
+ <div className="flex items-start gap-2">
+ <NovaOrb size={30} className="blur-none!" />
+ <span className="text-sm text-white/50">Thinking...</span>
+ </div>
+ )}
+ {timelineMessages.length === 0 &&
+ chatMessages.length === 0 &&
+ !isLoading &&
+ !formData && (
+ <div className="flex items-center gap-2 text-white/50">
+ <NovaOrb size={28} className="blur-none!" />
+ <span className="text-sm">Waiting for your input</span>
+ </div>
+ )}
{isLoading && (
<div className="flex items-center gap-2 text-foreground/50">
<NovaOrb size={28} className="blur-none!" />
@@ -452,7 +625,106 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
)}
</div>
- <div className="p-4">
+ {draftDocs.some((d) => d.kind === "x_research") && !isConfirmed && (
+ <div className="px-4 pb-2 space-y-3">
+ <div className="bg-[#293952]/40 rounded-lg p-3 space-y-2">
+ <h3
+ className="text-sm font-medium"
+ style={{
+ background:
+ "linear-gradient(90deg, #369BFD 0%, #36FDFD 30%, #36FDB5 100%)",
+ WebkitBackgroundClip: "text",
+ WebkitTextFillColor: "transparent",
+ backgroundClip: "text",
+ }}
+ >
+ Your Profile Summary
+ </h3>
+ <div className="overflow-y-auto scrollbar-thin max-h-32">
+ <p className="text-xs text-white/70">
+ {draftDocs.find((d) => d.kind === "x_research")?.content}
+ </p>
+ </div>
+ <div className="flex items-center gap-2 pt-2">
+ <span className="text-xs text-white/50">
+ Is this accurate?
+ </span>
+ <button
+ type="button"
+ onClick={() => {
+ setXResearchStatus("correct")
+ handleConfirmDocs()
+ }}
+ disabled={isConfirmed || isLoading}
+ className={cn(
+ "flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors cursor-pointer",
+ xResearchStatus === "correct"
+ ? "bg-green-500/20 text-green-400 border border-green-500/40"
+ : "bg-[#1B1F24] text-white/50 hover:text-white/70",
+ (isConfirmed || isLoading) && "opacity-50 cursor-not-allowed",
+ )}
+ >
+ <CheckIcon className="size-3" />
+ Correct
+ </button>
+ <button
+ type="button"
+ onClick={() => setXResearchStatus("incorrect")}
+ className={cn(
+ "flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors cursor-pointer",
+ xResearchStatus === "incorrect"
+ ? "bg-red-500/20 text-red-400 border border-red-500/40"
+ : "bg-[#1B1F24] text-white/50 hover:text-white/70",
+ )}
+ >
+ <XIcon className="size-3" />
+ Incorrect
+ </button>
+ </div>
+ {xResearchStatus === "incorrect" && (
+ <>
+ <p className="text-xs text-white/40 pt-1">
+ If incorrect, share your info in the input below, or you
+ can add memories later as well.
+ </p>
+ <Button
+ type="button"
+ onClick={handleConfirmDocs}
+ disabled={isConfirmed || isLoading}
+ className="w-full bg-[#267BF1] hover:bg-[#1E6AD9] text-white rounded-lg py-2 text-sm cursor-pointer mt-2 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Continue
+ </Button>
+ </>
+ )}
+ </div>
+ </div>
+ )}
+
+ {!draftDocs.some((d) => d.kind === "x_research") &&
+ draftDocs.length > 0 &&
+ !isConfirmed && (
+ <div className="px-4 pb-2">
+ <Button
+ type="button"
+ onClick={handleConfirmDocs}
+ disabled={isConfirmed || isLoading}
+ className="w-full bg-[#267BF1] hover:bg-[#1E6AD9] text-white rounded-lg py-2 text-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ Continue
+ </Button>
+ </div>
+ )}
+
+ <div className="p-4 space-y-2">
+ {isFetchingDrafts && (
+ <div className="flex items-center gap-2 text-white/50 px-2">
+ <NovaOrb size={20} className="blur-none!" />
+ <span className="text-sm">
+ Getting all relevant info about you...
+ </span>
+ </div>
+ )}
<form
className="flex flex-col gap-3 bg-[#0D121A] rounded-xl p-2 relative"
onSubmit={(e) => {
@@ -468,11 +740,16 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
onKeyDown={handleKeyDown}
placeholder="Chat with your Supermemory"
className="w-full text-white placeholder:text-white/20 rounded-sm outline-none resize-none text-base leading-relaxed bg-transparent px-2 h-10"
+ disabled={status === "submitted" || status === "streaming"}
/>
<div className="flex justify-end absolute bottom-3 right-2">
<Button
type="submit"
- disabled={!message.trim()}
+ disabled={
+ !message.trim() ||
+ status === "submitted" ||
+ status === "streaming"
+ }
className="text-white/20 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed rounded-xl transition-all"
size="icon"
>
diff --git a/apps/web/components/new/onboarding/setup/integrations-step.tsx b/apps/web/components/new/onboarding/setup/integrations-step.tsx
index 49e3062a..8f83bdfe 100644
--- a/apps/web/components/new/onboarding/setup/integrations-step.tsx
+++ b/apps/web/components/new/onboarding/setup/integrations-step.tsx
@@ -165,7 +165,7 @@ export function IntegrationsStep() {
variant="link"
className="text-white hover:text-gray-300 hover:no-underline cursor-pointer"
onClick={() =>
- router.push("/new/onboarding?flow=setup&step=relatable")
+ router.push("/new/onboarding/setup?step=relatable")
}
>
← Back
diff --git a/apps/web/components/new/onboarding/setup/relatable-question.tsx b/apps/web/components/new/onboarding/setup/relatable-question.tsx
index 5fbe9bb4..5edb4a99 100644
--- a/apps/web/components/new/onboarding/setup/relatable-question.tsx
+++ b/apps/web/components/new/onboarding/setup/relatable-question.tsx
@@ -35,7 +35,7 @@ export function RelatableQuestion() {
const [selectedOptions, setSelectedOptions] = useState<number[]>([])
const handleContinueOrSkip = () => {
- router.push("/new/onboarding?flow=setup&step=integrations")
+ router.push("/new/onboarding/setup?step=integrations")
}
return (
diff --git a/apps/web/components/new/onboarding/welcome/continue-step.tsx b/apps/web/components/new/onboarding/welcome/continue-step.tsx
index 86b4593a..7d56d30e 100644
--- a/apps/web/components/new/onboarding/welcome/continue-step.tsx
+++ b/apps/web/components/new/onboarding/welcome/continue-step.tsx
@@ -1,45 +1,195 @@
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
-import { motion } from "motion/react"
+import { motion, type Variants } from "motion/react"
import { useRouter } from "next/navigation"
+import { ProfileStep } from "./profile-step"
+import { continueVariants, contentVariants } from "@/lib/variants"
-export function ContinueStep() {
+type OnboardingView = "continue" | "features" | "memories"
+
+interface OnboardingContentStepProps {
+ currentView?: OnboardingView
+ onSubmit?: (data: {
+ twitter: string
+ linkedin: string
+ description: string
+ otherLinks: string[]
+ }) => void
+}
+
+const containerVariants: Variants = {
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.4,
+ ease: "easeOut",
+ },
+ },
+ hidden: {
+ opacity: 0,
+ transition: {
+ duration: 0,
+ },
+ },
+}
+
+export function OnboardingContentStep({
+ currentView = "continue",
+ onSubmit,
+}: OnboardingContentStepProps) {
const router = useRouter()
const handleContinue = () => {
- router.push("/new/onboarding?flow=welcome&step=features")
+ router.push("/new/onboarding/welcome?step=features")
}
+ const handleAddMemories = () => {
+ router.push("/new/onboarding/welcome?step=memories")
+ }
+
+ const isContinue = currentView === "continue"
+ const isFeatures = currentView === "features"
+ const isMemories = currentView === "memories"
+
return (
<motion.div
- className="text-center"
- initial={{ opacity: 0, y: 0, scale: 0.9 }}
- animate={{ opacity: 1, y: 0, scale: 1 }}
- exit={{ opacity: 0, y: 0, scale: 0.9 }}
- transition={{ duration: 1, ease: "easeOut" }}
- layout
+ variants={containerVariants}
+ initial="hidden"
+ animate="visible"
+ exit="hidden"
+ className="text-center relative"
>
- <p
+ {/* Continue content */}
+ <motion.div
+ variants={continueVariants}
+ animate={isContinue ? "visible" : "hidden"}
+ initial="visible"
+ className={cn(
+ "flex flex-col items-center justify-center max-w-88",
+ !isContinue && "absolute inset-0 pointer-events-none",
+ )}
+ >
+ <p
+ className={cn(
+ "text-[#8A8A8A] text-sm mb-6 max-w-sm",
+ dmSansClassName(),
+ )}
+ >
+ I'm built with Supermemory's super fast memory API,
+ <br /> so you never have to worry about forgetting <br /> what matters
+ across your AI apps.
+ </p>
+ <Button
+ variant="onboarding"
+ onClick={handleContinue}
+ style={{
+ background: "linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
+ width: "147px",
+ }}
+ >
+ Continue →
+ </Button>
+ </motion.div>
+
+ {/* Features content */}
+ <motion.div
+ variants={contentVariants}
+ animate={isFeatures ? "visible" : "hiddenDown"}
+ initial="hiddenDown"
className={cn(
- "text-[#8A8A8A] text-sm mb-6 max-w-sm",
- dmSansClassName(),
+ "space-y-6 max-w-88",
+ !isFeatures && "absolute inset-0 pointer-events-none",
)}
>
- I'm built with Supermemory's super fast memory API,
- <br /> so you never have to worry about forgetting <br /> what matters
- across your AI apps.
- </p>
- <Button
- variant="onboarding"
- onClick={handleContinue}
- style={{
- background: "linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
- width: "147px",
- }}
+ <h2 className="text-white text-[32px] font-medium leading-[110%]">
+ What I can do for you
+ </h2>
+
+ <div className={cn("space-y-4 mb-[24px] mx-4", dmSansClassName())}>
+ <div className="flex items-start space-x-2">
+ <div className="w-14 h-14 rounded-lg flex items-center justify-center shrink-0">
+ <img
+ src="/onboarding/human-brain.png"
+ alt="Brain icon"
+ className="w-14 h-14"
+ />
+ </div>
+ <div className="text-left">
+ <p className="text-white font-light">Remember every context</p>
+ <p className="text-[#8A8A8A] text-[14px]">
+ I keep track of what you've saved and shared with your
+ supermemory.
+ </p>
+ </div>
+ </div>
+
+ <div className="flex items-start space-x-2">
+ <div className="w-14 h-14 rounded-lg flex items-center justify-center shrink-0">
+ <img
+ src="/onboarding/search.png"
+ alt="Search icon"
+ className="w-14 h-14"
+ />
+ </div>
+ <div className="text-left">
+ <p className="text-white font-light">Find when you need it</p>
+ <p className="text-[#8A8A8A] text-[14px]">
+ I surface the right memories inside <br /> your supermemory,
+ superfast.
+ </p>
+ </div>
+ </div>
+
+ <div className="flex items-start space-x-2">
+ <div className="w-14 h-14 rounded-lg flex items-center justify-center shrink-0">
+ <img
+ src="/onboarding/plant.png"
+ alt="Growth icon"
+ className="w-14 h-14"
+ />
+ </div>
+ <div className="text-left">
+ <p className="text-white font-light">
+ Grow with your supermemory
+ </p>
+ <p className="text-[#8A8A8A] text-[14px]">
+ I learn and personalize over time, so every interaction feels
+ natural.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <Button
+ variant="onboarding"
+ style={{
+ background: "linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
+ }}
+ onClick={handleAddMemories}
+ >
+ Add memories →
+ </Button>
+ </motion.div>
+
+ {/* Memories/Profile content */}
+ <div
+ className={cn(
+ "w-full",
+ !isMemories && "absolute inset-0 pointer-events-none",
+ )}
>
- Continue →
- </Button>
+ {onSubmit && (
+ <motion.div
+ variants={contentVariants}
+ animate={isMemories ? "visible" : "hiddenDown"}
+ initial="hiddenDown"
+ >
+ <ProfileStep onSubmit={onSubmit} />
+ </motion.div>
+ )}
+ </div>
</motion.div>
)
}
diff --git a/apps/web/components/new/onboarding/welcome/features-step.tsx b/apps/web/components/new/onboarding/welcome/features-step.tsx
deleted file mode 100644
index 2671efc1..00000000
--- a/apps/web/components/new/onboarding/welcome/features-step.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { motion } from "motion/react"
-import { Button } from "@ui/components/button"
-import { useRouter } from "next/navigation"
-import { cn } from "@lib/utils"
-import { dmSansClassName } from "@/lib/fonts"
-
-export function FeaturesStep() {
- const router = useRouter()
-
- const handleContinue = () => {
- router.push("/new/onboarding?flow=welcome&step=memories")
- }
- return (
- <motion.div
- initial={{ opacity: 0, y: 40 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ duration: 0.6, ease: "easeOut", delay: 0.3 }}
- className="text-center max-w-88 space-y-6"
- layout
- >
- <h2 className="text-white text-[32px] font-medium leading-[110%]">
- What I can do for you
- </h2>
-
- <div className={cn("space-y-4 mb-[24px] mx-4", dmSansClassName())}>
- <div className="flex items-start space-x-2">
- <div className="w-14 h-14 rounded-lg flex items-center justify-center shrink-0">
- <img
- src="/onboarding/human-brain.png"
- alt="Brain icon"
- className="w-14 h-14"
- />
- </div>
- <div className="text-left">
- <p className="text-white font-light">Remember every context</p>
- <p className="text-[#8A8A8A] text-[14px]">
- I keep track of what you've saved and shared with your
- supermemory.
- </p>
- </div>
- </div>
-
- <div className="flex items-start space-x-2">
- <div className="w-14 h-14 rounded-lg flex items-center justify-center shrink-0">
- <img
- src="/onboarding/search.png"
- alt="Search icon"
- className="w-14 h-14"
- />
- </div>
- <div className="text-left">
- <p className="text-white font-light">Find when you need it</p>
- <p className="text-[#8A8A8A] text-[14px]">
- I surface the right memories inside <br /> your supermemory,
- superfast.
- </p>
- </div>
- </div>
-
- <div className="flex items-start space-x-2">
- <div className="w-14 h-14 rounded-lg flex items-center justify-center shrink-0">
- <img
- src="/onboarding/plant.png"
- alt="Growth icon"
- className="w-14 h-14"
- />
- </div>
- <div className="text-left">
- <p className="text-white font-light">Grow with your supermemory</p>
- <p className="text-[#8A8A8A] text-[14px]">
- I learn and personalize over time, so every interaction feels
- natural.
- </p>
- </div>
- </div>
- </div>
-
- <motion.div
- animate={{
- opacity: 1,
- y: 0,
- }}
- transition={{ duration: 1, ease: "easeOut", delay: 1 }}
- initial={{ opacity: 0, y: 10 }}
- >
- <Button
- variant="onboarding"
- style={{
- background: "linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
- }}
- onClick={handleContinue}
- >
- Add memories →
- </Button>
- </motion.div>
- </motion.div>
- )
-}
diff --git a/apps/web/components/new/onboarding/welcome/profile-step.tsx b/apps/web/components/new/onboarding/welcome/profile-step.tsx
index 65eb21c6..34393dad 100644
--- a/apps/web/components/new/onboarding/welcome/profile-step.tsx
+++ b/apps/web/components/new/onboarding/welcome/profile-step.tsx
@@ -292,7 +292,7 @@ export function ProfileStep({ onSubmit }: ProfileStepProps) {
otherLinks: otherLinks.filter((l) => l.trim()),
}
onSubmit(formData)
- router.push("/new/onboarding?flow=setup&step=relatable")
+ router.push("/new/onboarding/setup?step=relatable")
}}
>
{isSubmitting ? "Fetching..." : "Remember this →"}
diff --git a/apps/web/components/views/chat/chat-messages.tsx b/apps/web/components/views/chat/chat-messages.tsx
index 55940a60..3e55dc23 100644
--- a/apps/web/components/views/chat/chat-messages.tsx
+++ b/apps/web/components/views/chat/chat-messages.tsx
@@ -20,7 +20,7 @@ import { Streamdown } from "streamdown"
import { TextShimmer } from "@/components/text-shimmer"
import { usePersistentChat, useProject } from "@/stores"
import { useGraphHighlights } from "@/stores/highlights"
-import { modelNames, ModelIcon } from "@/lib/models"
+import { ModelIcon } from "@/lib/models"
import { Spinner } from "../../spinner"
import { areUIMessageArraysEqual } from "@/stores/chat"
@@ -270,7 +270,6 @@ export function ChatMessages() {
},
},
}),
- maxSteps: 10,
onFinish: (result) => {
const activeId = activeChatIdRef.current
if (!activeId) return