Thanks to visit codestin.com
Credit goes to github.com

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 <[email protected]>
1 parent 8f64d49 commit a1c03b6

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)