Path: blob/master/src/packages/frontend/editors/markdown-input/mentionable-users.tsx
5854 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Tooltip } from "antd";6import { List } from "immutable";7import { isEmpty } from "lodash";8import { Avatar } from "@cocalc/frontend/account/avatar/avatar";9import { useLanguageModelSetting } from "@cocalc/frontend/account/useLanguageModelSetting";10import { redux, useMemo, useTypedRedux } from "@cocalc/frontend/app-framework";11import AnthropicAvatar from "@cocalc/frontend/components/anthropic-avatar";12import GoogleGeminiLogo from "@cocalc/frontend/components/google-gemini-avatar";13import { LanguageModelVendorAvatar } from "@cocalc/frontend/components/language-model-icon";14import MistralAvatar from "@cocalc/frontend/components/mistral-avatar";15import OpenAIAvatar from "@cocalc/frontend/components/openai-avatar";16import XAIAvatar from "@cocalc/frontend/components/xai-avatar";17import { LLMModelPrice } from "@cocalc/frontend/frame-editors/llm/llm-selector";18import { useUserDefinedLLM } from "@cocalc/frontend/frame-editors/llm/use-userdefined-llm";19import { useProjectContext } from "@cocalc/frontend/project/context";20import {21ANTHROPIC_MODELS,22GOOGLE_MODELS,23LLMServicesAvailable,24LLM_DESCR,25LLM_USERNAMES,26LanguageModel,27MISTRAL_MODELS,28MODELS_OPENAI,29UserDefinedLLM,30fromCustomOpenAIModel,31fromOllamaModel,32isCustomOpenAI,33isOllamaLLM,34isUserDefinedModel,35model2service,36model2vendor,37toCustomOpenAIModel,38toOllamaModel,39toUserLLMModelName,40XAI_MODELS,41} from "@cocalc/util/db-schema/llm-utils";42import { cmp, timestamp_cmp, trunc_middle } from "@cocalc/util/misc";43import { CustomLLMPublic } from "@cocalc/util/types/llm";44import { Item as CompleteItem } from "./complete";4546// we make the show_llm_main_menu field required, to avoid forgetting to set it ;-)47type Item = CompleteItem & Required<Pick<CompleteItem, "show_llm_main_menu">>;4849interface Opts {50avatarUserSize?: number;51avatarLLMSize?: number;52}5354export function useMentionableUsers(): (55search: string | undefined,56opts?: Opts,57) => Item[] {58const { project_id, enabledLLMs } = useProjectContext();5960const selectableLLMs = useTypedRedux("customize", "selectable_llms");61const ollama = useTypedRedux("customize", "ollama");62const custom_openai = useTypedRedux("customize", "custom_openai");63const user_llm = useUserDefinedLLM();6465// the current default model. This is always a valid LLM, even if none has ever been selected.66const [model] = useLanguageModelSetting();6768return useMemo(() => {69return (search: string | undefined, opts?: Opts) => {70return mentionableUsers({71search,72project_id,73enabledLLMs,74model,75ollama: ollama?.toJS() ?? {},76custom_openai: custom_openai?.toJS() ?? {},77user_llm,78selectableLLMs,79opts,80});81};82}, [project_id, JSON.stringify(enabledLLMs), ollama, custom_openai, model]);83}8485interface Props {86search: string | undefined;87project_id: string;88model: LanguageModel;89ollama: { [key: string]: CustomLLMPublic };90custom_openai: { [key: string]: CustomLLMPublic };91enabledLLMs: LLMServicesAvailable;92selectableLLMs: List<string>;93user_llm: UserDefinedLLM[];94opts?: Opts;95}9697function mentionableUsers({98search,99project_id,100enabledLLMs,101model,102ollama,103custom_openai,104selectableLLMs,105user_llm,106opts,107}: Props): Item[] {108const { avatarUserSize = 24, avatarLLMSize = 24 } = opts ?? {};109110const users = redux111.getStore("projects")112.getIn(["project_map", project_id, "users"]);113114const last_active = redux115.getStore("projects")116.getIn(["project_map", project_id, "last_active"]);117118if (users == null || last_active == null) return []; // e.g., for an admin119120const my_account_id = redux.getStore("account").get("account_id");121122function getProjectUsers() {123const project_users: {124account_id: string;125last_active: Date | undefined;126}[] = [];127for (const [account_id] of users) {128project_users.push({129account_id,130last_active: last_active.get(account_id),131});132}133project_users.sort((a, b) => {134// always push self to bottom...135if (a.account_id == my_account_id) {136return 1;137}138if (b.account_id == my_account_id) {139return -1;140}141if (a == null || b == null) return cmp(a.account_id, b.account_id);142if (a == null && b != null) return 1;143if (a != null && b == null) return -1;144return timestamp_cmp(a, b, "last_active");145});146return project_users;147}148149const project_users = getProjectUsers();150151const users_store = redux.getStore("users");152153const mentions: Item[] = [];154155if (enabledLLMs.openai) {156// NOTE: all modes are included, including the 16k version, because:157// (1) if you use GPT-3.5 too much you hit your limit,158// (2) this is a non-free BUT CHEAP model you can actually use after hitting your limit, which is much cheaper than GPT-4.159for (const moai of MODELS_OPENAI) {160if (!selectableLLMs.includes(moai)) continue;161const show_llm_main_menu = moai === model;162const size = show_llm_main_menu ? avatarUserSize : avatarLLMSize;163const v = "openai";164const m = moai.replace(/-/g, "");165const n = LLM_USERNAMES[moai].replace(/ /g, "");166const search_term = `${v}chat${m}${n}`.toLowerCase();167if (!search || search_term.includes(search)) {168mentions.push({169value: model2service(moai),170label: (171<LLMTooltip model={moai}>172<OpenAIAvatar size={size} /> {LLM_USERNAMES[moai]}{" "}173<LLMModelPrice model={moai} floatRight />174</LLMTooltip>175),176search: search_term,177is_llm: true,178show_llm_main_menu,179});180}181}182}183184if (enabledLLMs.google) {185for (const m of GOOGLE_MODELS) {186if (!selectableLLMs.includes(m)) continue;187const show_llm_main_menu = m === model;188const size = show_llm_main_menu ? avatarUserSize : avatarLLMSize;189const v = model2vendor(m);190const search_term = `${v}${m.replace(/-/g, "").toLowerCase()}`;191if (!search || search_term.includes(search)) {192mentions.push({193value: model2service(m),194label: (195<LLMTooltip model={m}>196<GoogleGeminiLogo size={size} /> {LLM_USERNAMES[m]}{" "}197<LLMModelPrice model={m} floatRight />198</LLMTooltip>199),200search: search_term,201is_llm: true,202show_llm_main_menu,203});204}205}206}207208if (enabledLLMs.xai) {209for (const m of XAI_MODELS) {210if (!selectableLLMs.includes(m)) continue;211const show_llm_main_menu = m === model;212const size = show_llm_main_menu ? avatarUserSize : avatarLLMSize;213const name = LLM_USERNAMES[m] ?? m;214const vendor = model2vendor(m);215const search_term =216`${vendor.name}${m.replace(/-/g, "")}${name.replace(/ /g, "")}`.toLowerCase();217if (!search || search_term.includes(search)) {218mentions.push({219value: model2service(m),220label: (221<LLMTooltip model={m}>222<XAIAvatar size={size} /> {name}{" "}223<LLMModelPrice model={m} floatRight />224</LLMTooltip>225),226search: search_term,227is_llm: true,228show_llm_main_menu,229});230}231}232}233234if (enabledLLMs.mistralai) {235for (const m of MISTRAL_MODELS) {236if (!selectableLLMs.includes(m)) continue;237const show_llm_main_menu = m === model;238const size = show_llm_main_menu ? avatarUserSize : avatarLLMSize;239const name = LLM_USERNAMES[m] ?? m;240const s = model2vendor(m);241const search_term = `${s}${m}${name}`.toLowerCase();242if (!search || search_term.includes(search)) {243mentions.push({244value: model2service(m),245label: (246<LLMTooltip model={m}>247<MistralAvatar size={size} /> {name}{" "}248<LLMModelPrice model={m} floatRight />249</LLMTooltip>250),251search: search_term,252is_llm: true,253show_llm_main_menu,254});255}256}257}258259if (enabledLLMs.anthropic) {260for (const m of ANTHROPIC_MODELS) {261if (!selectableLLMs.includes(m)) continue;262const show_llm_main_menu = m === model;263const size = show_llm_main_menu ? avatarUserSize : avatarLLMSize;264const name = LLM_USERNAMES[m] ?? m;265const s = model2vendor(m);266const search_term = `${s}${m}${name}`.toLowerCase();267if (!search || search_term.includes(search)) {268mentions.push({269value: model2service(m),270label: (271<LLMTooltip model={m}>272<AnthropicAvatar size={size} /> {name}{" "}273<LLMModelPrice model={m} floatRight />274</LLMTooltip>275),276search: search_term,277is_llm: true,278show_llm_main_menu,279});280}281}282}283284if (enabledLLMs.ollama && !isEmpty(ollama)) {285for (const [m, conf] of Object.entries(ollama)) {286const show_llm_main_menu =287isOllamaLLM(model) && m === fromOllamaModel(model);288const size = show_llm_main_menu ? avatarUserSize : avatarLLMSize;289const value = toOllamaModel(m);290const search_term = `${m}${value}${conf.display}`.toLowerCase();291if (!search || search_term.includes(search)) {292mentions.push({293value,294label: (295<span>296<LanguageModelVendorAvatar model={value} size={size} />{" "}297{conf.display} <LLMModelPrice model={m} floatRight />298</span>299),300search: search_term,301is_llm: true,302show_llm_main_menu,303});304}305}306}307308if (enabledLLMs.custom_openai && !isEmpty(custom_openai)) {309for (const [m, conf] of Object.entries(custom_openai)) {310const show_llm_main_menu =311isCustomOpenAI(model) && m === fromCustomOpenAIModel(model);312const size = show_llm_main_menu ? avatarUserSize : avatarLLMSize;313const value = toCustomOpenAIModel(m);314const search_term = `${m}${value}${conf.display}`.toLowerCase();315if (!search || search_term.includes(search)) {316mentions.push({317value,318label: (319<span>320<LanguageModelVendorAvatar model={value} size={size} />{" "}321{conf.display} <LLMModelPrice model={m} floatRight />322</span>323),324search: search_term,325is_llm: true,326show_llm_main_menu,327});328}329}330}331332if (!isEmpty(user_llm)) {333for (const llm of user_llm) {334const m = toUserLLMModelName(llm);335const show_llm_main_menu = isUserDefinedModel(model) && m === model;336const size = show_llm_main_menu ? avatarUserSize : avatarLLMSize;337const value = m;338const search_term = `${value}${llm.display}`.toLowerCase();339if (!search || search_term.includes(search)) {340mentions.push({341value,342label: (343<span>344<LanguageModelVendorAvatar model={value} size={size} />{" "}345{llm.display}346</span>347),348search: search_term,349is_llm: true,350show_llm_main_menu,351});352}353}354}355356for (const { account_id } of project_users) {357const fullname = users_store.get_name(account_id) ?? "";358const s = fullname.toLowerCase();359if (search != null && s.indexOf(search) == -1) continue;360const name = trunc_middle(fullname, 64);361const label = (362<span>363<Avatar account_id={account_id} size={avatarUserSize} /> {name}364</span>365);366mentions.push({367value: account_id,368label,369search: s,370is_llm: false,371show_llm_main_menu: true, // irrelevant, but that's what it will do for standard user accounts372});373}374375return mentions;376}377378function LLMTooltip({379model,380children,381}: {382model: string;383children: React.ReactNode;384}) {385const descr = LLM_DESCR[model];386const title = <>{descr}</>;387return (388<Tooltip title={title} placement="right">389<div style={{ width: "100%" }}>{children}</div>390</Tooltip>391);392}393394395