Skip to content

Commit bbb3c9a

Browse files
sid597maparent
andauthored
Roam: ENG-709: Settings panel for suggestive mode (#327)
* settings for context and page groups * node settings * address review * fix node creation if it does not exist * remove unused, refactor * address coderabbit review * unify spelling, handle fallback * sync * address coderabbit code * address lint errors * use async instead of backend * address review * address review * bug fixes * bug fixes * unused import * Add an input type for platform accounts Add functions to upsert platform accounts (individually or in bulk) Keep the old create_account_in_space function as a shim for now. Use the new upsert_account in upsert_documents and upsert_content, allowing for more complete inline information. * Add an input type for platform accounts Add functions to upsert platform accounts (individually or in bulk) Keep the old create_account_in_space function as a shim for now. Use the new upsert_account in upsert_documents and upsert_content, allowing for more complete inline information. * Add an input type for platform accounts Add functions to upsert platform accounts (individually or in bulk) Keep the old create_account_in_space function as a shim for now. Use the new upsert_account in upsert_documents and upsert_content, allowing for more complete inline information. * bulk upsert accounts, use database function imports * add comment for future * use better import method * commonjs-to-esm * fix merge errors, fix tree config setting extraction, test functionality, fix functionality * add progress toaster, use space existence condition for on load sync * defaultvalue * address review, leaving out update embeddings from here * update embeddings * ENG-818: Declare dbDotEnv as mjs explicitly * reverting create a new pr for update embeddings --------- Co-authored-by: Marc-Antoine Parent <[email protected]>
1 parent 0e381c6 commit bbb3c9a

13 files changed

+644
-52
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
import React, {
3+
useState,
4+
useMemo,
5+
useEffect,
6+
useRef,
7+
useCallback,
8+
} from "react";
9+
import { Button, Intent } from "@blueprintjs/core";
10+
import BlocksPanel from "roamjs-components/components/ConfigPanels/BlocksPanel";
11+
import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel";
12+
import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel";
13+
import getSubTree from "roamjs-components/util/getSubTree";
14+
import { DiscourseNode } from "~/utils/getDiscourseNodes";
15+
import extractRef from "roamjs-components/util/extractRef";
16+
17+
const BlockRenderer = ({ uid }: { uid: string }) => {
18+
const containerRef = useRef<HTMLDivElement>(null);
19+
20+
useEffect(() => {
21+
const container = containerRef.current;
22+
if (container) {
23+
container.innerHTML = "";
24+
25+
window.roamAlphaAPI.ui.components.renderBlock({
26+
uid: uid,
27+
el: container,
28+
});
29+
}
30+
}, [uid]);
31+
32+
return <div ref={containerRef} className="my-2 rounded border p-2" />;
33+
};
34+
35+
const DiscourseNodeSuggestiveRules = ({
36+
node,
37+
parentUid,
38+
}: {
39+
node: DiscourseNode;
40+
parentUid: string;
41+
}) => {
42+
const nodeUid = node.type;
43+
44+
const [embeddingRef, setEmbeddingRef] = useState(node.embeddingRef);
45+
46+
useEffect(() => {
47+
setEmbeddingRef(node.embeddingRef || "");
48+
}, [node.embeddingRef]);
49+
50+
const blockUidToRender = useMemo(
51+
() => extractRef(embeddingRef),
52+
[embeddingRef],
53+
);
54+
55+
const templateUid = useMemo(
56+
() =>
57+
getSubTree({
58+
parentUid: nodeUid,
59+
key: "Template",
60+
}).uid || "",
61+
[nodeUid],
62+
);
63+
64+
const handleEmbeddingRefChange = useCallback(
65+
(e: React.ChangeEvent<HTMLInputElement>) => {
66+
const newValue = e.target.value;
67+
setEmbeddingRef(newValue);
68+
},
69+
[],
70+
);
71+
72+
return (
73+
<div className="flex flex-col gap-4 p-4">
74+
<BlocksPanel
75+
title="Template"
76+
description={`The template that auto fills ${node.text} page when generated.`}
77+
order={0}
78+
parentUid={nodeUid}
79+
uid={templateUid}
80+
defaultValue={node.template}
81+
/>
82+
83+
<TextPanel
84+
title="Embedding Block Ref"
85+
description="Copy block ref from template which you want to be embedded and ranked."
86+
order={0}
87+
uid={node.embeddingRefUid || ""}
88+
parentUid={parentUid}
89+
value={node.embeddingRef || ""}
90+
defaultValue={node.embeddingRef || ""}
91+
options={{
92+
placeholder: "((block-uid))",
93+
onChange: handleEmbeddingRefChange,
94+
}}
95+
/>
96+
97+
{blockUidToRender && (
98+
<div>
99+
<div className="mb-1 text-sm text-gray-600">Preview:</div>
100+
<BlockRenderer uid={blockUidToRender} />
101+
</div>
102+
)}
103+
104+
<FlagPanel
105+
title="First Child"
106+
description="If the block is the first child of the embedding block ref, it will be embedded and ranked."
107+
order={1}
108+
uid={node.isFirstChild?.uid || ""}
109+
parentUid={parentUid}
110+
value={node.isFirstChild?.value || false}
111+
/>
112+
113+
{/* TODO: Add a button to update embeddings in seperate PR */}
114+
<Button
115+
text="Update Embeddings"
116+
intent={Intent.NONE}
117+
onClick={() => console.log("Not implemented")}
118+
className="w-52"
119+
disabled
120+
/>
121+
</div>
122+
);
123+
};
124+
125+
export default DiscourseNodeSuggestiveRules;

apps/roam/src/components/settings/NodeConfig.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
12
import React, { useState, useCallback, useRef, useEffect } from "react";
23
import { DiscourseNode } from "~/utils/getDiscourseNodes";
34
import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel";
@@ -22,6 +23,7 @@ import { OnloadArgs } from "roamjs-components/types";
2223
import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid";
2324
import createBlock from "roamjs-components/writes/createBlock";
2425
import updateBlock from "roamjs-components/writes/updateBlock";
26+
import DiscourseNodeSuggestiveRules from "./DiscourseNodeSuggestiveRules";
2527

2628
const ValidatedInputPanel = ({
2729
label,
@@ -161,6 +163,7 @@ const NodeConfig = ({
161163
const graphOverviewUid = getUid("Graph Overview");
162164
const specificationUid = getUid("Specification");
163165
const indexUid = getUid("Index");
166+
const suggestiveRulesUid = getUid("Suggestive Rules");
164167
const attributeNode = getSubTree({
165168
parentUid: node.type,
166169
key: "Attributes",
@@ -383,6 +386,18 @@ const NodeConfig = ({
383386
</div>
384387
}
385388
/>
389+
<Tab
390+
id="suggestive-mode"
391+
title="Suggestive Mode"
392+
panel={
393+
<div className="flex flex-col gap-4 p-1">
394+
<DiscourseNodeSuggestiveRules
395+
node={node}
396+
parentUid={suggestiveRulesUid}
397+
/>
398+
</div>
399+
}
400+
/>
386401
</Tabs>
387402
</>
388403
);
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
import React, { useState, useCallback } from "react";
3+
import { Label, Button, Intent, Tag } from "@blueprintjs/core";
4+
import Description from "roamjs-components/components/Description";
5+
import AutocompleteInput from "roamjs-components/components/AutocompleteInput";
6+
import createBlock from "roamjs-components/writes/createBlock";
7+
import deleteBlock from "roamjs-components/writes/deleteBlock";
8+
import getAllPageNames from "roamjs-components/queries/getAllPageNames";
9+
import { type PageGroup } from "~/utils/getSuggestiveModeConfigSettings";
10+
11+
const PageGroupsPanel = ({
12+
uid,
13+
initialGroups,
14+
}: {
15+
uid: string;
16+
initialGroups: PageGroup[];
17+
}) => {
18+
const [pageGroups, setPageGroups] = useState<PageGroup[]>(initialGroups);
19+
20+
const [newGroupName, setNewGroupName] = useState("");
21+
const [newPageInputs, setNewPageInputs] = useState<Record<string, string>>(
22+
{},
23+
);
24+
const [autocompleteKeys, setAutocompleteKeys] = useState<
25+
Record<string, number>
26+
>({});
27+
28+
const addGroup = async (name: string) => {
29+
if (!name || pageGroups.some((g) => g.name === name)) return;
30+
try {
31+
const newGroupUid = await createBlock({
32+
parentUid: uid,
33+
node: { text: name },
34+
});
35+
setPageGroups([...pageGroups, { uid: newGroupUid, name, pages: [] }]);
36+
setNewGroupName("");
37+
} catch (e) {
38+
console.error("Error adding group", e);
39+
}
40+
};
41+
42+
const removeGroup = async (groupUid: string) => {
43+
try {
44+
await deleteBlock(groupUid);
45+
setPageGroups(pageGroups.filter((g) => g.uid !== groupUid));
46+
} catch (e) {
47+
console.error("Error removing group", e);
48+
}
49+
};
50+
51+
const addPageToGroup = async (groupUid: string, page: string) => {
52+
const group = pageGroups.find((g) => g.uid === groupUid);
53+
if (!page || group?.pages.some((p) => p.name === page)) {
54+
return;
55+
}
56+
try {
57+
const newPageUid = await createBlock({
58+
parentUid: groupUid,
59+
node: { text: page },
60+
});
61+
setPageGroups(
62+
pageGroups.map((g) =>
63+
g.uid === groupUid
64+
? { ...g, pages: [...g.pages, { uid: newPageUid, name: page }] }
65+
: g,
66+
),
67+
);
68+
setNewPageInputs((prev) => ({
69+
...prev,
70+
[groupUid]: "",
71+
}));
72+
setAutocompleteKeys((prev) => ({
73+
...prev,
74+
[groupUid]: (prev[groupUid] || 0) + 1,
75+
}));
76+
} catch (e) {
77+
console.error("Error adding page to group", e);
78+
}
79+
};
80+
81+
const removePageFromGroup = async (groupUid: string, pageUid: string) => {
82+
try {
83+
await deleteBlock(pageUid);
84+
setPageGroups(
85+
pageGroups.map((g) =>
86+
g.uid === groupUid
87+
? { ...g, pages: g.pages.filter((p) => p.uid !== pageUid) }
88+
: g,
89+
),
90+
);
91+
} catch (e) {
92+
console.error("Error removing page from group", e);
93+
}
94+
};
95+
96+
const getPageInput = (groupUid: string) => newPageInputs[groupUid] || "";
97+
const setPageInput = useCallback((groupUid: string, value: string) => {
98+
setTimeout(() => {
99+
setNewPageInputs((prev) => ({
100+
...prev,
101+
[groupUid]: value,
102+
}));
103+
}, 0);
104+
}, []);
105+
const getAutocompleteKey = (groupUid: string) =>
106+
autocompleteKeys[groupUid] || 0;
107+
108+
return (
109+
<Label>
110+
Page Groups
111+
<Description
112+
description={
113+
"Organize pages into named groups that will be can be selected when generating Discourse Suggestions."
114+
}
115+
/>
116+
<div className="flex flex-col gap-2">
117+
{/* Add Group */}
118+
<div className="flex items-center gap-2">
119+
<AutocompleteInput
120+
value={newGroupName}
121+
setValue={setNewGroupName}
122+
placeholder="Page group name…"
123+
options={[]}
124+
/>
125+
<Button
126+
icon="plus"
127+
small
128+
minimal
129+
disabled={
130+
!newGroupName || pageGroups.some((g) => g.name === newGroupName)
131+
}
132+
onClick={() => void addGroup(newGroupName)}
133+
/>
134+
</div>
135+
136+
{/* Existing Groups */}
137+
{Object.keys(pageGroups).length === 0 && (
138+
<div className="text-sm italic text-gray-500">No groups added.</div>
139+
)}
140+
{pageGroups.map((group) => (
141+
<div key={group.uid} className="rounded border p-2">
142+
<div className="mb-1 flex items-center justify-between">
143+
<span className="font-semibold">{group.name}</span>
144+
<Button
145+
icon="trash"
146+
minimal
147+
small
148+
intent={Intent.DANGER}
149+
onClick={() => void removeGroup(group.uid)}
150+
/>
151+
</div>
152+
<div className="flex flex-wrap items-center gap-2">
153+
<div
154+
className="flex-0 min-w-[160px]"
155+
onKeyDown={(e) => {
156+
if (e.key === "Enter" && getPageInput(group.uid)) {
157+
e.preventDefault();
158+
e.stopPropagation();
159+
void addPageToGroup(group.uid, getPageInput(group.uid));
160+
}
161+
}}
162+
>
163+
<AutocompleteInput
164+
key={getAutocompleteKey(group.uid)}
165+
value={getPageInput(group.uid)}
166+
placeholder="Add page…"
167+
setValue={(v) => setPageInput(group.uid, v)}
168+
options={getAllPageNames()}
169+
maxItemsDisplayed={50}
170+
/>
171+
</div>
172+
<Button
173+
icon="plus"
174+
small
175+
minimal
176+
onClick={() =>
177+
void addPageToGroup(group.uid, getPageInput(group.uid))
178+
}
179+
disabled={
180+
!getPageInput(group.uid) ||
181+
group.pages.some((p) => p.name === getPageInput(group.uid))
182+
}
183+
/>
184+
</div>
185+
{group.pages.length > 0 && (
186+
<div className="mt-2 flex flex-wrap gap-1">
187+
{group.pages.map((p) => (
188+
<Tag
189+
key={p.uid}
190+
onRemove={() => void removePageFromGroup(group.uid, p.uid)}
191+
round
192+
minimal
193+
>
194+
{p.name}
195+
</Tag>
196+
))}
197+
</div>
198+
)}
199+
</div>
200+
))}
201+
</div>
202+
</Label>
203+
);
204+
};
205+
206+
export default PageGroupsPanel;

apps/roam/src/components/settings/Settings.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import sendErrorEmail from "~/utils/sendErrorEmail";
2525
import HomePersonalSettings from "./HomePersonalSettings";
2626
import refreshConfigTree from "~/utils/refreshConfigTree";
2727
import { FeedbackWidget } from "~/components/BirdEatsBugs";
28+
import SuggestiveModeSettings from "./SuggestiveModeSettings";
2829
import { getVersionWithDate } from "~/utils/getVersion";
2930

3031
type SectionHeaderProps = {
@@ -161,6 +162,12 @@ export const SettingsDialog = ({
161162
className="overflow-y-auto"
162163
panel={<DiscourseGraphExport />}
163164
/>
165+
<Tab
166+
id="suggestive-mode-settings"
167+
title="Suggestive Mode"
168+
className="overflow-y-auto"
169+
panel={<SuggestiveModeSettings />}
170+
/>
164171
<SectionHeader>Grammar</SectionHeader>
165172
<Tab
166173
id="discourse-relations"

0 commit comments

Comments
 (0)