Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip] fix: better chatbot input & bubble #3404

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@
"@types/react-grid-layout": "^1.3.5",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/codemirror-extensions-langs": "^4.23.5",
"@uiw/codemirror-extensions-mentions": "^4.23.5",
"@uiw/react-codemirror": "^4.23.5",
"@valtown/codemirror-codeium": "^1.1.1",
"@xterm/addon-attach": "^0.11.0",
Expand Down
14 changes: 0 additions & 14 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 19 additions & 4 deletions frontend/src/components/editor/ai/add-cell-with-ai.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import ReactCodeMirror, {
import { Prec } from "@codemirror/state";
import { customPythonLanguageSupport } from "@/core/codemirror/language/python";
import { asURL } from "@/utils/url";
import { mentions } from "@uiw/codemirror-extensions-mentions";
import { useMemo, useState } from "react";
import { datasetTablesAtom } from "@/core/datasets/state";
import { useAtom, useAtomValue } from "jotai";
Expand All @@ -31,7 +30,7 @@ import { sql } from "@codemirror/lang-sql";
import { SQLLanguageAdapter } from "@/core/codemirror/language/sql";
import { atomWithStorage } from "jotai/utils";
import { type ResolvedTheme, useTheme } from "@/theme/useTheme";
import { getAICompletionBody } from "./completion-utils";
import { getAICompletionBody, mentions } from "./completion-utils";

const pythonExtensions = [
customPythonLanguageSupport(),
Expand Down Expand Up @@ -190,6 +189,11 @@ export const AddCellWithAI: React.FC<{
);
};

export interface AdditionalCompletions {
triggerSymbol: string; // Symbol that will trigger autocompletion when text begins with it
completions: Completion[];
}

interface PromptInputProps {
inputRef?: React.RefObject<ReactCodeMirrorRef>;
placeholder?: string;
Expand All @@ -198,6 +202,7 @@ interface PromptInputProps {
onClose: () => void;
onChange: (value: string) => void;
onSubmit: (e: KeyboardEvent | undefined, value: string) => void;
additionalCompletions?: AdditionalCompletions;
theme: ResolvedTheme;
}

Expand All @@ -215,6 +220,7 @@ export const PromptInput = ({
onChange,
onSubmit,
onClose,
additionalCompletions,
theme,
}: PromptInputProps) => {
const handleSubmit = onSubmit;
Expand Down Expand Up @@ -277,8 +283,17 @@ export const PromptInput = ({
}),
);

// Trigger autocompletion for text that begins with @ or
// @ + additional symbols specified
const matchBeforeRegex = additionalCompletions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do 2 separate mentions plugins? One for "@" data and one for "/" prompts; instead of appending the trigger, replace it.

? new RegExp(`[@${additionalCompletions.triggerSymbol}](\\w+)?`)
: /@(\w+)?/;
const allCompletions = additionalCompletions
? [...completions, ...additionalCompletions.completions]
: completions;

return [
mentions(completions),
mentions(matchBeforeRegex, allCompletions),
EditorView.lineWrapping,
minimalSetup(),
Prec.highest(
Expand Down Expand Up @@ -349,7 +364,7 @@ export const PromptInput = ({
},
]),
];
}, [tables, handleSubmit, handleEscape]);
}, [tables, additionalCompletions, handleSubmit, handleEscape]);

