Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/frontend/src/components/document_picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import type { Document, Uuid } from "catlog-wasm";
import { useApi } from "../api";
import { TheoryLibraryContext } from "../theory";
import { isDocumentVisible, useUserSettings } from "../user/user_settings_context";
import { useUserState } from "../user/user_state_context";

import "./document_picker.css";
Expand Down Expand Up @@ -166,6 +167,7 @@ function DocSearchInput(
]);

const userState = useUserState();
const { settings } = useUserSettings();
const theories = useContext(TheoryLibraryContext);

const [inputText, setInputText] = createSignal("");
Expand All @@ -191,6 +193,9 @@ function DocSearchInput(
const entries = Object.entries(docs) as [string, DocInfo][];
return entries
.filter(([refId, doc]) => {
if (!isDocumentVisible(doc, settings())) {
return false;
}
if (doc.deletedAt !== null) {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/page/menubar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ function SettingsMenuItem() {
return (
<MenuItem onSelect={() => navigate("/profile")}>
<SettingsIcon />
<MenuItemLabel>{"Edit user profile"}</MenuItemLabel>
<MenuItemLabel>{"Settings"}</MenuItemLabel>
</MenuItem>
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/user/document_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { stringify as uuidStringify } from "uuid";

import { RelativeTime, createVirtualList, DocumentTypeIcon } from "catcolab-ui-components";
import { TheoryLibraryContext } from "../theory";
import { isDocumentVisible, type UserSettings } from "./user_settings_context";
import { currentUserPermission, formatOwners, useUserState } from "./user_state_context";

import "./documents.css";
Expand All @@ -17,9 +18,11 @@ export function filterDocuments(
opts: {
query: string;
deleted: boolean;
settings: UserSettings;
},
): (DocInfo & { refId: string })[] {
return (Object.entries(documents) as [string, DocInfo][])
.filter(([, doc]) => isDocumentVisible(doc, opts.settings))
.filter(([, doc]) => (opts.deleted ? doc.deletedAt !== null : doc.deletedAt === null))
.map(([refId, doc]) => Object.assign({ refId }, doc))
.filter((doc) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/user/documents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IconButton } from "catcolab-ui-components";
import { BrandedToolbar, PageActionsContext } from "../page";
import { DocumentList, filterDocuments } from "./document_list";
import { LoginGate } from "./login";
import { useUserSettings } from "./user_settings_context";
import { useUserState } from "./user_state_context";

import "./documents.css";
Expand All @@ -34,6 +35,7 @@ export default function UserDocuments() {

function DocumentsSearch() {
const userState = useUserState();
const { settings } = useUserSettings();
const [searchQuery, setSearchQuery] = createSignal("");
const actions = useContext(PageActionsContext);
invariant(actions, "Page actions should be provided");
Expand All @@ -42,6 +44,7 @@ function DocumentsSearch() {
filterDocuments(userState.documents, {
query: searchQuery().trim().toLowerCase(),
deleted: false,
settings: settings(),
}),
);

Expand Down
42 changes: 27 additions & 15 deletions packages/frontend/src/user/inference_key_provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,42 @@ import { type JSX, createEffect, createResource } from "solid-js";

import { useApi } from "../api";
import { type InferenceKeyResult, InferenceKeyContext } from "./inference_key_context";
import { useUserSettings } from "./user_settings_context";

/** Provides the authenticated user's inference key. */
export function InferenceKeyProvider(props: { children: JSX.Element }) {
const api = useApi();
const firebaseApp = useFirebaseApp();
const auth = useAuth(getAuth(firebaseApp));
const { settings, isReady } = useUserSettings();

const [inferenceKey, { mutate }] = createResource(
() => auth.data?.uid ?? null,
async () => {
const result = await api.rpc.get_inference_key.query();
if (result.tag === "Ok") {
return { tag: "Ready", key: result.content } as InferenceKeyResult;
}
if (result.code === 503) {
return { tag: "Unavailable" } as InferenceKeyResult;
}
throw new Error(result.message);
},
);
const enabledUserId = () => {
const userId = auth.data?.uid;
if (userId === undefined) {
return null;
}
if (!isReady()) {
return null;
}
if (!settings().llmCapabilitiesEnabled) {
return null;
}
return userId;
};

const [inferenceKey, { mutate }] = createResource(enabledUserId, async () => {
const result = await api.rpc.get_inference_key.query();
if (result.tag === "Ok") {
return { tag: "Ready", key: result.content } as InferenceKeyResult;
}
if (result.code === 503) {
return { tag: "Unavailable" } as InferenceKeyResult;
}
throw new Error(result.message);
});

// clear the resource explicitly on sign-out
createEffect(() => {
if (auth.data == null) {
if (enabledUserId() === null) {
mutate(undefined);
}
});
Expand Down
59 changes: 59 additions & 0 deletions packages/frontend/src/user/llm_capabilities.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { DocInfo } from "catcolab-api/src/user_state";
import { renderToString } from "solid-js/web";
import { assert, test, vi } from "vitest";

import type { UserSettings } from "./user_settings_context";

const { getInferenceKey } = vi.hoisted(() => ({
getInferenceKey: vi.fn<() => Promise<never>>(),
}));

vi.mock("firebase/auth", () => ({
getAuth: vi.fn<() => unknown>(),
}));

vi.mock("solid-firebase", () => ({
useAuth: () => ({ data: { uid: "test-user" } }),
useFirebaseApp: () => ({}),
}));

vi.mock("../api", () => ({
useApi: () => ({
rpc: {
get_inference_key: {
query: getInferenceKey,
},
},
}),
}));

vi.mock("./user_settings_context", async (importOriginal) => ({
...(await importOriginal<typeof import("./user_settings_context")>()),
useUserSettings: () => ({
settings: () => ({ llmCapabilitiesEnabled: false }),
isReady: () => true,
updateSettings: vi.fn<(patch: Partial<UserSettings>) => void>(),
}),
}));

import { InferenceKeyProvider } from "./inference_key_provider";
import { isDocumentVisible } from "./user_settings_context";

test("choosing no blocks LLM features", async () => {
renderToString(() => <InferenceKeyProvider>{null}</InferenceKeyProvider>);
await Promise.resolve();

assert.equal(getInferenceKey.mock.calls.length, 0);

const documents: Array<Pick<DocInfo, "typeName">> = [
{ typeName: "llmconversation" },
{ typeName: "model" },
];
const settings: UserSettings = { llmCapabilitiesEnabled: false };
const visibleDocuments = documents.filter((doc) => isDocumentVisible(doc, settings));

assert.deepEqual(
visibleDocuments.map((doc) => doc.typeName),
["model"],
);
});
41 changes: 38 additions & 3 deletions packages/frontend/src/user/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,63 @@ import { Button, FormGroup, TextInputField } from "catcolab-ui-components";
import { useApi } from "../api";
import { BrandedToolbar } from "../page";
import { LoginGate } from "./login";
import { useUserSettings } from "./user_settings_context";
import { useUserState } from "./user_state_context";

/** Page to configure user profile. */
/** Page to configure user settings. */
export default function UserProfilePage() {
const appTitle = import.meta.env.VITE_APP_TITLE;

return (
<>
<Title>Profile - {appTitle}</Title>
<Title>User Settings - {appTitle}</Title>
<div class="growable-container">
<BrandedToolbar />
<div class="page-container">
<LoginGate>
<h1>User settings</h1>
<hr />
<h2>Public profile</h2>
<UserProfileForm />
<hr />
<h2>Functionality</h2>
<LLMCapabilitiesSetting />
</LoginGate>
</div>
</div>
</>
);
}

/** Toggle the user's access to LLM-powered features. */
function LLMCapabilitiesSetting() {
const { settings, isReady, updateSettings } = useUserSettings();

return (
<p>
<label>
<input
Comment thread
tslil-topos marked this conversation as resolved.
Outdated
type="checkbox"
checked={settings().llmCapabilitiesEnabled}
disabled={!isReady()}
onChange={(evt) =>
updateSettings({
llmCapabilitiesEnabled: evt.currentTarget.checked,
})
}
/>{" "}
<span style={{ display: "inline-block", "vertical-align": "middle" }}>
Comment thread
tslil-topos marked this conversation as resolved.
Outdated
LLM capabilities
<br />
<small>
Enable LLM-powered features, including LLM Conversation documents.
</small>
</span>
</label>
</p>
);
}

/** Form to configure user proifle. */
export function UserProfileForm() {
const api = useApi();
Expand Down Expand Up @@ -96,7 +131,7 @@ export function UserProfileForm() {
</Field>
</FormGroup>
<Button type="submit" variant="positive">
Update profile
Update public profile
</Button>
</Form>
);
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/user/trash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useApi } from "../api";
import { BrandedToolbar } from "../page";
import { DocumentList, filterDocuments } from "./document_list";
import { LoginGate } from "./login";
import { useUserSettings } from "./user_settings_context";
import { useUserState } from "./user_state_context";

import "./documents.css";
Expand All @@ -35,12 +36,14 @@ export default function TrashBin() {

function TrashBinSearch() {
const userState = useUserState();
const { settings } = useUserSettings();
const [searchQuery, setSearchQuery] = createSignal("");

const documents = createMemo(() =>
filterDocuments(userState.documents, {
query: searchQuery().trim().toLowerCase(),
deleted: true,
settings: settings(),
}),
);

Expand Down
27 changes: 27 additions & 0 deletions packages/frontend/src/user/user_settings_context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { DocInfo } from "catcolab-api/src/user_state";
import { type Accessor, createContext, useContext } from "solid-js";
import invariant from "tiny-invariant";

export type UserSettings = {
llmCapabilitiesEnabled: boolean;
};

export type UserSettingsContextValue = {
settings: Accessor<UserSettings>;
isReady: Accessor<boolean>;
updateSettings: (patch: Partial<UserSettings>) => void;
};

export const UserSettingsContext = createContext<UserSettingsContextValue>();

/** Whether a document should be visible with the current user settings. */
export function isDocumentVisible(doc: Pick<DocInfo, "typeName">, settings: UserSettings): boolean {
return doc.typeName !== "llmconversation" || settings.llmCapabilitiesEnabled;
}

/** Retrieve the current user's settings and settings actions. */
export function useUserSettings(): UserSettingsContextValue {
const context = useContext(UserSettingsContext);
invariant(context, "User settings should be provided as context");
return context;
}
8 changes: 7 additions & 1 deletion packages/frontend/src/user/user_state_context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import type { PermissionInfo, UserInfo, UserState } from "catcolab-api/src/user_
import { createContext, useContext } from "solid-js";
import invariant from "tiny-invariant";

export const INITIAL_USER_STATE: UserState = {
import type { UserSettings } from "./user_settings_context";

export type AppUserState = UserState & {
settings?: Partial<UserSettings>;
};

export const INITIAL_USER_STATE: AppUserState = {
profile: { username: null, displayName: null },
knownUsers: {},
documents: {},
Expand Down
Loading