Skip to content

Commit 35dcff0

Browse files
authored
feat: add an interrupt button to chat (#34)
1 parent 8f7db1a commit 35dcff0

File tree

4 files changed

+81
-24
lines changed

4 files changed

+81
-24
lines changed

chat/src/components/chat-provider.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function isDraftMessage(message: Message | DraftMessage): boolean {
4040

4141
type MessageType = "user" | "raw";
4242

43-
type ServerStatus = "online" | "offline" | "unknown";
43+
export type ServerStatus = "stable" | "running" | "offline" | "unknown";
4444

4545
interface ChatContextValue {
4646
messages: (Message | DraftMessage)[];
@@ -121,7 +121,13 @@ export function ChatProvider({ children }: PropsWithChildren) {
121121
// Handle status changes
122122
eventSource.addEventListener("status_change", (event) => {
123123
const data: StatusChangeEvent = JSON.parse(event.data);
124-
setServerStatus(data.status as ServerStatus);
124+
if (data.status === "stable") {
125+
setServerStatus("stable");
126+
} else if (data.status === "running") {
127+
setServerStatus("running");
128+
} else {
129+
setServerStatus("unknown");
130+
}
125131
});
126132

127133
// Handle connection open (server is online)

chat/src/components/chat.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import MessageInput from "./message-input";
55
import MessageList from "./message-list";
66

77
export function Chat() {
8-
const { messages, loading, sendMessage } = useChat();
8+
const { messages, loading, sendMessage, serverStatus } = useChat();
99

1010
return (
1111
<>
1212
<MessageList messages={messages} />
13-
<MessageInput onSendMessage={sendMessage} disabled={loading} />
13+
<MessageInput
14+
onSendMessage={sendMessage}
15+
disabled={loading}
16+
serverStatus={serverStatus}
17+
/>
1418
</>
1519
);
1620
}

chat/src/components/message-input.tsx

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ import {
1010
CornerDownLeftIcon,
1111
DeleteIcon,
1212
SendIcon,
13+
Square,
1314
} from "lucide-react";
1415
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
16+
import type { ServerStatus } from "./chat-provider";
1517

1618
interface MessageInputProps {
1719
onSendMessage: (message: string, type: "user" | "raw") => void;
1820
disabled?: boolean;
21+
serverStatus: ServerStatus;
1922
}
2023

2124
interface SentChar {
@@ -24,9 +27,27 @@ interface SentChar {
2427
timestamp: number;
2528
}
2629

30+
// List of keys to send as raw input when in control mode
31+
32+
const specialKeys: Record<string, string> = {
33+
ArrowUp: "\x1b[A", // Escape sequence for up arrow
34+
ArrowDown: "\x1b[B", // Escape sequence for down arrow
35+
ArrowRight: "\x1b[C", // Escape sequence for right arrow
36+
ArrowLeft: "\x1b[D", // Escape sequence for left arrow
37+
Escape: "\x1b", // Escape key
38+
Tab: "\t", // Tab key
39+
Delete: "\x1b[3~", // Delete key
40+
Home: "\x1b[H", // Home key
41+
End: "\x1b[F", // End key
42+
PageUp: "\x1b[5~", // Page Up
43+
PageDown: "\x1b[6~", // Page Down
44+
Backspace: "\b", // Backspace key
45+
};
46+
2747
export default function MessageInput({
2848
onSendMessage,
2949
disabled = false,
50+
serverStatus,
3051
}: MessageInputProps) {
3152
const [message, setMessage] = useState("");
3253
const [inputMode, setInputMode] = useState("text");
@@ -69,22 +90,6 @@ export default function MessageInput({
6990
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
7091
// In control mode, send special keys as raw messages
7192
if (inputMode === "control" && !disabled) {
72-
// List of keys to send as raw input when in control mode
73-
const specialKeys: Record<string, string> = {
74-
ArrowUp: "\x1b[A", // Escape sequence for up arrow
75-
ArrowDown: "\x1b[B", // Escape sequence for down arrow
76-
ArrowRight: "\x1b[C", // Escape sequence for right arrow
77-
ArrowLeft: "\x1b[D", // Escape sequence for left arrow
78-
Escape: "\x1b", // Escape key
79-
Tab: "\t", // Tab key
80-
Delete: "\x1b[3~", // Delete key
81-
Home: "\x1b[H", // Home key
82-
End: "\x1b[F", // End key
83-
PageUp: "\x1b[5~", // Page Up
84-
PageDown: "\x1b[6~", // Page Down
85-
Backspace: "\b", // Backspace key
86-
};
87-
8893
// Check if the pressed key is in our special keys map
8994
if (specialKeys[e.key]) {
9095
e.preventDefault();
@@ -168,8 +173,13 @@ export default function MessageInput({
168173
value={message}
169174
onChange={(e) => setMessage(e.target.value)}
170175
onKeyDown={handleKeyDown}
171-
placeholder={"Type a message..."}
176+
placeholder={
177+
serverStatus === "running"
178+
? "Running..."
179+
: "Type a message..."
180+
}
172181
className="resize-none w-full text-sm outline-none p-4 h-20"
182+
disabled={serverStatus !== "stable"}
173183
/>
174184
)}
175185
</div>
@@ -194,7 +204,7 @@ export default function MessageInput({
194204
</TabsTrigger>
195205
</TabsList>
196206

197-
{inputMode === "text" && (
207+
{inputMode === "text" && serverStatus !== "running" && (
198208
<Button
199209
type="submit"
200210
disabled={disabled || !message.trim()}
@@ -206,6 +216,20 @@ export default function MessageInput({
206216
</Button>
207217
)}
208218

219+
{inputMode === "text" && serverStatus === "running" && (
220+
<Button
221+
size="icon"
222+
className="rounded-full"
223+
disabled={disabled}
224+
onClick={() => {
225+
onSendMessage(specialKeys.Escape, "raw");
226+
}}
227+
>
228+
<Square />
229+
<span className="sr-only">Stop</span>
230+
</Button>
231+
)}
232+
209233
{inputMode === "control" && !disabled && (
210234
<div className="flex items-center gap-1">
211235
{sentChars.map((char) => (

chat/src/stories/message-input.stories.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,31 @@ const meta = {
1414
export default meta;
1515
type Story = StoryObj<typeof meta>;
1616

17-
export const Default: Story = {
17+
const defaultArgs = {
18+
onSendMessage: () => {},
19+
};
20+
21+
export const ServerStatusStable: Story = {
22+
args: {
23+
...defaultArgs,
24+
serverStatus: "stable",
25+
},
26+
};
27+
export const ServerStatusRunning: Story = {
28+
args: {
29+
...defaultArgs,
30+
serverStatus: "running",
31+
},
32+
};
33+
export const ServerStatusOffline: Story = {
34+
args: {
35+
...defaultArgs,
36+
serverStatus: "offline",
37+
},
38+
};
39+
export const ServerStatusUnknown: Story = {
1840
args: {
19-
onSendMessage: () => {},
41+
...defaultArgs,
42+
serverStatus: "unknown",
2043
},
2144
};

0 commit comments

Comments
 (0)