Skip to content

Commit b98a452

Browse files
committed
frontend/llm history: search box, dstream, styling
1 parent de65a0c commit b98a452

File tree

7 files changed

+188
-200
lines changed

7 files changed

+188
-200
lines changed

src/CLAUDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ This file provides guidance to Claude Code (claude.ai/code) and also Gemini CLI
1616
- Run `pretter -w [filename]` after modifying a file (ts, tsx, md, json, ...) to format it correctly.
1717
- All .js and .ts files are formatted by the tool prettier
1818
- Add suitable types when you write code
19-
- Variable name styles are "camelCase" for local and "FOO_BAR" for global variables. If you edit older code not following these guidlines, adjust this rule to fit the files style.
19+
- Follow DRY principles!
20+
- Variable name styles are `camelCase` for local and `FOO_BAR` for global variables. React Components and Classes are `FooBar`. If you edit older code not following these guidlines, adjust this rule to fit the files style.
2021
- Some older code is JavaScript or CoffeeScript, which will be translated to TypeScript
2122
- Use ES modules (import/export) syntax, not CommonJS (require)
2223
- Organize the list of imports in such a way: installed npm packages are on top, newline, then are imports from @cocalc's code base. Sorted alphabetically.
@@ -28,7 +29,7 @@ This file provides guidance to Claude Code (claude.ai/code) and also Gemini CLI
2829
### Essential Commands
2930

3031
- `pnpm build-dev` - Build all packages for development
31-
- `pnpm clean` - Clean all node_modules and dist directories
32+
- `pnpm clean` - Clean all `node_modules` and `dist` directories
3233
- `pnpm test` - Run full test suite
3334
- `pnpm depcheck` - Check for dependency issues
3435
- `prettier -w [filename]` to format the style of a file after editing it
@@ -38,7 +39,6 @@ This file provides guidance to Claude Code (claude.ai/code) and also Gemini CLI
3839

3940
- `cd packages/[package] && pnpm build` - Build and compile a specific package
4041
- for packages/next and packages/static, run `cd packages/[package] && pnpm build-dev`
41-
- `cd packages/[package] && pnpm tsc:watch` - TypeScript compilation in watch mode for a specific package
4242
- `cd packages/[package] && pnpm test` - Run tests for a specific package
4343
- `cd packages/[package] && pnpm build` - Build a specific package
4444
- To typecheck the frontend, it is best to run `cd packages/static && pnpm build` - this implicitly compiles the frontend and reports typescript errors
@@ -48,6 +48,7 @@ This file provides guidance to Claude Code (claude.ai/code) and also Gemini CLI
4848

4949
- **IMPORTANT**: Always run `prettier -w [filename]` immediately after editing any .ts, .tsx, .md, or .json file to ensure consistent styling
5050
- After TypeScript or `*.tsx` changes, run `pnpm build` in the relevant package directory
51+
- When editing the frontend, run `pnpm build-dev` in `packages/static`. This implicitly builds the frontend!
5152

5253
## Architecture Overview
5354

src/packages/frontend/codemirror/extensions/ai-formula.tsx

Lines changed: 8 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,7 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6-
import type { MenuProps } from "antd";
7-
import {
8-
Button,
9-
Descriptions,
10-
Divider,
11-
Dropdown,
12-
Input,
13-
Modal,
14-
Select,
15-
Space,
16-
Tooltip,
17-
} from "antd";
6+
import { Button, Descriptions, Divider, Input, Modal, Space } from "antd";
187
import { debounce } from "lodash";
198
import { FormattedMessage, useIntl } from "react-intl";
209

