Skip to content

Commit e9df00f

Browse files
authored
feat(chat): ai chat with workspace (#91)
* initial cut for chat * add formatting and allow chatId to be present in the path * Add chat, page refresh on it, reditect if invalid id and bookmarks * rebase with main * remove type errors * make the sidebar in search fixed and improve border for search results * Improve styles of search and make searchbar component * add search query params and use it to get search results * remove type errors
1 parent 00e6320 commit e9df00f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4914
-316
lines changed

frontend/src/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { hc } from "hono/client"
22
import type { WebSocketApp, AppType } from "shared/types"
3-
export const api = hc<AppType>("/")
3+
export const api = hc<AppType>("/api/v1")
44

55
export const wsClient = hc<WebSocketApp>(
66
import.meta.env.VITE_WS_BASE_URL || "http://localhost:3000",
Lines changed: 4 additions & 0 deletions
Loading

frontend/src/assets/retry.svg

Lines changed: 6 additions & 0 deletions
Loading

frontend/src/components/GroupFilter.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Apps, DriveEntity, Entity, GooglePeopleEntity } from "shared/types"
22
import { Filter, Groups } from "@/types"
33
import { getIcon } from "@/lib/common"
44
import allFilter from "@/assets/allFilter.svg"
5+
import { humanizeNumbers } from "@/lib/utils"
56

67
const flattenGroups = (groups: Groups) => {
78
return Object.keys(groups || {}).flatMap((app) =>
@@ -21,6 +22,7 @@ const GroupFilterItem = ({
2122
isFilter,
2223
Image,
2324
className,
25+
index,
2426
}: {
2527
title: string
2628
onClick: any
@@ -29,9 +31,10 @@ const GroupFilterItem = ({
2931
isFilter: any
3032
Image: JSX.Element
3133
className?: string
34+
index: number
3235
}) => {
3336
return (
34-
<div className={`rounded-md h-[32px] ml-[40px] ${className}`}>
37+
<div className={`rounded-md h-[32px] ml-[40px] ${className}`} key={index}>
3538
<div
3639
onClick={onClick}
3740
className={`${isFilter(filter) ? "bg-[#F0F4F7]" : ""} flex flex-row rounded-[6px] items-center justify-between cursor-pointer pl-[12px] pr-[12px] pt-[4px] pb-[4px] w-[248px]`}
@@ -88,8 +91,8 @@ export const GroupFilter = ({
8891
total: number
8992
}) => {
9093
return (
91-
<div className="flex border-l-[1px] border-[#E6EBF5] flex-col">
92-
<p className="text-[11.5px] font-medium text-[#97A6C4] ml-[40px] mt-[28px]">{`FOUND ${total} RESULTS`}</p>
94+
<div className="flex flex-col">
95+
<p className="text-[11.5px] font-medium text-[#97A6C4] ml-[40px] mt-[28px] tracking-[0.08em]">{`FOUND ${humanizeNumbers(total)} RESULTS`}</p>
9396
<GroupFilterItem
9497
className={"mt-4"}
9598
title={"All"}
@@ -100,10 +103,12 @@ export const GroupFilter = ({
100103
}}
101104
total={total!}
102105
Image={<img src={allFilter} className="mr-[10px]" />}
106+
index={0}
103107
/>
104108
{flattenGroups(groups).map(({ app, entity, count }, index) => {
105109
return (
106110
<GroupFilterItem
111+
index={index}
107112
title={getName(app, entity)}
108113
filter={filter}
109114
isFilter={(filter: Filter | null) =>

frontend/src/components/Highlight.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const parseHighlight = (text: string): ReactNode[] => {
7070

7171
// Component that renders chunk summary with parsing
7272
const HighlightedText = ({ chunk_summary }: { chunk_summary: string }) => (
73-
<p className="text-left text-sm mt-1 text-[#464B53] line-clamp-[2.5] text-ellipsis overflow-hidden">
73+
<p className="text-left text-sm mt-1 text-[#464B53] line-clamp-[2.5] text-ellipsis overflow-hidden ml-[44px]">
7474
{chunk_summary ? parseHighlight(chunk_summary) : " "}
7575
</p>
7676
)
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { Search } from "lucide-react"
2+
import React, { useRef, useEffect } from "react"
3+
import { SearchFilters } from "./SearchFilter"
4+
import { ArrowRight, X } from "lucide-react" // Assuming ArrowRight and X are imported from lucide-react
5+
import { AutocompleteElement } from "@/components/Autocomplete"
6+
import { Button } from "@/components/ui/button"
7+
import { useNavigate } from "@tanstack/react-router"
8+
9+
export const SearchBar = React.forwardRef<HTMLDivElement, any>(
10+
(
11+
{
12+
hasSearched,
13+
autocompleteResults,
14+
setQuery,
15+
setAutocompleteResults,
16+
setAutocompleteQuery,
17+
setOffset,
18+
setFilter,
19+
query,
20+
handleSearch,
21+
handleAnswer,
22+
},
23+
autocompleteRef,
24+
) => {
25+
const inputRef = useRef<HTMLInputElement | null>(null)
26+
const navigate = useNavigate({ from: "/search" })
27+
28+
useEffect(() => {
29+
if (inputRef.current) {
30+
inputRef.current.focus()
31+
}
32+
}, [])
33+
34+
return (
35+
<div
36+
className={`flex flex-col bg-white ${
37+
hasSearched
38+
? "pt-[12px] border-b-[1px] border-b-[#E6EBF5] justify-center sticky top-0 z-10"
39+
: "items-center"
40+
}`}
41+
>
42+
<div
43+
className={`flex flex-col max-w-3xl ${
44+
hasSearched ? "ml-[186px]" : ""
45+
} w-full`}
46+
>
47+
<div className="flex w-full">
48+
<div className="relative w-full">
49+
<div
50+
className={`flex w-full items-center ${
51+
hasSearched ? "bg-[#F0F4F7]" : "bg-white"
52+
} ${
53+
autocompleteResults.length > 0
54+
? "rounded-t-lg border-b-0"
55+
: "rounded-full"
56+
} ${
57+
hasSearched ? "" : "border border-[#AEBAD3]"
58+
} h-[52px] shadow-sm`}
59+
>
60+
<Search className="text-[#AEBAD3] ml-4 mr-2" size={18} />
61+
<input
62+
ref={inputRef}
63+
placeholder="Search anything across connected apps..."
64+
value={query}
65+
onChange={(e) => {
66+
setQuery(e.target.value)
67+
setAutocompleteQuery(e.target.value)
68+
setOffset(0)
69+
}}
70+
className={`text-[#1C1D1F] w-full text-[15px] focus-visible:ring-0 placeholder-[#BDC6D8] font-[450] leading-[24px] focus:outline-none ${
71+
hasSearched ? "bg-[#F0F4F7]" : ""
72+
}`}
73+
onKeyDown={(e) => {
74+
if (e.key === "Enter") {
75+
navigate({
76+
to: "/search",
77+
search: { query: encodeURIComponent(query) },
78+
})
79+
setFilter(null)
80+
handleSearch(0, null)
81+
// we only want to look for answer if at least
82+
// 3 words are there in the query
83+
if (query.split(" ").length > 2) {
84+
handleAnswer()
85+
}
86+
}
87+
}}
88+
/>
89+
{!hasSearched ? (
90+
<Button
91+
onClick={(e) => handleSearch()}
92+
className="mr-2 bg-[#464B53] text-white p-2 hover:bg-[#5a5f66] rounded-full"
93+
>
94+
<ArrowRight className="text-white" size={20} />
95+
</Button>
96+
) : (
97+
<X
98+
className="text-[#ACB8D1] cursor-pointer mr-[16px]"
99+
size={20}
100+
onClick={(e) => {
101+
setQuery("")
102+
inputRef.current?.focus()
103+
}}
104+
/>
105+
)}
106+
{!!autocompleteResults?.length && (
107+
<div
108+
ref={autocompleteRef}
109+
className="absolute top-full w-full left-0 bg-white rounded-b-lg border border-t-0 border-[#AEBAD3] shadow-md"
110+
>
111+
{autocompleteResults.map((result: any, index: number) => (
112+
<AutocompleteElement
113+
key={index}
114+
onClick={() => {
115+
if (result.type === "file") {
116+
setQuery(result.title)
117+
}
118+
setAutocompleteResults([])
119+
}}
120+
result={result}
121+
/>
122+
))}
123+
</div>
124+
)}
125+
</div>
126+
</div>
127+
</div>
128+
</div>
129+
{/* search filters */}
130+
{hasSearched && (
131+
<div className="ml-[230px] text-[13px]">
132+
<SearchFilters />
133+
</div>
134+
)}
135+
</div>
136+
)
137+
},
138+
)

frontend/src/components/SearchResult.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ export const SearchResult = ({
77
index,
88
}: { result: SearchResultDiscriminatedUnion; index: number }) => {
99
let content = <></>
10+
let commonClassVals = "pr-[60px]"
1011
if (result.type === "file") {
1112
content = (
12-
<div className="flex flex-col mt-[28px]" key={index}>
13+
<div className={`flex flex-col mt-[28px] ${commonClassVals}`} key={index}>
1314
<div className="flex items-center justify-start space-x-2">
1415
<a
1516
href={result.url ?? ""}
@@ -21,7 +22,7 @@ export const SearchResult = ({
2122
{result.title}
2223
</a>
2324
</div>
24-
<div className="flex flex-row items-center mt-1">
25+
<div className="flex flex-row items-center mt-1 ml-[44px]">
2526
<img
2627
referrerPolicy="no-referrer"
2728
className="mr-2 w-[16px] h-[16px] rounded-full"
@@ -47,13 +48,13 @@ export const SearchResult = ({
4748
)
4849
} else if (result.type === "user") {
4950
content = (
50-
<div className="flex flex-col mt-[28px]" key={index}>
51-
<div className="flex items-center justify-start space-x-2">
51+
<div className={`flex flex-col mt-[28px] ${commonClassVals}`} key={index}>
52+
<div className="flex items-center justify-start">
5253
<a
5354
href={`https://contacts.google.com/${result.email}`}
5455
target="_blank"
5556
rel="noopener noreferrer"
56-
className="flex items-center text-[#2067F5] space-x-2"
57+
className="flex items-center text-[#2067F5]"
5758
>
5859
{/* TODO: if photoLink doesn't exist then show icon */}
5960
<img
@@ -68,14 +69,14 @@ export const SearchResult = ({
6869
)
6970
} else if (result.type === "mail") {
7071
content = (
71-
<div className="flex flex-col mt-[28px]" key={index}>
72+
<div className={`flex flex-col mt-[28px] ${commonClassVals}`} key={index}>
7273
<div className="flex items-center justify-start">
7374
{getIcon(result.app, result.entity, { w: 24, h: 24, mr: 20 })}
7475
<a
7576
href={`https://mail.google.com/mail/u/0/#inbox/${result.docId}`}
7677
target="_blank"
7778
rel="noopener noreferrer"
78-
className="flex items-center text-[#2067F5] space-x-2"
79+
className="flex items-center text-[#2067F5]"
7980
>
8081
{/* TODO: if photoLink doesn't exist then show icon */}
8182
{/* <img

frontend/src/components/Sidebar.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { Link, useLocation } from "@tanstack/react-router"
2-
import { Home, Search, Settings, Bell, User, Plug } from "lucide-react"
2+
import {
3+
Home,
4+
Search,
5+
Settings,
6+
Bell,
7+
User,
8+
Plug,
9+
MessageSquarePlus,
10+
} from "lucide-react"
311

412
export const Sidebar = ({ className = "" }: { className?: string }) => {
513
const location = useLocation()
614
return (
715
<div
8-
className={`h-full w-[52px] bg-gray-100 p-4 flex flex-col items-center space-y-6 ${className}`}
16+
className={`h-full w-[52px] bg-gray-100 p-4 flex flex-col items-center space-y-6 fixed ${className} z-20`}
917
>
1018
<Link to="/">
1119
<Home
@@ -23,6 +31,14 @@ export const Sidebar = ({ className = "" }: { className?: string }) => {
2331
: { className: "hover:text-blue-600" })}
2432
/>
2533
</Link>
34+
<Link to="/chat">
35+
<MessageSquarePlus
36+
size={18}
37+
{...(location.pathname === "/_authenticated/chat"
38+
? { className: "text-blue-500 hover:text-blue-600" }
39+
: { className: "hover:text-blue-600" })}
40+
/>
41+
</Link>
2642
{
2743
// @ts-ignore
2844
<Link to="/notifications" className="hover:text-blue-500">

frontend/src/lib/common.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ import {
1616
export const getIcon = (
1717
app: Apps,
1818
entity: Entity,
19-
size?: { w: number; h: number; mr: number },
19+
size?: { w: number; h: number; mr: number; ml?: number },
2020
) => {
2121
let classNameVal = ""
2222
if (size) {
2323
classNameVal = `h-[${size.h}px] w-[${size.w}px] mr-[${size.mr}px]`
24+
if (size.ml) {
25+
classNameVal += ` ml-[${size.ml}px]`
26+
}
2427
} else {
2528
classNameVal = "h-[12px] w-[12px] mr-[10px]"
2629
}

frontend/src/lib/utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,20 @@ export const replaceLinks = (text: string): string => {
3434
}
3535
})
3636
}
37+
38+
export const humanizeNumbers = (num: number): string => {
39+
if (num < 1000) return num.toString()
40+
41+
const units = ["k", "M", "B", "T"]
42+
const exponent = Math.floor(Math.log10(num) / 3)
43+
const unit = units[exponent - 1]
44+
const scaledNum = num / Math.pow(1000, exponent)
45+
46+
// Use Intl.NumberFormat to format the number
47+
const formatter = new Intl.NumberFormat("en-US", {
48+
minimumFractionDigits: 0,
49+
maximumFractionDigits: scaledNum < 10 ? 1 : 0,
50+
})
51+
52+
return `${formatter.format(scaledNum)}${unit}`
53+
}

0 commit comments

Comments
 (0)