Path: blob/master/src/packages/frontend/chat/side-chat.tsx
1496 views
import { Button, Flex, Space, Tooltip } from "antd";1import { useCallback, useEffect, useRef, useState } from "react";2import {3CSS,4redux,5useActions,6useRedux,7useTypedRedux,8} from "@cocalc/frontend/app-framework";9import { AddCollaborators } from "@cocalc/frontend/collaborators";10import { A, Icon, Loading } from "@cocalc/frontend/components";11import { IS_MOBILE } from "@cocalc/frontend/feature";12import { ProjectUsers } from "@cocalc/frontend/projects/project-users";13import { user_activity } from "@cocalc/frontend/tracker";14import { COLORS } from "@cocalc/util/theme";15import type { ChatActions } from "./actions";16import { ChatLog } from "./chat-log";17import Filter from "./filter";18import ChatInput from "./input";19import { LLMCostEstimationChat } from "./llm-cost-estimation";20import { SubmitMentionsFn } from "./types";21import { INPUT_HEIGHT, markChatAsReadIfUnseen } from "./utils";2223interface Props {24project_id: string;25path: string;26style?: CSS;27fontSize?: number;28actions?: ChatActions;29desc?;30}3132export default function SideChat({33actions: actions0,34project_id,35path,36style,37fontSize,38desc,39}: Props) {40// This actionsViaContext via useActions is ONLY needed for side chat for non-frame41// editors, i.e., basically just Sage Worksheets!42const actionsViaContext = useActions(project_id, path);43const actions: ChatActions = actions0 ?? actionsViaContext;44const disableFilters = actions0 == null;45const messages = useRedux(["messages"], project_id, path);46const [lastVisible, setLastVisible] = useState<Date | null>(null);47const [input, setInput] = useState("");48const search = desc?.get("data-search") ?? "";49const selectedHashtags = desc?.get("data-selectedHashtags");50const scrollToIndex = desc?.get("data-scrollToIndex") ?? null;51const scrollToDate = desc?.get("data-scrollToDate") ?? null;52const fragmentId = desc?.get("data-fragmentId") ?? null;53const costEstimate = desc?.get("data-costEstimate");54const addCollab: boolean = useRedux(["add_collab"], project_id, path);55const project_map = useTypedRedux("projects", "project_map");56const project = project_map?.get(project_id);57const scrollToBottomRef = useRef<any>(null);58const submitMentionsRef = useRef<SubmitMentionsFn | undefined>(undefined);5960const markAsRead = useCallback(() => {61markChatAsReadIfUnseen(project_id, path);62}, [project_id, path]);6364// The act of opening/displaying the chat marks it as seen...65// since this happens when the user shows it.66useEffect(() => {67markAsRead();68}, []);6970const sendChat = useCallback(71(options?) => {72actions.sendChat({ submitMentionsRef, ...options });73actions.deleteDraft(0);74scrollToBottomRef.current?.(true);75setTimeout(() => {76scrollToBottomRef.current?.(true);77}, 10);78setTimeout(() => {79scrollToBottomRef.current?.(true);80}, 1000);81},82[actions],83);8485if (messages == null) {86return <Loading />;87}8889// WARNING: making autofocus true would interfere with chat and terminals90// -- where chat and terminal are both focused at same time sometimes91// (esp on firefox).9293return (94<div95style={{96height: "100%",97width: "100%",98display: "flex",99flexDirection: "column",100backgroundColor: "#efefef",101...style,102}}103onMouseMove={markAsRead}104onFocus={() => {105// Remove any active key handler that is next to this side chat.106// E.g, this is critical for tasks lists...107redux.getActions("page").erase_active_key_handler();108}}109>110{!IS_MOBILE && project != null && actions != null && (111<div112style={{113margin: "0 5px",114paddingTop: "5px",115maxHeight: "50vh",116overflow: "auto",117borderBottom: "1px solid lightgrey",118}}119>120<CollabList121addCollab={addCollab}122project={project}123actions={actions}124/>125<AddChatCollab addCollab={addCollab} project_id={project_id} />126</div>127)}128{!disableFilters && (129<Filter130actions={actions}131search={search}132style={{133margin: 0,134...(messages.size >= 2135? undefined136: { visibility: "hidden", height: 0 }),137}}138/>139)}140<div141className="smc-vfill"142style={{143backgroundColor: "#fff",144paddingLeft: "15px",145flex: 1,146margin: "5px 0",147}}148>149<ChatLog150actions={actions}151fontSize={fontSize}152project_id={project_id}153path={path}154scrollToBottomRef={scrollToBottomRef}155mode={"sidechat"}156setLastVisible={setLastVisible}157search={search}158selectedHashtags={selectedHashtags}159disableFilters={disableFilters}160scrollToIndex={scrollToIndex}161scrollToDate={scrollToDate}162selectedDate={fragmentId}163costEstimate={costEstimate}164/>165</div>166167<div>168{input.trim() ? (169<Flex170vertical={false}171align="center"172justify="space-between"173style={{ margin: "5px" }}174>175<Space>176{lastVisible && (177<Tooltip title="Reply to the current thread (shift+enter)">178<Button179disabled={!input.trim() || actions == null}180type="primary"181onClick={() => {182sendChat({ reply_to: new Date(lastVisible) });183}}184>185<Icon name="reply" /> Reply186</Button>187</Tooltip>188)}189<Tooltip190title={191lastVisible192? "Start a new thread"193: "Start a new thread (shift+enter)"194}195>196<Button197type={!lastVisible ? "primary" : undefined}198style={{ marginLeft: "5px" }}199onClick={() => {200sendChat();201user_activity("side_chat", "send_chat", "click");202}}203disabled={!input?.trim() || actions == null}204>205<Icon name="paper-plane" />206New Thread207</Button>208</Tooltip>209</Space>210<div style={{ flex: 1 }} />211<Space>212<Tooltip title={"Launch video chat specific to this document"}>213<Button214disabled={actions == null}215onClick={() => {216actions?.frameTreeActions?.getVideoChat().startChatting();217}}218>219<Icon name="video-camera" />220Video221</Button>222</Tooltip>223{costEstimate?.get("date") == 0 && (224<LLMCostEstimationChat225compact226costEstimate={costEstimate?.toJS()}227style={{ margin: "5px" }}228/>229)}230</Space>231</Flex>232) : undefined}233<ChatInput234autoFocus235fontSize={fontSize}236cacheId={`${path}${project_id}-new`}237input={input}238on_send={() => {239sendChat(lastVisible ? { reply_to: lastVisible } : undefined);240user_activity("side_chat", "send_chat", "keyboard");241actions?.clearAllFilters();242}}243style={{ height: INPUT_HEIGHT }}244height={INPUT_HEIGHT}245onChange={(value) => {246setInput(value);247// submitMentionsRef processes the reply, but does not actually send the mentions248const input = submitMentionsRef.current?.(undefined, true) ?? value;249actions?.llmEstimateCost({ date: 0, input });250}}251submitMentionsRef={submitMentionsRef}252syncdb={actions.syncdb}253date={0}254editBarStyle={{ overflow: "none" }}255/>256</div>257</div>258);259}260261function AddChatCollab({ addCollab, project_id }) {262if (!addCollab) {263return null;264}265return (266<div>267@mention AI or collaborators, add more collaborators below, or{" "}268<A href="https://discord.gg/EugdaJZ8">join the CoCalc Discord.</A>269<AddCollaborators project_id={project_id} autoFocus where="side-chat" />270<div style={{ color: COLORS.GRAY_M }}>271(Collaborators have access to all files in this project.)272</div>273</div>274);275}276277function CollabList({ project, addCollab, actions }) {278return (279<div280style={281!addCollab282? {283maxHeight: "1.7em",284whiteSpace: "nowrap",285overflow: "hidden",286textOverflow: "ellipsis",287cursor: "pointer",288}289: { cursor: "pointer" }290}291onClick={() => actions.setState({ add_collab: !addCollab })}292>293<div style={{ width: "16px", display: "inline-block" }}>294<Icon name={addCollab ? "caret-down" : "caret-right"} />295</div>296<span style={{ color: COLORS.GRAY_M, fontSize: "10pt" }}>297<ProjectUsers298project={project}299none={<span>Add people to work with...</span>}300/>301</span>302</div>303);304}305306307