@@ -38,10 +27,8 @@ import {
3827
} from "@cocalc/frontend/components";
3928
import AIAvatar from "@cocalc/frontend/components/ai-avatar";
4029
import { LLMModelName } from "@cocalc/frontend/components/llm-name";
41-
import {
42-
MAX_PROMPTS,
43-
useLLMHistory,
44-
} from "@cocalc/frontend/frame-editors/llm/use-llm-history";
30+
import { useLLMHistory } from "@cocalc/frontend/frame-editors/llm/use-llm-history";
31+
import { LLMHistorySelector } from "@cocalc/frontend/frame-editors/llm/llm-history-selector";
4532
import LLMSelector from "@cocalc/frontend/frame-editors/llm/llm-selector";
4633
import { dialogs, labels } from "@cocalc/frontend/i18n";
4734
import { show_react_modal } from "@cocalc/frontend/misc";
@@ -367,43 +354,11 @@ function AiGenFormula({ mode, text = "", project_id, locale, cb }: Props) {
367354
onPressEnter={doGenerate}
368355
addonBefore={<Icon name="fx" />}
369356
/>
370-
{historyPrompts.length > 0 && (
371-
<Select
372-
style={{ width: 200 }}
373-
placeholder="Search history..."
374-
showSearch
375-
allowClear
376-
optionFilterProp="children"
377-
filterOption={(input, option) =>
378-
(option?.label ?? "")
379-
.toString()
380-
.toLowerCase()
381-
.includes(input.toLowerCase())
382-
}
383-
onSelect={(value) => setInput(value)}
384-
disabled={generating}
385-
dropdownStyle={{ maxHeight: 400, overflow: "auto" }}
386-
suffixIcon={<Icon name="history" />}
387-
options={historyPrompts.slice(0, MAX_PROMPTS).map((prompt, idx) => ({
388-
key: idx.toString(),
389-
value: prompt,
390-
label: (
391-
<Tooltip title={prompt} placement="left">
392-
<div
393-
style={{
394-
maxWidth: "300px",
395-
whiteSpace: "nowrap",
396-
overflow: "hidden",
397-
textOverflow: "ellipsis",
398-
}}
399-
>
400-
{prompt}
401-
</div>
402-
</Tooltip>
403-
),
404-
}))}
405-
/>
406-
)}
357+
<LLMHistorySelector
358+
prompts={historyPrompts}
359+
onSelect={setInput}
360+
disabled={generating}
361+
/>
407362
<Button
408363
disabled={!input.trim() || generating}
409364
loading={generating}
@@ -513,7 +468,6 @@ function AiGenFormula({ mode, text = "", project_id, locale, cb }: Props) {
513468
open
514469
footer={renderButtons()}
515470
onCancel={onCancel}
516-
centered
517471
width={{ xs: "90vw", sm: "90vw", md: "80vw", lg: "70vw", xl: "60vw" }}
518472
>
519473
{renderBody()}

src/packages/frontend/frame-editors/llm/llm-assistant-button.tsx

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
Input,
2020
Popover,
2121
Radio,
22-
Select,
2322
Space,
2423
Tooltip,
2524
} from "antd";
@@ -57,7 +56,8 @@ import { Actions } from "../code-editor/actions";
5756
import { AI_ASSIST_TAG } from "./consts";
5857
import Context from "./context";
5958
import { Options, createChatMessage } from "./create-chat";
60-
import { MAX_PROMPTS, useLLMHistory } from "./use-llm-history";
59+
import { useLLMHistory } from "./use-llm-history";
60+
import { LLMHistorySelector } from "./llm-history-selector";
6161
import LLMSelector, { modelToName } from "./llm-selector";
6262
import TitleBarButtonTour from "./llm-assistant-tour";
6363

@@ -670,48 +670,12 @@ export default function LanguageModelTitleBarButton({
670670
}}
671671
autoSize={{ minRows: 2, maxRows: 10 }}
672672
/>
673-
{historyPrompts.length > 0 && (
674-
<Select
675-
style={{
676-
width: 200,
677-
alignSelf: "stretch",
678-
}}
679-
placeholder="Search history..."
680-
showSearch
681-
allowClear
682-
optionFilterProp="children"
683-
filterOption={(input, option) =>
684-
(option?.label ?? "")
685-
.toString()
686-
.toLowerCase()
687-
.includes(input.toLowerCase())
688-
}
689-
onSelect={(value) => setCommand(value)}
690-
disabled={querying}
691-
dropdownStyle={{ maxHeight: 400, overflow: "auto" }}
692-
suffixIcon={<Icon name="history" />}
693-
options={historyPrompts
694-
.slice(0, MAX_PROMPTS)
695-
.map((histPrompt, idx) => ({
696-
key: idx.toString(),
697-
value: histPrompt,
698-
label: (
699-
<Tooltip title={histPrompt} placement="left">
700-
<div
701-
style={{
702-
maxWidth: "300px",
703-
whiteSpace: "nowrap",
704-
overflow: "hidden",
705-
textOverflow: "ellipsis",
706-
}}
707-
>
708-
{histPrompt}
709-
</div>
710-
</Tooltip>
711-
),
712-
}))}
713-
/>
714-
)}
673+
<LLMHistorySelector
674+
prompts={historyPrompts}
675+
onSelect={setCommand}
676+
disabled={querying}
677+
alignSelf="stretch"
678+
/>
715679
</Space.Compact>
716680
</Paragraph>
717681
{renderOptions()}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* This file is part of CoCalc: Copyright © 2025 Sagemath, Inc.
3+
* License: MS-RSL – see LICENSE.md for details
4+
*/
5+
6+
import { CSSProperties, useState } from "react";
7+
import { Button, Dropdown, Input, Menu, MenuProps, Tooltip } from "antd";
8+
9+
import { Icon } from "@cocalc/frontend/components/icon";
10+
import { COLORS } from "@cocalc/util/theme";
11+
12+
interface LLMHistorySelectorProps {
13+
prompts: string[];
14+
onSelect: (value: string) => void;
15+
disabled?: boolean;
16+
style?: CSSProperties;
17+
width?: number;
18+
alignSelf?: "stretch" | "flex-start" | "flex-end" | "center" | "baseline";
19+
}
20+
21+
export function LLMHistorySelector({
22+
prompts,
23+
onSelect,
24+
disabled = false,
25+
style,
26+
width = 350,
27+
alignSelf = "stretch",
28+
}: LLMHistorySelectorProps) {
29+
const [searchText, setSearchText] = useState("");
30+
const [isOpen, setIsOpen] = useState(false);
31+
32+
// Don't render if no prompts
33+
if (prompts.length === 0) {
34+
return null;
35+
}
36+
37+
const defaultStyle: CSSProperties = {
38+
height: "auto",
39+
alignSelf,
40+
...style,
41+
};
42+
43+
// Filter prompts based on search text
44+
const filteredPrompts = prompts.filter((prompt) =>
45+
prompt.toLowerCase().includes(searchText.toLowerCase()),
46+
);
47+
48+
const menuItems: MenuProps["items"] = filteredPrompts.map((prompt, idx) => ({
49+
key: idx.toString(),
50+
label: (
51+
<Tooltip title={prompt} placement="left">
52+
<div
53+
style={{
54+
maxWidth: `${width - 50}px`,
55+
whiteSpace: "nowrap",
56+
overflow: "hidden",
57+
textOverflow: "ellipsis",
58+
}}
59+
>
60+
{prompt}
61+
</div>
62+
</Tooltip>
63+
),
64+
onClick: () => {
65+
onSelect(prompt);
66+
setIsOpen(false);
67+
setSearchText("");
68+
},
69+
}));
70+
71+
const overlay = (
72+
<div
73+
style={{
74+
backgroundColor: "white",
75+
border: `1px solid ${COLORS.GRAY_DDD}`,
76+
borderRadius: "6px",
77+
boxShadow: "0 6px 16px 0 rgba(0, 0, 0, 0.08)",
78+
width,
79+
overflowX: "hidden",
80+
}}
81+
>
82+
<div style={{ padding: 8, borderBottom: `1px solid ${COLORS.GRAY_LL}` }}>
83+
<Input
84+
placeholder="Search history..."
85+
allowClear
86+
autoFocus
87+
value={searchText}
88+
onChange={(e) => setSearchText(e.target.value)}
89+
style={{ width: "100%" }}
90+
/>
91+
</div>
92+
<div
93+
style={{
94+
maxHeight: width - 50,
95+
overflowY: "auto",
96+
overflowX: "hidden",
97+
}}
98+
>
99+
{filteredPrompts.length > 0 ? (
100+
<Menu
101+
items={menuItems}
102+
style={{
103+
border: "none",
104+
boxShadow: "none",
105+
maxHeight: "none",
106+
overflow: "visible",
107+
width: "100%",
108+
}}
109+
/>
110+
) : (
111+
<div
112+
style={{ padding: 16, textAlign: "center", color: COLORS.FILE_EXT }}
113+
>
114+
No matching prompts
115+
</div>
116+
)}
117+
</div>
118+
</div>
119+
);
120+
121+
return (
122+
<Dropdown
123+
popupRender={() => overlay}
124+
trigger={["click"]}
125+
open={isOpen}
126+
onOpenChange={(open) => {
127+
setIsOpen(open);
128+
if (!open) {
129+
setSearchText("");
130+
}
131+
}}
132+
disabled={disabled}
133+
placement="bottomRight"
134+
>
135+
<Button
136+
style={defaultStyle}
137+
icon={<Icon name="history" />}
138+
disabled={disabled}
139+
/>
140+
</Dropdown>
141+
);
142+
}

src/packages/frontend/frame-editors/llm/use-llm-history.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ import { webapp_client } from "@cocalc/frontend/webapp-client";
2828
import { CONAT_LLM_HISTORY_KEY } from "@cocalc/util/consts";
2929
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
3030

31-
// Maximum number of prompts to keep in history per type
32-
export const MAX_PROMPTS = 100;
31+
// limit max prompts to keep in history per type
32+
const MAX_PROMPTS_NUM = 1000;
33+
const MAX_PROMPTS_BYTES = 1024 * 1024;
3334

3435
export type LLMHistoryType = "general" | "formula";
3536

@@ -57,8 +58,8 @@ const getDStream = reuseInFlight(async (type: LLMHistoryType) => {
5758
name: `${CONAT_LLM_HISTORY_KEY}-${type}`,
5859
config: {
5960
discard_policy: "old",
60-
max_msgs: MAX_PROMPTS,
61-
max_bytes: 1024 * 1024,
61+
max_msgs: MAX_PROMPTS_NUM,
62+
max_bytes: MAX_PROMPTS_BYTES,
6263
},
6364
});
6465

@@ -118,11 +119,11 @@ export function useLLMHistory(type: LLMHistoryType = "general") {
118119

119120
// Clean up old prompts if we exceed MAX_PROMPTS
120121
const currentLength = stream.length;
121-
if (currentLength > MAX_PROMPTS) {
122+
if (currentLength > MAX_PROMPTS_NUM) {
122123
// Note: dstream doesn't have a built-in way to remove old entries
123124
// but we limit the display to MAX_PROMPTS in the UI
124125
console.warn(
125-
`LLM history has ${currentLength} entries, exceeding MAX_PROMPTS=${MAX_PROMPTS}`,
126+
`LLM history has ${currentLength} entries, exceeding MAX_PROMPTS=${MAX_PROMPTS_NUM}`,
126127
);
127128
}
128129
} catch (err) {
@@ -147,7 +148,7 @@ export function useLLMHistory(type: LLMHistoryType = "general") {
147148
// Reload prompts on error
148149
try {
149150
const stream = await getDStream(type);
150-
const allPrompts = stream.getAll().slice(-MAX_PROMPTS).reverse();
151+
const allPrompts = stream.getAll().slice(-MAX_PROMPTS_NUM).reverse();
151152
setPrompts(allPrompts);
152153
} catch (reloadErr) {
153154
console.warn(

0 commit comments

Comments
 (0)