Skip to content

Commit a1c03b6

Browse files
johnstcnkylecarbs
andauthored
feat: add experimental Chat UI (#17650)
Builds on #17570 Frontend portion of https://github.com/coder/coder/tree/chat originally authored by @kylecarbs Additional changes: - Addresses linter complaints - Brings `ChatToolInvocation` argument definitions in line with those defined in `codersdk/toolsdk` - Ensures chat-related features are not shown unless `ExperimentAgenticChat` is enabled. Co-authored-by: Kyle Carberry <kyle@carberry.com>
1 parent 8f64d49 commit a1c03b6

File tree

14 files changed

+3381
-6
lines changed

14 files changed

+3381
-6
lines changed

site/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
"update-emojis": "cp -rf ./node_modules/emoji-datasource-apple/img/apple/64/* ./static/emojis"
3636
},
3737
"dependencies": {
38+
"@ai-sdk/provider-utils": "2.2.6",
39+
"@ai-sdk/react": "1.2.6",
3840
"@emoji-mart/data": "1.2.1",
3941
"@emoji-mart/react": "1.1.1",
4042
"@emotion/cache": "11.14.0",
@@ -111,6 +113,7 @@
111113
"react-virtualized-auto-sizer": "1.0.24",
112114
"react-window": "1.8.11",
113115
"recharts": "2.15.0",
116+
"rehype-raw": "7.0.0",
114117
"remark-gfm": "4.0.0",
115118
"resize-observer-polyfill": "1.5.1",
116119
"rollup-plugin-visualizer": "5.14.0",

site/pnpm-lock.yaml

Lines changed: 216 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/api/api.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,13 @@ class ApiMethods {
827827
return response.data;
828828
};
829829

830+
getDeploymentLLMs = async (): Promise<TypesGen.LanguageModelConfig> => {
831+
const response = await this.axios.get<TypesGen.LanguageModelConfig>(
832+
"/api/v2/deployment/llms",
833+
);
834+
return response.data;
835+
};
836+
830837
getOrganizationIdpSyncClaimFieldValues = async (
831838
organization: string,
832839
field: string,
@@ -2489,6 +2496,23 @@ class ApiMethods {
24892496
markAllInboxNotificationsAsRead = async () => {
24902497
await this.axios.put<void>("/api/v2/notifications/inbox/mark-all-as-read");
24912498
};
2499+
2500+
createChat = async () => {
2501+
const res = await this.axios.post<TypesGen.Chat>("/api/v2/chats");
2502+
return res.data;
2503+
};
2504+
2505+
getChats = async () => {
2506+
const res = await this.axios.get<TypesGen.Chat[]>("/api/v2/chats");
2507+
return res.data;
2508+
};
2509+
2510+
getChatMessages = async (chatId: string) => {
2511+
const res = await this.axios.get<TypesGen.ChatMessage[]>(
2512+
`/api/v2/chats/${chatId}/messages`,
2513+
);
2514+
return res.data;
2515+
};
24922516
}
24932517

24942518
// This is a hard coded CSRF token/cookie pair for local development. In prod,

site/src/api/queries/chats.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { API } from "api/api";
2+
import type { QueryClient } from "react-query";
3+
4+
export const createChat = (queryClient: QueryClient) => {
5+
return {
6+
mutationFn: API.createChat,
7+
onSuccess: async () => {
8+
await queryClient.invalidateQueries(["chats"]);
9+
},
10+
};
11+
};
12+
13+
export const getChats = () => {
14+
return {
15+
queryKey: ["chats"],
16+
queryFn: API.getChats,
17+
};
18+
};
19+
20+
export const getChatMessages = (chatID: string) => {
21+
return {
22+
queryKey: ["chatMessages", chatID],
23+
queryFn: () => API.getChatMessages(chatID),
24+
};
25+
};

site/src/api/queries/deployment.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,10 @@ export const deploymentIdpSyncFieldValues = (field: string) => {
3636
queryFn: () => API.getDeploymentIdpSyncFieldValues(field),
3737
};
3838
};
39+
40+
export const deploymentLanguageModels = () => {
41+
return {
42+
queryKey: ["deployment", "llms"],
43+
queryFn: API.getDeploymentLLMs,
44+
};
45+
};

site/src/contexts/useAgenticChat.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { experiments } from "api/queries/experiments";
2+
3+
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
4+
import { useQuery } from "react-query";
5+
6+
interface AgenticChat {
7+
readonly enabled: boolean;
8+
}
9+
10+
export const useAgenticChat = (): AgenticChat => {
11+
const { metadata } = useEmbeddedMetadata();
12+
const enabledExperimentsQuery = useQuery(experiments(metadata.experiments));
13+
return {
14+
enabled: enabledExperimentsQuery.data?.includes("agentic-chat") ?? false,
15+
};
16+
};

site/src/modules/dashboard/Navbar/NavbarView.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Button } from "components/Button/Button";
44
import { ExternalImage } from "components/ExternalImage/ExternalImage";
55
import { CoderIcon } from "components/Icons/CoderIcon";
66
import type { ProxyContextValue } from "contexts/ProxyContext";
7+
import { useAgenticChat } from "contexts/useAgenticChat";
78
import { useWebpushNotifications } from "contexts/useWebpushNotifications";
89
import { NotificationsInbox } from "modules/notifications/NotificationsInbox/NotificationsInbox";
910
import type { FC } from "react";
@@ -45,8 +46,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
4546
canViewAuditLog,
4647
proxyContextValue,
4748
}) => {
48-
const { subscribed, enabled, loading, subscribe, unsubscribe } =
49-
useWebpushNotifications();
49+
const webPush = useWebpushNotifications();
5050

5151
return (
5252
<div className="border-0 border-b border-solid h-[72px] flex items-center leading-none px-6">
@@ -76,13 +76,21 @@ export const NavbarView: FC<NavbarViewProps> = ({
7676
/>
7777
</div>
7878

79-
{enabled ? (
80-
subscribed ? (
81-
<Button variant="outline" disabled={loading} onClick={unsubscribe}>
79+
{webPush.enabled ? (
80+
webPush.subscribed ? (
81+
<Button
82+
variant="outline"
83+
disabled={webPush.loading}
84+
onClick={webPush.unsubscribe}
85+
>
8286
Disable WebPush
8387
</Button>
8488
) : (
85-
<Button variant="outline" disabled={loading} onClick={subscribe}>
89+
<Button
90+
variant="outline"
91+
disabled={webPush.loading}
92+
onClick={webPush.subscribe}
93+
>
8694
Enable WebPush
8795
</Button>
8896
)
@@ -132,6 +140,7 @@ interface NavItemsProps {
132140

133141
const NavItems: FC<NavItemsProps> = ({ className }) => {
134142
const location = useLocation();
143+
const agenticChat = useAgenticChat();
135144

136145
return (
137146
<nav className={cn("flex items-center gap-4 h-full", className)}>
@@ -154,6 +163,16 @@ const NavItems: FC<NavItemsProps> = ({ className }) => {
154163
>
155164
Templates
156165
</NavLink>
166+
{agenticChat.enabled ? (
167+
<NavLink
168+
className={({ isActive }) => {
169+
return cn(linkStyles.default, isActive ? linkStyles.active : "");
170+
}}
171+
to="/chat"
172+
>
173+
Chat
174+
</NavLink>
175+
) : null}
157176
</nav>
158177
);
159178
};
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { useTheme } from "@emotion/react";
2+
import SendIcon from "@mui/icons-material/Send";
3+
import Button from "@mui/material/Button";
4+
import IconButton from "@mui/material/IconButton";
5+
import Paper from "@mui/material/Paper";
6+
import Stack from "@mui/material/Stack";
7+
import TextField from "@mui/material/TextField";
8+
import { createChat } from "api/queries/chats";
9+
import type { Chat } from "api/typesGenerated";
10+
import { Margins } from "components/Margins/Margins";
11+
import { useAuthenticated } from "hooks";
12+
import { type FC, type FormEvent, useState } from "react";
13+
import { useMutation, useQueryClient } from "react-query";
14+
import { useNavigate } from "react-router-dom";
15+
import { LanguageModelSelector } from "./LanguageModelSelector";
16+
17+
export interface ChatLandingLocationState {
18+
chat: Chat;
19+
message: string;
20+
}
21+
22+
const ChatLanding: FC = () => {
23+
const { user } = useAuthenticated();
24+
const theme = useTheme();
25+
const [input, setInput] = useState("");
26+
const navigate = useNavigate();
27+
const queryClient = useQueryClient();
28+
const createChatMutation = useMutation(createChat(queryClient));
29+
30+
return (
31+
<Margins>
32+
<div
33+
css={{
34+
display: "flex",
35+
flexDirection: "column",
36+
marginTop: theme.spacing(24),
37+
alignItems: "center",
38+
paddingBottom: theme.spacing(4),
39+
}}
40+
>
41+
{/* Initial Welcome Message Area */}
42+
<div
43+
css={{
44+
flexGrow: 1,
45+
display: "flex",
46+
flexDirection: "column",
47+
justifyContent: "center",
48+
alignItems: "center",
49+
gap: theme.spacing(1),
50+
padding: theme.spacing(1),
51+
width: "100%",
52+
maxWidth: "700px",
53+
marginBottom: theme.spacing(4),
54+
}}
55+
>
56+
<h1
57+
css={{
58+
fontSize: theme.typography.h4.fontSize,
59+
fontWeight: theme.typography.h4.fontWeight,
60+
lineHeight: theme.typography.h4.lineHeight,
61+
marginBottom: theme.spacing(1),
62+
textAlign: "center",
63+
}}
64+
>
65+
Good evening, {user?.name.split(" ")[0]}
66+
</h1>
67+
<p
68+
css={{
69+
fontSize: theme.typography.h6.fontSize,
70+
fontWeight: theme.typography.h6.fontWeight,
71+
lineHeight: theme.typography.h6.lineHeight,
72+
color: theme.palette.text.secondary,
73+
textAlign: "center",
74+
margin: 0,
75+
maxWidth: "500px",
76+
marginInline: "auto",
77+
}}
78+
>
79+
How can I help you today?
80+
</p>
81+
</div>
82+
83+
{/* Input Form and Suggestions - Always Visible */}
84+
<div css={{ width: "100%", maxWidth: "700px", marginTop: "auto" }}>
85+
<Stack
86+
direction="row"
87+
spacing={2}
88+
justifyContent="center"
89+
sx={{ mb: 2 }}
90+
>
91+
<Button
92+
variant="outlined"
93+
onClick={() => setInput("Help me work on issue #...")}
94+
>
95+
Work on Issue
96+
</Button>
97+
<Button
98+
variant="outlined"
99+
onClick={() => setInput("Help me build a template for...")}
100+
>
101+
Build a Template
102+
</Button>
103+
<Button
104+
variant="outlined"
105+
onClick={() => setInput("Help me start a new project using...")}
106+
>
107+
Start a Project
108+
</Button>
109+
</Stack>
110+
<LanguageModelSelector />
111+
<Paper
112+
component="form"
113+
onSubmit={async (e: FormEvent<HTMLFormElement>) => {
114+
e.preventDefault();
115+
setInput("");
116+
const chat = await createChatMutation.mutateAsync();
117+
navigate(`/chat/${chat.id}`, {
118+
state: {
119+
chat,
120+
message: input,
121+
},
122+
});
123+
}}
124+
elevation={2}
125+
css={{
126+
padding: "16px",
127+
display: "flex",
128+
alignItems: "center",
129+
width: "100%",
130+
borderRadius: "12px",
131+
border: `1px solid ${theme.palette.divider}`,
132+
}}
133+
>
134+
<TextField
135+
value={input}
136+
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
137+
setInput(event.target.value);
138+
}}
139+
placeholder="Ask Coder..."
140+
required
141+
fullWidth
142+
variant="outlined"
143+
multiline
144+
maxRows={5}
145+
css={{
146+
marginRight: theme.spacing(1),
147+
"& .MuiOutlinedInput-root": {
148+
borderRadius: "8px",
149+
padding: "10px 14px",
150+
},
151+
}}
152+
autoFocus
153+
/>
154+
<IconButton type="submit" color="primary" disabled={!input.trim()}>
155+
<SendIcon />
156+
</IconButton>
157+
</Paper>
158+
</div>
159+
</div>
160+
</Margins>
161+
);
162+
};
163+
164+
export default ChatLanding;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy