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

Willy/commands #1744

Merged
merged 4 commits into from
Jan 20, 2025
Merged
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
12 changes: 12 additions & 0 deletions backend/chainlit/emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from chainlit.types import (
AskActionResponse,
AskSpec,
CommandDict,
FileDict,
FileReference,
MessagePayload,
Expand Down Expand Up @@ -132,6 +133,10 @@ async def set_chat_settings(self, settings: dict):
"""Stub method to set chat settings."""
pass

async def set_commands(self, commands: List[CommandDict]):
"""Stub method to send the available commands to the UI."""
pass

async def send_window_message(self, data: Any):
"""Stub method to send custom data to the host window."""
pass
Expand Down Expand Up @@ -394,6 +399,13 @@ def send_token(self, id: str, token: str, is_sequence=False, is_input=False):
def set_chat_settings(self, settings: Dict[str, Any]):
self.session.chat_settings = settings

def set_commands(self, commands: List[CommandDict]):
"""Send the available commands to the UI."""
return self.emit(
"set_commands",
commands,
)

def send_window_message(self, data: Any):
"""Send custom data to the host window."""
return self.emit("window_message", data)
7 changes: 7 additions & 0 deletions backend/chainlit/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class MessageBase(ABC):
fail_on_persist_error: bool = False
persisted = False
is_error = False
command: Optional[str] = None
parent_id: Optional[str] = None
language: Optional[str] = None
metadata: Optional[Dict] = None
Expand Down Expand Up @@ -65,6 +66,7 @@ def from_dict(self, _dict: StepDict):
created_at=_dict["createdAt"],
content=_dict["output"],
author=_dict.get("name", config.ui.name),
command=_dict.get("command"),
type=type, # type: ignore
language=_dict.get("language"),
metadata=_dict.get("metadata", {}),
Expand All @@ -76,6 +78,7 @@ def to_dict(self) -> StepDict:
"threadId": self.thread_id,
"parentId": self.parent_id,
"createdAt": self.created_at,
"command": self.command,
"start": self.created_at,
"end": self.created_at,
"output": self.content,
Expand Down Expand Up @@ -216,6 +219,7 @@ def __init__(
tags: Optional[List[str]] = None,
id: Optional[str] = None,
parent_id: Optional[str] = None,
command: Optional[str] = None,
created_at: Union[str, None] = None,
):
time.sleep(0.001)
Expand All @@ -239,6 +243,9 @@ def __init__(
if parent_id:
self.parent_id = str(parent_id)

if command:
self.command = str(command)

if created_at:
self.created_at = created_at

Expand Down
1 change: 1 addition & 0 deletions backend/chainlit/step.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class StepDict(TypedDict, total=False):
id: str
threadId: str
parentId: Optional[str]
command: Optional[str]
streaming: bool
waitForAnswer: Optional[bool]
isError: Optional[bool]
Expand Down
9 changes: 9 additions & 0 deletions backend/chainlit/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ class ChatProfile(DataClassJsonMixin):
FeedbackStrategy = Literal["BINARY"]


class CommandDict(TypedDict):
# The identifier of the command, will be displayed in the UI
id: str
# The description of the command, will be displayed in the UI
description: str
# The lucide icon name
icon: str


class FeedbackDict(TypedDict):
forId: str
id: Optional[str]
Expand Down
24 changes: 24 additions & 0 deletions cypress/e2e/command/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import chainlit as cl

commands = [
{"id": "Picture", "icon": "image", "description": "Use DALL-E"},
{"id": "Search", "icon": "globe", "description": "Find on the web"},
{
"id": "Canvas",
"icon": "pen-line",
"description": "Collaborate on writing and code",
},
]


@cl.on_chat_start
async def start():
await cl.context.emitter.set_commands(commands)


@cl.on_message
async def message(msg: cl.Message):
if msg.command == "Picture":
await cl.context.emitter.set_commands([])

await cl.Message(content=f"Command: {msg.command}").send()
33 changes: 33 additions & 0 deletions cypress/e2e/command/spec.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { runTestServer } from '../../support/testUtils';

describe('Command', () => {
before(() => {
runTestServer();
});

it('should correctly display commands', () => {
cy.get(`#chat-input`).type("/sear")
cy.get(".command-item").should('have.length', 1);
cy.get(".command-item").eq(0).click()

cy.get(`#chat-input`).type("Hello{enter}")

cy.get(".step").should('have.length', 2);
cy.get(".step").eq(0).find(".command-span").should("have.text", "Search")

cy.get("#command-button").should("exist")

cy.get(".step").eq(1).should("have.text", "Command: Search")

cy.get(`#chat-input`).type("/pic")
cy.get(".command-item").should('have.length', 1);
cy.get(".command-item").eq(0).click()

cy.get(`#chat-input`).type("Hello{enter}")

cy.get(".step").should('have.length', 4);
cy.get(".step").eq(2).find(".command-span").should("have.text", "Picture")

cy.get("#command-button").should("not.exist")
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const Attachments = () => {
<div className="absolute -right-2 -top-2">
<Button
size="icon"
className="w-6 h-6 shadow-sm rounded-full border-4 bg-card hover:bg-card text-foreground border-accent"
className="w-6 h-6 shadow-sm rounded-full border-4 bg-card hover:bg-card text-foreground light:border-muted"
onClick={attachment.remove}
>
<X className="!size-3" />
Expand Down
92 changes: 92 additions & 0 deletions frontend/src/components/chat/MessageComposer/CommandButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
Popover,
PopoverContent,
PopoverTrigger
} from '@radix-ui/react-popover';
import { useRecoilValue } from 'recoil';

import { ICommand, commandsState } from '@chainlit/react-client';

import Icon from '@/components/Icon';
import { ToolBox } from '@/components/icons/ToolBox';
import { Button } from '@/components/ui/button';
import {
Command,
CommandGroup,
CommandItem,
CommandList
} from '@/components/ui/command';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip';

interface Props {
disabled?: boolean;
onCommandSelect: (command: ICommand) => void;
}

export const CommandButton = ({ disabled = false, onCommandSelect }: Props) => {
const commands = useRecoilValue(commandsState);

if (!commands.length) return null;

return (
<Popover>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
id="command-button"
variant="ghost"
size="icon"
className="hover:bg-muted"
disabled={disabled}
>
<ToolBox className="!size-6" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Commands</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<PopoverContent
align="start"
sideOffset={12}
className="focus:outline-none"
>
<Command className="rounded-lg border shadow-md">
<CommandList>
<CommandGroup>
{commands.map((command) => (
<CommandItem
key={command.id}
onSelect={() => onCommandSelect(command)}
className="command-item cursor-pointer flex items-center space-x-2 p-2"
>
<Icon
name={command.icon}
className="!size-5 text-muted-foreground"
/>
<div>
<div className="font-medium">{command.id}</div>
<div className="text-sm text-muted-foreground">
{command.description}
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};

export default CommandButton;
Loading
Loading