return (
<ReactCodeMirror
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/components/editor/ai/completion-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import type { AiCompletionRequest } from "@/core/network/types";
import { store } from "@/core/state/jotai";
import { Logger } from "@/utils/Logger";
import { Maps } from "@/utils/maps";
import {
autocompletion,
type Completion,
type CompletionContext,
} from "@codemirror/autocomplete";
import type { Extension } from "@codemirror/state";

/**
* Gets the request body for the AI completion API.
Expand Down Expand Up @@ -48,3 +54,30 @@ function extractDatasets(input: string): DataTable[] {
.map((name) => existingDatasets.get(name))
.filter(Boolean);
}

/**
* Adapted from @uiw/codemirror-extensions-mentions
* Allows you to specify a custom regex to trigger the autocompletion.
*/
export function mentions(
matchBeforeRegex: RegExp,
data: Completion[] = [],
): Extension {
return autocompletion({
override: [
(context: CompletionContext) => {
const word = context.matchBefore(matchBeforeRegex);
if (!word) {
return null;
}
if (word && word.from === word.to && !context.explicit) {
return null;
}
return {
from: word?.from,
options: [...data],
};
},
],
});
}
12 changes: 12 additions & 0 deletions frontend/src/core/codemirror/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export function isAtEndOfEditor(ev: { state: EditorState }, isVim = false) {
return main.from === docLength && main.to === docLength;
}

export function moveToEndOfEditor(ev: EditorView | undefined) {
if (!ev) {
return;
}
ev.dispatch({
selection: {
anchor: ev.state.doc.length,
head: ev.state.doc.length,
},
});
}

export function isInVimNormalMode(ev: EditorView): boolean {
const vimState = getCM(ev)?.state.vim;
if (!vimState) {
Expand Down
60 changes: 46 additions & 14 deletions frontend/src/plugins/impl/chat/chat-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ import { renderHTML } from "@/plugins/core/RenderHTML";
import { Input } from "@/components/ui/input";
import { PopoverAnchor } from "@radix-ui/react-popover";
import { copyToClipboard } from "@/utils/copy";
import {
type AdditionalCompletions,
PromptInput,
} from "@/components/editor/ai/add-cell-with-ai";
import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { useTheme } from "@/theme/useTheme";
import { moveToEndOfEditor } from "@/core/codemirror/utils";
import type { Completion } from "@codemirror/autocomplete";

interface Props {
prompts: string[];
Expand All @@ -60,18 +68,19 @@ interface Props {
}

export const Chatbot: React.FC<Props> = (props) => {
const inputRef = React.useRef<HTMLInputElement>(null);
const [config, setConfig] = useState<ChatConfig>(props.config);
const [files, setFiles] = useState<FileList | undefined>(undefined);
const fileInputRef = useRef<HTMLInputElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const codeMirrorInputRef = useRef<ReactCodeMirrorRef>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const { theme } = useTheme();

const {
messages,
setMessages,
input,
setInput,
handleInputChange,
handleSubmit,
isLoading,
stop,
Expand Down Expand Up @@ -196,6 +205,17 @@ export const Chatbot: React.FC<Props> = (props) => {
props.allowAttachments.length > 0) ||
props.allowAttachments === true;

const promptCompletions: AdditionalCompletions = {
triggerSymbol: "/",
completions: props.prompts.map(
(prompt): Completion => ({
label: `/${prompt}`,
displayLabel: prompt,
apply: prompt,
}),
),
};

useEffect(() => {
// When the message length changes, scroll to the bottom
scrollContainerRef.current?.scrollTo({
Expand Down Expand Up @@ -242,7 +262,11 @@ export const Chatbot: React.FC<Props> = (props) => {
: "bg-[var(--slate-4)] text-[var(--slate-12)]"
}`}
>
<p>{renderMessage(message)}</p>
<p
className={cn(message.role === "user" && "whitespace-pre-wrap")}
>
{renderMessage(message)}
</p>
</div>
<div className="flex justify-end text-xs gap-2 invisible group-hover:visible">
<button
Expand Down Expand Up @@ -298,6 +322,7 @@ export const Chatbot: React.FC<Props> = (props) => {
experimental_attachments: files,
});
}}
ref={formRef}
className="flex w-full border-t border-[var(--slate-6)] px-2 py-1 items-center"
>
{props.showConfigurationControls && (
Expand All @@ -309,22 +334,29 @@ export const Chatbot: React.FC<Props> = (props) => {
onSelect={(prompt) => {
setInput(prompt);
requestAnimationFrame(() => {
inputRef.current?.focus();
inputRef.current?.setSelectionRange(
prompt.length,
prompt.length,
);
codeMirrorInputRef.current?.view?.focus();
moveToEndOfEditor(codeMirrorInputRef.current?.view);
});
}}
/>
)}
<input
name="prompt"
ref={inputRef}
<PromptInput
className="rounded-sm mr-2"
placeholder="Type your message here, / for prompts"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we change the placeholder of prompts exist or not

value={input}
onChange={handleInputChange}
className="flex w-full outline-none bg-transparent ml-2 text-[var(--slate-12)] mr-2"
placeholder="Type your message..."
inputRef={codeMirrorInputRef}
theme={theme}
onChange={setInput}
onSubmit={(_evt, newValue) => {
if (!newValue.trim()) {
return;
}
formRef.current?.requestSubmit();
}}
onClose={() => {
// no-op
}}
additionalCompletions={promptCompletions}
/>
{files && files.length === 1 && (
<span
Expand Down
Loading