Skip to content

Commit 9673e17

Browse files
committed
chat search -- quick proof of concept
1 parent 0e90dc1 commit 9673e17

File tree

8 files changed

+278
-217
lines changed

8 files changed

+278
-217
lines changed

src/packages/frontend/chat/filter-messages.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function filterMessages({
3030
// no filters -- typical special case; waste now time.
3131
return messages;
3232
}
33-
const searchData = getSearchData(messages);
33+
const searchData = getSearchData({ messages, threads: true });
3434
let matchingRootTimes: Set<string>;
3535
if (filter) {
3636
matchingRootTimes = new Set<string>();
@@ -107,23 +107,37 @@ type SearchData = {
107107

108108
const cache = new LRU<ChatMessages, SearchData>({ max: 25 });
109109

110-
function getSearchData(messages): SearchData {
110+
export function getSearchData({
111+
messages,
112+
threads,
113+
}: {
114+
messages: ChatMessages;
115+
threads: boolean;
116+
}): SearchData {
111117
if (cache.has(messages)) {
112118
return cache.get(messages)!;
113119
}
114120
const data: SearchData = {};
115121
const userMap = redux.getStore("users").get("user_map");
116-
for (const [time, message] of messages) {
122+
for (let [time, message] of messages) {
123+
if (typeof time != "string") {
124+
// for typescript
125+
time = `${time}`;
126+
}
127+
const messageTime = parseFloat(time);
128+
const content = getContent(message, userMap);
129+
if (!threads) {
130+
data[time] = { content, newestTime: messageTime };
131+
continue;
132+
}
117133
let rootTime: string;
118134
if (message.get("reply_to")) {
119135
// non-root in thread
120-
rootTime = `${new Date(message.get("reply_to")).valueOf()}`;
136+
rootTime = `${new Date(message.get("reply_to")!).valueOf()}`;
121137
} else {
122138
// new root thread
123139
rootTime = time;
124140
}
125-
const messageTime = parseFloat(time);
126-
const content = getContent(message, userMap);
127141
if (data[rootTime] == null) {
128142
data[rootTime] = {
129143
content,

src/packages/frontend/frame-editors/chat-editor/editor.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { createEditor } from "@cocalc/frontend/frame-editors/frame-tree/editor";
1414
import type { EditorDescription } from "@cocalc/frontend/frame-editors/frame-tree/types";
1515
import { terminal } from "@cocalc/frontend/frame-editors/terminal-editor/editor";
1616
import { time_travel } from "@cocalc/frontend/frame-editors/time-travel-editor/editor";
17+
import { search } from "./search";
1718

1819
const chatroom: EditorDescription = {
1920
type: "chatroom",
@@ -39,6 +40,7 @@ const chatroom: EditorDescription = {
3940
"chatgpt",
4041
"scrollToBottom",
4142
"scrollToTop",
43+
"show_search",
4244
]),
4345
customizeCommands: {
4446
scrollToTop: {
@@ -59,13 +61,15 @@ const chatroom: EditorDescription = {
5961
"increase_font_size",
6062
"scrollToTop",
6163
"scrollToBottom",
64+
"show_search",
6265
]),
6366
} as const;
6467

6568
const EDITOR_SPEC = {
6669
chatroom,
6770
terminal,
6871
time_travel,
72+
search,
6973
} as const;
7074

7175
export const Editor = createEditor({
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3+
* License: MS-RSL – see LICENSE.md for details
4+
*/
5+
6+
/*
7+
Full text search that is better than a simple filter.
8+
*/
9+
10+
import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context";
11+
import type { EditorDescription } from "@cocalc/frontend/frame-editors/frame-tree/types";
12+
import { Button, Card, Input } from "antd";
13+
import { set } from "@cocalc/util/misc";
14+
import { useEffect, useMemo, useState } from "react";
15+
import { throttle } from "lodash";
16+
import useSearchIndex from "./use-search-index";
17+
import ShowError from "@cocalc/frontend/components/error";
18+
19+
interface Props {
20+
font_size: number;
21+
desc;
22+
}
23+
24+
function Search({ font_size, desc }: Props) {
25+
const { project_id, path, actions, id } = useFrameContext();
26+
const [search, setSearch] = useState<string>(desc.get("data-search") ?? "");
27+
const [result, setResult] = useState<any>(null);
28+
const saveSearch = useMemo(
29+
() =>
30+
throttle((search) => {
31+
if (!actions.isClosed()) {
32+
actions.set_frame_data({ id, search });
33+
}
34+
}, 250),
35+
[project_id, path],
36+
);
37+
38+
const { error, setError, index, doRefresh } = useSearchIndex();
39+
40+
useEffect(() => {
41+
if (index == null) {
42+
return;
43+
}
44+
if (!search.trim()) {
45+
setResult([]);
46+
return;
47+
}
48+
(async () => {
49+
const result = await index.search({ term: search });
50+
setResult(result);
51+
})();
52+
}, [search, index]);
53+
54+
return (
55+
<div className="smc-vfill">
56+
<Card
57+
title={
58+
<>
59+
Search {path}
60+
<Button
61+
onClick={() => {
62+
doRefresh();
63+
}}
64+
style={{ float: "right" }}
65+
>
66+
Refresh
67+
</Button>
68+
</>
69+
}
70+
style={{ fontSize: font_size }}
71+
>
72+
<ShowError error={error} setError={setError} />
73+
<Input.Search
74+
allowClear
75+
placeholder="Search for messages..."
76+
value={search}
77+
onChange={(e) => {
78+
const search = e.target.value ?? "";
79+
setSearch(search);
80+
saveSearch(search);
81+
}}
82+
/>
83+
</Card>
84+
<pre className="smc-vfill" style={{ overflow: "auto" }}>
85+
{JSON.stringify(result, undefined, 2)}
86+
</pre>
87+
</div>
88+
);
89+
}
90+
91+
export const search = {
92+
type: "search",
93+
short: "Search",
94+
name: "Search",
95+
icon: "comment",
96+
commands: set(["decrease_font_size", "increase_font_size"]),
97+
component: Search,
98+
} as EditorDescription;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context";
2+
import { useEffect, useState } from "react";
3+
import { create, search, insertMultiple } from "@orama/orama";
4+
import { getSearchData } from "@cocalc/frontend/chat/filter-messages";
5+
import useCounter from "@cocalc/frontend/app-framework/counter-hook";
6+
7+
export default function useSearchIndex() {
8+
const { actions, project_id, path } = useFrameContext();
9+
const [index, setIndex] = useState<null | SearchIndex>(null);
10+
const [error, setError] = useState<string>("");
11+
const { val: refresh, inc: doRefresh } = useCounter();
12+
13+
useEffect(() => {
14+
(async () => {
15+
try {
16+
setError("");
17+
const index = new SearchIndex({ actions });
18+
await index.init();
19+
setIndex(index);
20+
} catch (err) {
21+
setError(`${err}`);
22+
}
23+
})();
24+
}, [project_id, path, refresh]);
25+
26+
return { index, error, doRefresh, setError };
27+
}
28+
29+
class SearchIndex {
30+
private actions;
31+
private state: "init" | "ready" | "failed" = "init";
32+
private error: Error | null = null;
33+
private db;
34+
35+
constructor({ actions }) {
36+
this.actions = actions;
37+
}
38+
39+
getState = () => this.state;
40+
getError = () => this.error;
41+
42+
search = async (query) => {
43+
if (this.state != "ready") {
44+
throw Error("index not ready");
45+
}
46+
return await search(this.db, query);
47+
};
48+
49+
init = async () => {
50+
this.db = await create({
51+
schema: {
52+
time: "number",
53+
message: "string",
54+
},
55+
});
56+
57+
const messages = this.actions.store?.get("messages");
58+
if (messages == null) {
59+
return;
60+
}
61+
const searchData = getSearchData({ messages, threads: false });
62+
const docs: { time: number; message: string }[] = [];
63+
for (const time in searchData) {
64+
docs.push({
65+
time: parseInt(time),
66+
message: searchData[time]?.content ?? "",
67+
});
68+
}
69+
await insertMultiple(this.db, docs);
70+
this.state = "ready";
71+
};
72+
}

0 commit comments

Comments
 (0)