Skip to content

Commit 300504d

Browse files
committed
feat: embed chat ui in the task sidebar
1 parent da9a313 commit 300504d

File tree

5 files changed

+448
-391
lines changed

5 files changed

+448
-391
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { WorkspaceApp } from "api/typesGenerated";
2+
import { useAppLink } from "modules/apps/useAppLink";
3+
import type { Task } from "modules/tasks/tasks";
4+
import type { FC } from "react";
5+
import { cn } from "utils/cn";
6+
7+
type TaskAppIFrameProps = {
8+
task: Task;
9+
app: WorkspaceApp;
10+
active: boolean;
11+
};
12+
13+
export const TaskAppIFrame: FC<TaskAppIFrameProps> = ({
14+
task,
15+
app,
16+
active,
17+
}) => {
18+
const agent = task.workspace.latest_build.resources
19+
.flatMap((r) => r.agents)
20+
.filter((a) => !!a)
21+
.find((a) => a.apps.some((a) => a.id === app.id));
22+
23+
if (!agent) {
24+
throw new Error(`Agent for app ${app.id} not found in task workspace`);
25+
}
26+
27+
const link = useAppLink(app, {
28+
agent,
29+
workspace: task.workspace,
30+
});
31+
32+
return (
33+
<iframe
34+
src={link.href}
35+
title={link.label}
36+
loading="eager"
37+
className={cn([active ? "block" : "hidden", "w-full h-full border-0"])}
38+
/>
39+
);
40+
};

site/src/pages/TaskPage/TaskApps.tsx

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import type { WorkspaceApp } from "api/typesGenerated";
2+
import { Button } from "components/Button/Button";
3+
import {
4+
DropdownMenu,
5+
DropdownMenuContent,
6+
DropdownMenuItem,
7+
DropdownMenuTrigger,
8+
} from "components/DropdownMenu/DropdownMenu";
9+
import { ExternalImage } from "components/ExternalImage/ExternalImage";
10+
import { ChevronDownIcon, LayoutGridIcon } from "lucide-react";
11+
import { useAppLink } from "modules/apps/useAppLink";
12+
import type { Task } from "modules/tasks/tasks";
13+
import type React from "react";
14+
import { type FC, useState } from "react";
15+
import { Link as RouterLink } from "react-router-dom";
16+
import { cn } from "utils/cn";
17+
import { TaskAppIFrame } from "./TaskAppIframe";
18+
19+
type TaskAppsProps = {
20+
task: Task;
21+
};
22+
23+
export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
24+
const agents = task.workspace.latest_build.resources
25+
.flatMap((r) => r.agents)
26+
.filter((a) => !!a);
27+
28+
const apps = agents.flatMap((a) => a?.apps).filter((a) => !!a);
29+
30+
const [activeAppId, setActiveAppId] = useState<string>(() => {
31+
const appId = task.workspace.latest_app_status?.app_id;
32+
if (!appId) {
33+
throw new Error("No active app found in task");
34+
}
35+
return appId;
36+
});
37+
38+
const activeApp = apps.find((app) => app.id === activeAppId);
39+
if (!activeApp) {
40+
throw new Error(`Active app with ID ${activeAppId} not found in task`);
41+
}
42+
43+
const agent = agents.find((a) =>
44+
a.apps.some((app) => app.id === activeAppId),
45+
);
46+
if (!agent) {
47+
throw new Error(`Agent for app ${activeAppId} not found in task workspace`);
48+
}
49+
50+
const embeddedApps = apps.filter((app) => !app.external);
51+
const externalApps = apps.filter((app) => app.external);
52+
53+
return (
54+
<main className="flex-1 flex flex-col">
55+
<div className="border-0 border-b border-border border-solid w-full p-1 flex gap-2">
56+
{embeddedApps
57+
.filter((app) => !app.external)
58+
.map((app) => (
59+
<TaskAppButton
60+
key={app.id}
61+
task={task}
62+
app={app}
63+
active={app.id === activeAppId}
64+
onClick={(e) => {
65+
if (app.external) {
66+
return;
67+
}
68+
69+
e.preventDefault();
70+
setActiveAppId(app.id);
71+
}}
72+
/>
73+
))}
74+
75+
{externalApps.length > 0 && (
76+
<div className="ml-auto">
77+
<DropdownMenu>
78+
<DropdownMenuTrigger asChild>
79+
<Button size="sm" variant="subtle">
80+
Open locally
81+
<ChevronDownIcon />
82+
</Button>
83+
</DropdownMenuTrigger>
84+
<DropdownMenuContent>
85+
{externalApps
86+
.filter((app) => app.external)
87+
.map((app) => {
88+
const link = useAppLink(app, {
89+
agent,
90+
workspace: task.workspace,
91+
});
92+
93+
return (
94+
<DropdownMenuItem key={app.id} asChild>
95+
<RouterLink to={link.href}>
96+
{app.icon ? (
97+
<ExternalImage src={app.icon} />
98+
) : (
99+
<LayoutGridIcon />
100+
)}
101+
{link.label}
102+
</RouterLink>
103+
</DropdownMenuItem>
104+
);
105+
})}
106+
</DropdownMenuContent>
107+
</DropdownMenu>
108+
</div>
109+
)}
110+
</div>
111+
112+
<div className="flex-1">
113+
{embeddedApps.map((app) => {
114+
return (
115+
<TaskAppIFrame
116+
key={app.id}
117+
active={activeAppId === app.id}
118+
app={app}
119+
task={task}
120+
/>
121+
);
122+
})}
123+
</div>
124+
</main>
125+
);
126+
};
127+
128+
type TaskAppButtonProps = {
129+
task: Task;
130+
app: WorkspaceApp;
131+
active: boolean;
132+
onClick: (e: React.MouseEvent<HTMLAnchorElement>) => void;
133+
};
134+
135+
const TaskAppButton: FC<TaskAppButtonProps> = ({
136+
task,
137+
app,
138+
active,
139+
onClick,
140+
}) => {
141+
const agent = task.workspace.latest_build.resources
142+
.flatMap((r) => r.agents)
143+
.filter((a) => !!a)
144+
.find((a) => a.apps.some((a) => a.id === app.id));
145+
146+
if (!agent) {
147+
throw new Error(`Agent for app ${app.id} not found in task workspace`);
148+
}
149+
150+
const link = useAppLink(app, {
151+
agent,
152+
workspace: task.workspace,
153+
});
154+
155+
return (
156+
<Button
157+
size="sm"
158+
variant="subtle"
159+
key={app.id}
160+
asChild
161+
className={cn([
162+
{ "text-content-primary": active },
163+
{ "opacity-75 hover:opacity-100": !active },
164+
])}
165+
>
166+
<RouterLink to={link.href} onClick={onClick}>
167+
{app.icon ? <ExternalImage src={app.icon} /> : <LayoutGridIcon />}
168+
{link.label}
169+
</RouterLink>
170+
</Button>
171+
);
172+
};

0 commit comments

Comments
 (0)