From c87ef5b67447e0e3672d272e9a55be89c8a3ca4a Mon Sep 17 00:00:00 2001
From: Sean Hatfield <seanhatfield5@gmail.com>
Date: Thu, 22 Feb 2024 13:16:20 -0800
Subject: [PATCH] [FEAT] show chunk score on citations (#773)

* show similarity score in citation chunks

* refactor combine like sources to handle separate similarity scores and improve UI for displaying chunks

* fix parseChunkSource function to work with links

* destructure properties in chunks mapping

* check chunk length in parseChunkSource

* change UI on how score is shown

* remove duplicate import

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
---
 .../ChatHistory/Citation/index.jsx            | 66 ++++++++++++++-----
 frontend/src/utils/numbers.js                 |  8 +++
 2 files changed, 58 insertions(+), 16 deletions(-)

diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx
index 85b7fb7bb..1dfeaaaf3 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx
@@ -1,31 +1,37 @@
 import { memo, useState } from "react";
 import { v4 } from "uuid";
 import { decode as HTMLDecode } from "he";
-import { CaretRight, FileText } from "@phosphor-icons/react";
 import truncate from "truncate";
 import ModalWrapper from "@/components/ModalWrapper";
 import { middleTruncate } from "@/utils/directories";
 import {
+  CaretRight,
+  FileText,
+  Info,
   ArrowSquareOut,
   GithubLogo,
   Link,
   X,
   YoutubeLogo,
 } from "@phosphor-icons/react";
+import { Tooltip } from "react-tooltip";
+import { toPercentString } from "@/utils/numbers";
 
 function combineLikeSources(sources) {
   const combined = {};
   sources.forEach((source) => {
-    const { id, title, text, chunkSource = "" } = source;
+    const { id, title, text, chunkSource = "", score = null } = source;
     if (combined.hasOwnProperty(title)) {
-      combined[title].text += `\n\n ---- Chunk ${id || ""} ---- \n\n${text}`;
+      combined[title].chunks.push({ id, text, chunkSource, score });
       combined[title].references += 1;
-      combined[title].chunkSource = chunkSource;
     } else {
-      combined[title] = { title, text, chunkSource, references: 1 };
+      combined[title] = {
+        title,
+        chunks: [{ id, text, chunkSource, score }],
+        references: 1,
+      };
     }
   });
-
   return Object.values(combined);
 }
 
@@ -109,7 +115,7 @@ function SkeletonLine() {
 }
 
 function CitationDetailModal({ source, onClose }) {
-  const { references, title, text } = source;
+  const { references, title, chunks } = source;
   const { isUrl, text: webpageUrl, href: linkTo } = parseChunkSource(source);
 
   return (
@@ -156,12 +162,39 @@ function CitationDetailModal({ source, onClose }) {
             {[...Array(3)].map((_, idx) => (
               <SkeletonLine key={idx} />
             ))}
-            <p className="text-white whitespace-pre-line">{HTMLDecode(text)}</p>
-            <div className="mb-6">
-              {[...Array(3)].map((_, idx) => (
-                <SkeletonLine key={idx} />
-              ))}
-            </div>
+            {chunks.map(({ text, score }, idx) => (
+              <div key={idx} className="pt-6 text-white">
+                <div className="flex flex-col w-full justify-start pb-6 gap-y-1">
+                  <p className="text-white whitespace-pre-line">
+                    {HTMLDecode(text)}
+                  </p>
+
+                  {!!score && (
+                    <>
+                      <div className="w-full flex items-center text-xs text-white/60 gap-x-2 cursor-default">
+                        <div
+                          data-tooltip-id="similarity-score"
+                          data-tooltip-content={`This is the semantic similarity score of this chunk of text compared to your query calculated by the vector database.`}
+                          className="flex items-center gap-x-1"
+                        >
+                          <Info size={14} />
+                          <p>{toPercentString(score)} match</p>
+                        </div>
+                      </div>
+                      <Tooltip
+                        id="similarity-score"
+                        place="top"
+                        delayShow={100}
+                      />
+                    </>
+                  )}
+                </div>
+                {[...Array(3)].map((_, idx) => (
+                  <SkeletonLine key={idx} />
+                ))}
+              </div>
+            ))}
+            <div className="mb-6"></div>
           </div>
         </div>
       </div>
@@ -180,7 +213,7 @@ const ICONS = {
 // which contain valid outbound links that can be clicked by the
 // user when viewing a citation. Optionally allows various icons
 // to show distinct types of sources.
-function parseChunkSource({ title = "", chunkSource = "" }) {
+function parseChunkSource({ title = "", chunks = [] }) {
   const nullResponse = {
     isUrl: false,
     text: null,
@@ -188,9 +221,10 @@ function parseChunkSource({ title = "", chunkSource = "" }) {
     icon: "file",
   };
 
-  if (!chunkSource.startsWith("link://")) return nullResponse;
+  if (!chunks.length || !chunks[0].chunkSource.startsWith("link://"))
+    return nullResponse;
   try {
-    const url = new URL(chunkSource.split("link://")[1]);
+    const url = new URL(chunks[0].chunkSource.split("link://")[1]);
     let text = url.host + url.pathname;
     let icon = "link";
 
diff --git a/frontend/src/utils/numbers.js b/frontend/src/utils/numbers.js
index bac17787c..0b4da3cbd 100644
--- a/frontend/src/utils/numbers.js
+++ b/frontend/src/utils/numbers.js
@@ -15,6 +15,14 @@ export function dollarFormat(input) {
   }).format(input);
 }
 
+export function toPercentString(input = null, decimals = 0) {
+  if (isNaN(input) || input === null) return "";
+  const percentage = Math.round(input * 100);
+  return (
+    (decimals > 0 ? percentage.toFixed(decimals) : percentage.toString()) + "%"
+  );
+}
+
 export function humanFileSize(bytes, si = false, dp = 1) {
   const thresh = si ? 1000 : 1024;