From c24dade1384387f6fc7ef370ac8f8f9c18ad3044 Mon Sep 17 00:00:00 2001 From: Harald Schilly Date: Mon, 5 May 2025 15:51:15 +0200 Subject: [PATCH 1/7] frontend: start working on more compact/hidden chat threads --- src/packages/frontend/chat/chat-log.tsx | 49 +++++++++++-------- src/packages/frontend/chat/chatroom.tsx | 15 +++--- .../frontend/chat/llm-cost-estimation.tsx | 3 +- src/packages/frontend/chat/message.tsx | 27 +++++----- src/packages/frontend/chat/types.ts | 4 ++ src/packages/frontend/chat/viewer.tsx | 10 ++-- src/packages/frontend/cspell.json | 7 ++- .../frame-editors/chat-editor/editor.ts | 3 +- .../frame-editors/code-editor/actions.ts | 4 +- .../time-travel-editor/viewer.tsx | 12 ++--- 10 files changed, 79 insertions(+), 55 deletions(-) diff --git a/src/packages/frontend/chat/chat-log.tsx b/src/packages/frontend/chat/chat-log.tsx index 1a316927575..1c859cdf8c7 100644 --- a/src/packages/frontend/chat/chat-log.tsx +++ b/src/packages/frontend/chat/chat-log.tsx @@ -7,15 +7,19 @@ Render all the messages in the chat. */ +// cSpell:ignore: timespan + import { Alert, Button } from "antd"; import { Set as immutableSet } from "immutable"; import { MutableRefObject, useEffect, useMemo, useRef } from "react"; import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; + import { chatBotName, isChatBot } from "@cocalc/frontend/account/chatbot"; import { useRedux, useTypedRedux } from "@cocalc/frontend/app-framework"; import { Icon } from "@cocalc/frontend/components"; import useVirtuosoScrollHook from "@cocalc/frontend/components/virtuoso-scroll-hook"; import { HashtagBar } from "@cocalc/frontend/editors/task-editor/hashtag-bar"; +import { DivTempHeight } from "@cocalc/frontend/jupyter/cell-list"; import { cmp, hoursToTimeIntervalHuman, @@ -24,12 +28,21 @@ import { } from "@cocalc/util/misc"; import type { ChatActions } from "./actions"; import Composing from "./composing"; -import Message from "./message"; -import type { ChatMessageTyped, ChatMessages, Mode } from "./types"; -import { getSelectedHashtagsSearch, newest_content } from "./utils"; -import { getRootMessage, getThreadRootDate } from "./utils"; -import { DivTempHeight } from "@cocalc/frontend/jupyter/cell-list"; import { filterMessages } from "./filter-messages"; +import Message from "./message"; +import type { + ChatMessageTyped, + ChatMessages, + CostEstimate, + Mode, + NumChildren, +} from "./types"; +import { + getRootMessage, + getSelectedHashtagsSearch, + getThreadRootDate, + newest_content, +} from "./utils"; interface Props { project_id: string; // used to render links more effectively @@ -83,7 +96,7 @@ export function ChatLog({ } = useMemo<{ dates: string[]; numFolded: number; - numChildren; + numChildren: NumChildren; }>(() => { const { dates, numFolded, numChildren } = getSortedDates( messages, @@ -288,10 +301,7 @@ function isPrevMessageSender( ); } -function isThread( - message: ChatMessageTyped, - numChildren: { [date: number]: number }, -) { +function isThread(message: ChatMessageTyped, numChildren: NumChildren) { if (message.get("reply_to") != null) { return true; } @@ -324,7 +334,7 @@ export function getSortedDates( ): { dates: string[]; numFolded: number; - numChildren: { [date: number]: number }; + numChildren: NumChildren; } { let numFolded = 0; let m = messages; @@ -342,7 +352,7 @@ export function getSortedDates( // Do a linear pass through all messages to divide into threads, so that // getSortedDates is O(n) instead of O(n^2) ! - const numChildren: { [date: number]: number } = {}; + const numChildren: NumChildren = {}; for (const [_, message] of m) { const parent = message.get("reply_to"); if (parent != null) { @@ -486,22 +496,21 @@ export function MessageList({ selectedDate, numChildren, }: { - messages; - account_id; + messages: ChatMessages; + account_id: string; user_map; mode; sortedDates; virtuosoRef?; - search?; - project_id?; - path?; - fontSize?; + project_id?: string; + path?: string; + fontSize?: number; selectedHashtags?; actions?; - costEstimate?; + costEstimate?: CostEstimate; manualScrollRef?; selectedDate?: string; - numChildren?; + numChildren?: NumChildren; }) { const virtuosoHeightsRef = useRef<{ [index: number]: number }>({}); const virtuosoScroll = useVirtuosoScrollHook({ diff --git a/src/packages/frontend/chat/chatroom.tsx b/src/packages/frontend/chat/chatroom.tsx index 227058b2936..36b8bf65087 100644 --- a/src/packages/frontend/chat/chatroom.tsx +++ b/src/packages/frontend/chat/chatroom.tsx @@ -5,6 +5,8 @@ import { Button, Divider, Input, Select, Space, Tooltip } from "antd"; import { debounce } from "lodash"; +import { FormattedMessage } from "react-intl"; + import { ButtonGroup, Col, Row, Well } from "@cocalc/frontend/antd-bootstrap"; import { React, @@ -18,16 +20,15 @@ import { Icon, Loading } from "@cocalc/frontend/components"; import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown"; import { FrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context"; import { hoursToTimeIntervalHuman } from "@cocalc/util/misc"; -import { FormattedMessage } from "react-intl"; import type { ChatActions } from "./actions"; -import type { ChatState } from "./store"; import { ChatLog } from "./chat-log"; +import Filter from "./filter"; import ChatInput from "./input"; import { LLMCostEstimationChat } from "./llm-cost-estimation"; +import type { ChatState } from "./store"; import { SubmitMentionsFn } from "./types"; import { INPUT_HEIGHT, markChatAsReadIfUnseen } from "./utils"; import VideoChatButton from "./video/launch-button"; -import Filter from "./filter"; const FILTER_RECENT_NONE = { value: 0, @@ -68,7 +69,7 @@ interface Props { project_id: string; path: string; is_visible?: boolean; - font_size: number; + fontSize: number; desc?; } @@ -77,7 +78,7 @@ export function ChatRoom({ project_id, path, is_visible, - font_size, + fontSize, desc, }: Props) { const useEditor = useEditorRedux({ project_id, path }); @@ -285,7 +286,7 @@ export function ChatRoom({ path={path} scrollToBottomRef={scrollToBottomRef} mode={"standalone"} - fontSize={font_size} + fontSize={fontSize} search={search} filterRecentH={filterRecentH} selectedHashtags={selectedHashtags} @@ -304,7 +305,7 @@ export function ChatRoom({ }} > ; project_id?: string; // improves relative links if given path?: string; - font_size: number; + font_size?: number; is_prev_sender?: boolean; show_avatar?: boolean; mode: Mode; @@ -395,7 +398,7 @@ export default function Message({ marginTop = "5px"; } - const message_style: CSSProperties = { + const messageStyle: CSSProperties = { color, background, wordWrap: "break-word", @@ -418,7 +421,7 @@ export default function Message({ const showEditButton = Date.now() - date < SHOW_EDIT_BUTTON_MS; const feedback = message.getIn(["feedback", account_id]); const otherFeedback = - isLLMThread && msgWrittenByLLM ? 0 : (message.get("feedback")?.size ?? 0); + isLLMThread && msgWrittenByLLM ? 0 : message.get("feedback")?.size ?? 0; const showOtherFeedback = otherFeedback > 0; const editControlRow = () => { @@ -563,7 +566,7 @@ export default function Message({ ) : undefined}
@@ -844,7 +847,7 @@ export default function Message({ return THREAD_STYLE_TOP; } } else { - return TRHEAD_STYLE_SINGLE; + return THREAD_STYLE_SINGLE; } } else if (allowReply) { return THREAD_STYLE_BOTTOM; @@ -945,7 +948,7 @@ export default function Message({ ); } - function getThreadfoldOrBlank() { + function getThreadFoldOrBlank() { const xs = 2; if (is_thread_body || (!is_thread_body && !is_thread)) { return BLANK_COLUMN(xs); @@ -964,7 +967,7 @@ export default function Message({ width: "100%", textAlign: "center", }; - const iconname = is_folded + const iconName = is_folded ? mode === "standalone" ? reverseRowOrdering ? "right-circle-o" @@ -978,7 +981,7 @@ export default function Message({ onClick={() => actions?.toggleFoldThread(message.get("date"), index)} icon={ } @@ -1027,14 +1030,14 @@ export default function Message({ switch (mode) { case "standalone": - const cols = [avatar_column(), contentColumn(), getThreadfoldOrBlank()]; + const cols = [avatar_column(), contentColumn(), getThreadFoldOrBlank()]; if (reverseRowOrdering) { cols.reverse(); } return cols; case "sidechat": - return [getThreadfoldOrBlank(), contentColumn()]; + return [getThreadFoldOrBlank(), contentColumn()]; default: unreachable(mode); diff --git a/src/packages/frontend/chat/types.ts b/src/packages/frontend/chat/types.ts index 12b3a6ad04e..a52543896cf 100644 --- a/src/packages/frontend/chat/types.ts +++ b/src/packages/frontend/chat/types.ts @@ -83,3 +83,7 @@ export type SubmitMentionsFn = ( ) => string; export type SubmitMentionsRef = MutableRefObject; + +export type NumChildren = { [date: number]: number }; + +export type CostEstimate = { min: number; max: number } | null; diff --git a/src/packages/frontend/chat/viewer.tsx b/src/packages/frontend/chat/viewer.tsx index 06c21e0f2cf..1a7e7e9d040 100644 --- a/src/packages/frontend/chat/viewer.tsx +++ b/src/packages/frontend/chat/viewer.tsx @@ -2,17 +2,19 @@ Used for viewing a list of messages, e.g., in timetravel. */ -import { MessageList, getSortedDates } from "./chat-log"; -import { useTypedRedux } from "@cocalc/frontend/app-framework"; -import { useMemo } from "react"; import { Map as immutableMap } from "immutable"; +import { useMemo } from "react"; + +import type { Document } from "@cocalc/sync/editor/generic/types"; +import { useTypedRedux } from "@cocalc/frontend/app-framework"; +import { MessageList, getSortedDates } from "./chat-log"; import type { ChatMessages } from "./types"; export default function Viewer({ doc, font_size, }: { - doc; + doc: Document; font_size?: number; }) { const messages = useMemo(() => { diff --git a/src/packages/frontend/cspell.json b/src/packages/frontend/cspell.json index 8a656694149..6c785870a48 100644 --- a/src/packages/frontend/cspell.json +++ b/src/packages/frontend/cspell.json @@ -35,7 +35,9 @@ "rtypes", "rclass", "ipython", - "Miniterm" + "Miniterm", + "syncdoc", + "synctables" ], "ignoreWords": [ "LLMs", @@ -55,7 +57,8 @@ "mintime", "PoweroffOutlined", "immutablejs", - "reuseinflight" + "reuseinflight", + "sidechat" ], "flagWords": [], "ignorePaths": ["node_modules/**", "dist/**", "dist-ts/**", "build/**"], diff --git a/src/packages/frontend/frame-editors/chat-editor/editor.ts b/src/packages/frontend/frame-editors/chat-editor/editor.ts index f7ca86b114a..8297003495c 100644 --- a/src/packages/frontend/frame-editors/chat-editor/editor.ts +++ b/src/packages/frontend/frame-editors/chat-editor/editor.ts @@ -8,12 +8,13 @@ Top-level react component for editing chat */ import { createElement } from "react"; + import { ChatRoom } from "@cocalc/frontend/chat/chatroom"; -import { set } from "@cocalc/util/misc"; import { createEditor } from "@cocalc/frontend/frame-editors/frame-tree/editor"; import type { EditorDescription } from "@cocalc/frontend/frame-editors/frame-tree/types"; import { terminal } from "@cocalc/frontend/frame-editors/terminal-editor/editor"; import { time_travel } from "@cocalc/frontend/frame-editors/time-travel-editor/editor"; +import { set } from "@cocalc/util/misc"; import { search } from "./search"; const chatroom: EditorDescription = { diff --git a/src/packages/frontend/frame-editors/code-editor/actions.ts b/src/packages/frontend/frame-editors/code-editor/actions.ts index 3ad502eba7c..52b81aec8c6 100644 --- a/src/packages/frontend/frame-editors/code-editor/actions.ts +++ b/src/packages/frontend/frame-editors/code-editor/actions.ts @@ -1134,7 +1134,7 @@ export class Actions< // need to check since this can get called by the close. if (!this._syncstring) return; // TODO: for now, just for the one syncstring obviously - // TOOD: this is probably naive and slow too... + // TODO: this is probably naive and slow too... let cursors: Map>> = Map(); this._syncstring .get_cursors({ @@ -1157,7 +1157,7 @@ export class Actions< } // Set the location of all of OUR cursors. This serves many purposes: - // -- propogate to other users via the syncstring. + // -- propagate to other users via the syncstring. // -- setting uri fragment page= at top of browser, so URL link is useful // -- don't delete trailing whitespace in lines with cursors set_cursor_locs(locs: any[]): void { diff --git a/src/packages/frontend/frame-editors/time-travel-editor/viewer.tsx b/src/packages/frontend/frame-editors/time-travel-editor/viewer.tsx index ab453debf3c..02cdaf3b169 100644 --- a/src/packages/frontend/frame-editors/time-travel-editor/viewer.tsx +++ b/src/packages/frontend/frame-editors/time-travel-editor/viewer.tsx @@ -2,16 +2,16 @@ Render a document, where the rendering is determined by the file extension */ -import type { Document } from "@cocalc/sync/editor/generic/types"; +import ChatViewer from "@cocalc/frontend/chat/viewer"; import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown"; +import { TasksHistoryViewer } from "@cocalc/frontend/editors/task-editor/history-viewer"; +import { getScale } from "@cocalc/frontend/frame-editors/frame-tree/hooks"; +import Whiteboard from "@cocalc/frontend/frame-editors/whiteboard-editor/time-travel"; +import { HistoryViewer as JupyterHistoryViewer } from "@cocalc/frontend/jupyter/history-viewer"; +import type { Document } from "@cocalc/sync/editor/generic/types"; import { TextDocument } from "./document"; -import { TasksHistoryViewer } from "../../editors/task-editor/history-viewer"; -import { HistoryViewer as JupyterHistoryViewer } from "../../jupyter/history-viewer"; import { SagewsCodemirror } from "./sagews-codemirror"; -import Whiteboard from "@cocalc/frontend/frame-editors/whiteboard-editor/time-travel"; import { isObjectDoc } from "./view-document"; -import { getScale } from "@cocalc/frontend/frame-editors/frame-tree/hooks"; -import ChatViewer from "@cocalc/frontend/chat/viewer"; export const HAS_SPECIAL_VIEWER = new Set([ "tasks", From 15cfcf5673edbe72d714174f90ab2ed0d1f7e198 Mon Sep 17 00:00:00 2001 From: Harald Schilly Date: Fri, 9 May 2025 15:49:59 +0200 Subject: [PATCH 2/7] frontend/chat: fold thread in bottom thread row, some cleanups --- src/packages/frontend/chat/filter-messages.ts | 7 +-- src/packages/frontend/chat/message.tsx | 50 ++++++++++++------- src/packages/frontend/chat/utils.ts | 8 +-- src/packages/frontend/components/icon.tsx | 2 + src/packages/frontend/cspell.json | 2 +- .../editors/slate/mostly-static-markdown.tsx | 4 +- 6 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/packages/frontend/chat/filter-messages.ts b/src/packages/frontend/chat/filter-messages.ts index 855ca44b6e9..578d1cb68d1 100644 --- a/src/packages/frontend/chat/filter-messages.ts +++ b/src/packages/frontend/chat/filter-messages.ts @@ -6,13 +6,14 @@ NOTE: chat uses every imaginable way to store a timestamp at once, which is the may source of weirdness in the code below... Beware. */ -import type { ChatMessages, ChatMessageTyped, MessageHistory } from "./types"; -import { search_match, search_split } from "@cocalc/util/misc"; import { List } from "immutable"; +import LRU from "lru-cache"; + import type { TypedMap } from "@cocalc/frontend/app-framework"; import { redux } from "@cocalc/frontend/app-framework"; import { webapp_client } from "@cocalc/frontend/webapp-client"; -import LRU from "lru-cache"; +import { search_match, search_split } from "@cocalc/util/misc"; +import type { ChatMessages, ChatMessageTyped, MessageHistory } from "./types"; export function filterMessages({ messages, diff --git a/src/packages/frontend/chat/message.tsx b/src/packages/frontend/chat/message.tsx index b8e072eaa1d..0eb60b10ed3 100644 --- a/src/packages/frontend/chat/message.tsx +++ b/src/packages/frontend/chat/message.tsx @@ -41,6 +41,7 @@ import { Name } from "./name"; import { Time } from "./time"; import { ChatMessageTyped, Mode, SubmitMentionsFn } from "./types"; import { + getReplyToRoot, getThreadRootDate, is_editing, message_colors, @@ -384,19 +385,15 @@ export default function Message({ } function contentColumn() { - let marginTop; - let value = newest_content(message); + const value = newest_content(message); const { background, color, lighten, message_class } = message_colors( account_id, message, ); - if (!is_prev_sender && is_viewers_message) { - marginTop = MARGIN_TOP_VIEWER; - } else { - marginTop = "5px"; - } + const marginTop = + !is_prev_sender && is_viewers_message ? MARGIN_TOP_VIEWER : "5px"; const messageStyle: CSSProperties = { color, @@ -911,6 +908,21 @@ export default function Message({ {showAISummarize && is_thread ? ( ) : undefined} + {is_thread ? ( + + ) : undefined}
); } @@ -920,16 +932,13 @@ export default function Message({ return; } - let label; - if (numChildren) { - label = ( - <> - {numChildren} {plural(numChildren, "Reply", "Replies")} - - ); - } else { - label = "View Replies"; - } + const label = numChildren ? ( + <> + Show {numChildren} {plural(numChildren, "Reply", "Replies")}… + + ) : ( + "View Replies…" + ); return ( @@ -940,6 +949,7 @@ export default function Message({ } type="link" style={{ color: "darkblue" }} + icon={} > {label} @@ -956,17 +966,18 @@ export default function Message({ const style: CSS = mode === "standalone" ? { - color: "#666", + color: COLORS.GRAY_M, marginTop: MARGIN_TOP_VIEWER, marginLeft: "5px", marginRight: "5px", } : { - color: "#666", + color: COLORS.GRAY_M, marginTop: "5px", width: "100%", textAlign: "center", }; + const iconName = is_folded ? mode === "standalone" ? reverseRowOrdering @@ -974,6 +985,7 @@ export default function Message({ : "left-circle-o" : "right-circle-o" : "down-circle-o"; + const button = ( + + ) : undefined} + {showDeleteButton && ( + + { + actions?.setEditing(message, true); + setTimeout(() => actions?.sendEdit(message, ""), 1); + }} + > + + + + )} + {showEditingStatus && render_editing_status(isEditing)} + {showHistory && ( + + )} + {showLLMFeedback && ( + <> + + + + )} + + + ); + } + function contentColumn() { const value = newest_content(message); @@ -414,129 +533,11 @@ export default function Message({ } as const; const mainXS = mode === "standalone" ? 20 : 22; - const showEditButton = Date.now() - date < SHOW_EDIT_BUTTON_MS; const feedback = message.getIn(["feedback", account_id]); const otherFeedback = isLLMThread && msgWrittenByLLM ? 0 : message.get("feedback")?.size ?? 0; const showOtherFeedback = otherFeedback > 0; - const editControlRow = () => { - if (isEditing) { - return null; - } - const showDeleteButton = - DELETE_BUTTON && newest_content(message).trim().length > 0; - const showEditingStatus = - (message.get("history")?.size ?? 0) > 1 || - (message.get("editing")?.size ?? 0) > 0; - const showHistory = (message.get("history")?.size ?? 0) > 1; - const showLLMFeedback = isLLMThread && msgWrittenByLLM; - - // Show the bottom line of the message -- this uses a LOT of extra - // vertical space, so only do it if there is a good reason to. - // Getting rid of this might be nice. - const show = - showEditButton || - showDeleteButton || - showEditingStatus || - showHistory || - showLLMFeedback; - if (!show) { - // important to explicitly check this before rendering below, since otherwise we get a big BLANK space. - return null; - } - - return ( -
- - {showEditButton ? ( - - Edit this message. You can edit any past message at - any time by double clicking on it. Fix other people's typos. - All versions are stored. - - } - placement="left" - > - - - ) : undefined} - {showDeleteButton && ( - - { - actions?.setEditing(message, true); - setTimeout(() => actions?.sendEdit(message, ""), 1); - }} - > - - - - )} - {showEditingStatus && editing_status(isEditing)} - {showHistory && ( - - )} - {showLLMFeedback && ( - <> - - - - )} - -
- ); - }; - return (
)} - {isEditing && renderEditMessage()} - {editControlRow()} + {isEditing ? renderEditMessage() : renderEditControlRow()}
{show_history && (
From 4d8aa8e99f7f205e16e506e2bde8ec672092bb2d Mon Sep 17 00:00:00 2001 From: Harald Schilly Date: Tue, 13 May 2025 13:02:05 +0200 Subject: [PATCH 4/7] frontend/chat/message: further refactoring and using "Tip" component --- .../frontend/chat/llm-msg-summarize.tsx | 13 +- src/packages/frontend/chat/message.tsx | 274 ++++++++++-------- src/packages/frontend/components/tip.tsx | 8 +- 3 files changed, 162 insertions(+), 133 deletions(-) diff --git a/src/packages/frontend/chat/llm-msg-summarize.tsx b/src/packages/frontend/chat/llm-msg-summarize.tsx index bb602e781e4..17cbf6d3a17 100644 --- a/src/packages/frontend/chat/llm-msg-summarize.tsx +++ b/src/packages/frontend/chat/llm-msg-summarize.tsx @@ -7,7 +7,7 @@ import { Button, Collapse, Switch } from "antd"; import { useLanguageModelSetting } from "@cocalc/frontend/account/useLanguageModelSetting"; import { useAsyncEffect, useState } from "@cocalc/frontend/app-framework"; -import { Icon, Paragraph, RawPrompt } from "@cocalc/frontend/components"; +import { Icon, Paragraph, RawPrompt, Tip } from "@cocalc/frontend/components"; import AIAvatar from "@cocalc/frontend/components/ai-avatar"; import PopconfirmKeyboard from "@cocalc/frontend/components/popconfirm-keyboard"; import LLMSelector, { @@ -111,9 +111,14 @@ export function SummarizeThread({ onConfirm={() => actions?.summarizeThread({ model, reply_to, short })} okText="Summarize" > - + + + ); } diff --git a/src/packages/frontend/chat/message.tsx b/src/packages/frontend/chat/message.tsx index a6a606d595a..d70a796099e 100644 --- a/src/packages/frontend/chat/message.tsx +++ b/src/packages/frontend/chat/message.tsx @@ -5,7 +5,7 @@ // cSpell:ignore blankcolumn -import { Badge, Button, Col, Popconfirm, Row, Space, Tooltip } from "antd"; +import { Badge, Button, Col, Popconfirm, Row, Space } from "antd"; import { List, Map } from "immutable"; import { CSSProperties, useEffect, useLayoutEffect } from "react"; import { useIntl } from "react-intl"; @@ -415,7 +415,7 @@ export default function Message({
{showEditButton ? ( - Edit this message. You can edit any past message at any @@ -436,10 +436,10 @@ export default function Message({ > Edit - + ) : undefined} {showDeleteButton && ( - @@ -462,7 +462,7 @@ export default function Message({ Delete - + )} {showEditingStatus && render_editing_status(isEditing)} {showHistory && ( @@ -502,9 +502,117 @@ export default function Message({ ); } - function contentColumn() { + function renderMessageBody({ lighten, message_class }) { const value = newest_content(message); + const feedback = message.getIn(["feedback", account_id]); + const otherFeedback = + isLLMThread && msgWrittenByLLM ? 0 : message.get("feedback")?.size ?? 0; + const showOtherFeedback = otherFeedback > 0; + + return ( + <> + + + + actions?.setHashtagState( + tag, + selectedHashtags?.has(tag) ? undefined : 1, + ) + : undefined + } + /> + + ); + } + + function contentColumn() { const { background, color, lighten, message_class } = message_colors( account_id, message, @@ -533,10 +641,6 @@ export default function Message({ } as const; const mainXS = mode === "standalone" ? 20 : 22; - const feedback = message.getIn(["feedback", account_id]); - const otherFeedback = - isLLMThread && msgWrittenByLLM ? 0 : message.get("feedback")?.size ?? 0; - const showOtherFeedback = otherFeedback > 0; return ( @@ -567,106 +671,14 @@ export default function Message({ className="smc-chat-message" onDoubleClick={edit_message} > - {!isEditing && ( - - - )} - {!isEditing && ( - - actions?.setHashtagState( - tag, - selectedHashtags?.has(tag) ? undefined : 1, - ) - : undefined - } - /> + {isEditing ? ( + renderEditMessage() + ) : ( + <> + {renderMessageBody({ lighten, message_class })} + {renderEditControlRow()} + )} - {isEditing ? renderEditMessage() : renderEditControlRow()}
{show_history && (
@@ -876,7 +888,8 @@ export default function Message({ return (
- ) : undefined} - + {showAISummarize && is_thread ? ( ) : undefined} - {is_thread ? ( - - ) : undefined} + + + )}
); } @@ -998,6 +1018,7 @@ export default function Message({ } /> ); + return ( @@ -1026,7 +1048,7 @@ export default function Message({ } > {button} - + )} ); diff --git a/src/packages/frontend/components/tip.tsx b/src/packages/frontend/components/tip.tsx index a12c2d425b2..f081b7ff052 100644 --- a/src/packages/frontend/components/tip.tsx +++ b/src/packages/frontend/components/tip.tsx @@ -21,7 +21,7 @@ type Size = "xsmall" | "small" | "medium" | "large"; type Trigger = "hover" | "focus" | "click" | "contextMenu"; interface Props { - title?: string | JSX.Element | JSX.Element[]; // not checked for update + title?: string | JSX.Element | JSX.Element[] | (() => JSX.Element); // not checked for update placement?: TooltipPlacement; tip?: string | JSX.Element | JSX.Element[]; // not checked for update size?: Size; // IMPORTANT: this is currently ignored -- see https://github.com/sagemathinc/cocalc/pull/4155 @@ -75,10 +75,12 @@ export const Tip: React.FC = React.memo((props: Props) => { } = props; function render_title() { - if (!icon) return title; + const renderedTitle = typeof title === "function" ? title() : title; + if (!renderedTitle) return null; + if (!icon) return renderedTitle; return ( - {title} + {renderedTitle} ); } From a6bfe454f8e439ba23e3c1765edfe9b4ec23a563 Mon Sep 17 00:00:00 2001 From: Harald Schilly Date: Tue, 13 May 2025 15:24:26 +0200 Subject: [PATCH 5/7] frontend/chat/message: progress cleaning up top buttons inside of message --- src/packages/frontend/chat/message.tsx | 177 +++++++++++++------------ 1 file changed, 91 insertions(+), 86 deletions(-) diff --git a/src/packages/frontend/chat/message.tsx b/src/packages/frontend/chat/message.tsx index d70a796099e..bb37b57afd5 100644 --- a/src/packages/frontend/chat/message.tsx +++ b/src/packages/frontend/chat/message.tsx @@ -514,84 +514,87 @@ export default function Message({ <> + {renderEditControlRow()} ); } @@ -636,8 +640,6 @@ export default function Message({ ? { marginLeft: "5px", marginRight: "5px" } : undefined), ...(selected ? { border: "3px solid #66bb6a" } : undefined), - maxHeight: is_folded ? "100px" : undefined, - overflowY: is_folded ? "auto" : undefined, } as const; const mainXS = mode === "standalone" ? 20 : 22; @@ -671,27 +673,27 @@ export default function Message({ className="smc-chat-message" onDoubleClick={edit_message} > - {isEditing ? ( - renderEditMessage() - ) : ( - <> - {renderMessageBody({ lighten, message_class })} - {renderEditControlRow()} - - )} + {isEditing + ? renderEditMessage() + : renderMessageBody({ lighten, message_class })}
- {show_history && ( -
- - - -
- )} - {replying ? renderComposeReply() : undefined} + {renderHistory()} + {renderComposeReply()} ); } + function renderHistory() { + if (!show_history) return; + return ( +
+ + + +
+ ); + } + function saveEditedMessage(): void { if (actions == null) return; const mesg = @@ -768,11 +770,14 @@ export default function Message({ } function renderComposeReply() { + if (!replying) return; + if (project_id == null || path == null || actions?.syncdb == null) { // should never get into this position // when null. return; } + const replyDate = -getThreadRootDate({ date, messages }); let input; let moveCursorToEndOfLine = false; From 1d0b67b897daaf9ae1cefaf143937d59d4b21cb5 Mon Sep 17 00:00:00 2001 From: Harald Schilly Date: Tue, 13 May 2025 15:52:15 +0200 Subject: [PATCH 6/7] frontend/chat: remove all messages of thread of folded --- src/packages/frontend/chat/message.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/packages/frontend/chat/message.tsx b/src/packages/frontend/chat/message.tsx index bb37b57afd5..71749c7d143 100644 --- a/src/packages/frontend/chat/message.tsx +++ b/src/packages/frontend/chat/message.tsx @@ -617,6 +617,8 @@ export default function Message({ } function contentColumn() { + const mainXS = mode === "standalone" ? 20 : 22; + const { background, color, lighten, message_class } = message_colors( account_id, message, @@ -642,8 +644,6 @@ export default function Message({ ...(selected ? { border: "3px solid #66bb6a" } : undefined), } as const; - const mainXS = mode === "standalone" ? 20 : 22; - return (
} onClick={() => actions?.toggleFoldThread( new Date(getThreadRootDate({ date, messages })), @@ -943,7 +942,7 @@ export default function Message({ ) } > - Fold… + Fold… )} @@ -958,26 +957,27 @@ export default function Message({ const label = numChildren ? ( <> - Show {numChildren} {plural(numChildren, "Reply", "Replies")}… + Show {numChildren + 1} {plural(numChildren + 1, "Message", "Messages")}… ) : ( - "View Replies…" + "View Messages…" ); return ( - -
+ + -
+ ); } @@ -1062,7 +1062,7 @@ export default function Message({ function renderCols(): JSX.Element[] | JSX.Element { // these columns should be filtered in the first place, this here is just an extra check - if (is_thread && is_folded && is_thread_body) { + if (is_folded || (is_thread && is_folded && is_thread_body)) { return <>; } From ca3feb963b960059d6aa869e216c2f5fb76726e9 Mon Sep 17 00:00:00 2001 From: Harald Schilly Date: Tue, 13 May 2025 16:58:26 +0200 Subject: [PATCH 7/7] frontend/chat: add a "fold all LLM threads" button --- src/packages/frontend/chat/actions.ts | 28 +++++++++++++++++-- src/packages/frontend/chat/chatroom.tsx | 2 ++ src/packages/frontend/chat/fold-threads.tsx | 25 +++++++++++++++++ src/packages/frontend/chat/message.tsx | 2 +- src/packages/frontend/chat/side-chat.tsx | 9 ++++-- .../frame-tree/commands/editor-menus.ts | 2 +- 6 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 src/packages/frontend/chat/fold-threads.tsx diff --git a/src/packages/frontend/chat/actions.ts b/src/packages/frontend/chat/actions.ts index 5921686c703..d921cfa7304 100644 --- a/src/packages/frontend/chat/actions.ts +++ b/src/packages/frontend/chat/actions.ts @@ -90,9 +90,7 @@ export class ChatActions extends Actions { }; toggleFoldThread = (reply_to: Date, messageIndex?: number) => { - if (this.syncdb == null) { - return; - } + if (this.syncdb == null) return; const account_id = this.redux.getStore("account").get_account_id(); const cur = this.syncdb.get_one({ event: "chat", date: reply_to }); const folding = cur?.get("folding") ?? List([]); @@ -113,6 +111,29 @@ export class ChatActions extends Actions { } }; + foldAllThreads = (onlyLLM = true) => { + if (this.syncdb == null || this.store == null) return; + const messages = this.store.get("messages"); + if (messages == null) return; + const account_id = this.redux.getStore("account").get_account_id(); + for (const [_timestamp, message] of messages) { + // ignore replies + if (message.get("reply_to") != null) continue; + const date = message.get("date"); + if (!(date instanceof Date)) continue; + const isLLMThread = this.isLanguageModelThread(date) !== false; + if (onlyLLM && !isLLMThread) continue; + const folding = message?.get("folding") ?? List([]); + const folded = folding.includes(account_id); + if (!folded) { + this.syncdb.set({ + folding: folding.push(account_id), + date, + }); + } + } + }; + feedback = (message: ChatMessageTyped, feedback: Feedback | null) => { if (this.syncdb == null) return; const date = message.get("date"); @@ -479,6 +500,7 @@ export class ChatActions extends Actions { scrollToDate: null, }); }; + scrollToIndex = (index: number = -1) => { if (this.syncdb == null) return; // we first clear, then set it, since scroll to needs to diff --git a/src/packages/frontend/chat/chatroom.tsx b/src/packages/frontend/chat/chatroom.tsx index 36b8bf65087..4212e4a0872 100644 --- a/src/packages/frontend/chat/chatroom.tsx +++ b/src/packages/frontend/chat/chatroom.tsx @@ -23,6 +23,7 @@ import { hoursToTimeIntervalHuman } from "@cocalc/util/misc"; import type { ChatActions } from "./actions"; import { ChatLog } from "./chat-log"; import Filter from "./filter"; +import { FoldAllThreads } from "./fold-threads"; import ChatInput from "./input"; import { LLMCostEstimationChat } from "./llm-cost-estimation"; import type { ChatState } from "./store"; @@ -262,6 +263,7 @@ export function ChatRoom({ {render_video_chat_button()} + ); } diff --git a/src/packages/frontend/chat/fold-threads.tsx b/src/packages/frontend/chat/fold-threads.tsx new file mode 100644 index 00000000000..a4030c00557 --- /dev/null +++ b/src/packages/frontend/chat/fold-threads.tsx @@ -0,0 +1,25 @@ +import { Button } from "antd"; + +import { ChatActions } from "@cocalc/frontend/chat/actions"; +import { Icon, Tip } from "@cocalc/frontend/components"; + +export function FoldAllThreads({ + actions, + shortLabel, +}: { + actions: ChatActions; + shortLabel: boolean; +}) { + return ( + + + + ); +} diff --git a/src/packages/frontend/chat/message.tsx b/src/packages/frontend/chat/message.tsx index 71749c7d143..22b718b22e0 100644 --- a/src/packages/frontend/chat/message.tsx +++ b/src/packages/frontend/chat/message.tsx @@ -964,7 +964,7 @@ export default function Message({ ); return ( - + +