aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src/components/ChatMessage.tsx
blob: 0ab22271db91c06443a60605742f0fda1b2a85eb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import React, { useEffect } from "react";
import { motion } from "framer-motion";
import { ArrowUpRight, Globe, Text } from "lucide-react";
import { convertRemToPixels } from "@/lib/utils";
import { SpaceIcon } from "@/assets/Memories";
import Markdown from "react-markdown";
import { ChatHistory } from "../../types/memory";

export function ChatAnswer({
  children: message,
  sources,
  loading = false,
}: {
  children: string;
  sources?: ChatHistory['answer']['sources'];
  loading?: boolean;
}) {
  return (
    <div className="flex h-max w-full flex-col items-start gap-5">
      {loading ? (
        <MessageSkeleton />
      ) : (
        <div className="chat-answer h-full w-full text-lg text-white/60">
          <Markdown>{message}</Markdown>
        </div>
      )}
      {!loading && sources && sources?.length > 0 && (
        <>
          <h1 className="animate-fade-in text-rgray-12 text-md flex items-center justify-center gap-2 opacity-0 [animation-duration:1s]">
            <SpaceIcon className="h-6 w-6 -translate-y-[2px]" />
            Related Memories
          </h1>
          <div className="animate-fade-in gap-1 -mt-3 flex items-center justify-start opacity-0 [animation-duration:1s]">
            {sources?.map((source) => source.isNote ? (
							<button
                className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm"
							>
								<Text className="w-4 h-4" />
								{source.source}
							</button>
						) : (
              <a
                className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm"
                key={source.source}
                href={source.source}
								target="_blank"
              >
                <Globe className="h-4 w-4" />
                {cleanUrl(source.source)}
              </a>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

export function ChatQuestion({ children }: { children: string }) {
  return (
    <div
      className={`text-rgray-12 h-max w-full text-left ${children.length > 200 ? "text-xl" : "text-2xl"}`}
    >
      {children}
    </div>
  );
}

export function ChatMessage({
  children,
  isLast = false,
  index,
}: {
  children: React.ReactNode | React.ReactNode[];
  isLast?: boolean;
  index: number;
}) {
  const messageRef = React.useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!isLast) return;
    messageRef.current?.parentElement?.scrollTo({
      top: messageRef.current?.offsetTop,
      behavior: "smooth",
    });
  }, []);

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{
        type: "tween",
        duration: 0.5,
      }}
      ref={messageRef}
      className={`${index === 0 ? "pt-16" : "pt-28"} flex h-max w-full resize-y flex-col items-start justify-start gap-5 transition-[height] ${isLast ? "min-h-screen pb-[40vh]" : "h-max"}`}
    >
      {children}
    </motion.div>
  );
}

function MessageSkeleton() {
  return (
    <div className="animate-fade-in flex w-full flex-col items-start gap-3 opacity-0 [animation-delay:0.5s] [animation-duration:1s]">
      <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div>
      <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div>
      <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div>
      <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div>
      <div className="bg-rgray-5 h-6 w-[70%] animate-pulse rounded-md text-lg"></div>
    </div>
  );
}

function cleanUrl(url: string) {
  if (url.startsWith("https://")) {
    url = url.slice(8);
  } else if (url.startsWith("http://")) {
    url = url.slice(7);
  }

  if (url.endsWith("/")) {
    url = url.slice(0, -1);
  }

  return url;
}