Skip to content

Commit 7edfc50

Browse files
authored
Merge pull request #283 from easyops-cn/steve/convert-jsx
feat(): support chat stream
2 parents 93427ce + 85222de commit 7edfc50

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1348
-424
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
.aside {
2+
padding: 12px 12px 12px 0;
3+
height: 100%;
4+
}
5+
6+
.box {
7+
background: #ffffff;
8+
box-shadow: 1px 1px 10px 0px rgba(0, 10, 26, 0.1);
9+
border-radius: 12px;
10+
height: 100%;
11+
}
12+
13+
.heading {
14+
height: 52px;
15+
display: flex;
16+
align-items: center;
17+
justify-content: space-between;
18+
border-bottom: 1px solid rgba(0, 0, 0, 0.15);
19+
padding: 0 20px;
20+
}
21+
22+
.title {
23+
font-weight: 500;
24+
font-size: 16px;
25+
color: #262626;
26+
}
27+
28+
.body {
29+
flex: 1;
30+
min-height: 0;
31+
padding: 20px;
32+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React, { useContext } from "react";
2+
import type { GeneralIconProps } from "@next-bricks/icons/general-icon";
3+
import styles from "./Aside.module.css";
4+
import { WrappedIconButton } from "../../shared/bricks";
5+
import type { Job } from "../../cruise-canvas/interfaces";
6+
import { ToolCallStatus } from "../../cruise-canvas/ToolCallStatus/ToolCallStatus";
7+
import { TaskContext } from "../../shared/TaskContext";
8+
9+
const ICON_SHRINK: GeneralIconProps = {
10+
lib: "antd",
11+
icon: "shrink",
12+
};
13+
14+
export interface AsideProps {
15+
job: Job;
16+
}
17+
18+
export function Aside({ job }: AsideProps) {
19+
const { setActiveToolCallJobId } = useContext(TaskContext);
20+
21+
return (
22+
<div className={styles.aside}>
23+
<div className={styles.box}>
24+
<div className={styles.heading}>
25+
<div className={styles.title}>Elevo&#39;s Computer</div>
26+
<WrappedIconButton
27+
icon={ICON_SHRINK}
28+
variant="mini"
29+
onClick={() => {
30+
setActiveToolCallJobId(null);
31+
}}
32+
/>
33+
</div>
34+
<div className={styles.body}>
35+
<ToolCallStatus job={job} variant="read-only" />
36+
</div>
37+
</div>
38+
</div>
39+
);
40+
}

bricks/ai-portal/src/chat-stream/AssistantMessage/AssistantMessage.module.css

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,29 @@
55
.body {
66
flex: 1;
77
min-width: 0;
8-
padding-left: 16px;
8+
padding: 5px 0 5px 16px;
9+
}
10+
11+
.texting::after {
12+
content: " ";
13+
animation: texting 2s infinite;
14+
white-space: pre;
15+
}
16+
17+
@keyframes texting {
18+
0% {
19+
content: " ";
20+
}
21+
25% {
22+
content: ". ";
23+
}
24+
50% {
25+
content: ".. ";
26+
}
27+
75% {
28+
content: "...";
29+
}
30+
100% {
31+
content: " ";
32+
}
933
}

bricks/ai-portal/src/chat-stream/AssistantMessage/AssistantMessage.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
1-
import React from "react";
1+
import React, { useMemo } from "react";
22
import type { Job, TaskState } from "../../cruise-canvas/interfaces.js";
33
import styles from "./AssistantMessage.module.css";
44
import Avatar from "../images/[email protected]";
55
import { NodeJob } from "../NodeJob/NodeJob.js";
6+
import { DONE_STATES } from "../../cruise-canvas/constants.js";
67

78
export interface AssistantMessageProps {
89
jobs: Job[];
910
taskState: TaskState | undefined;
1011
}
1112

1213
export function AssistantMessage({ jobs, taskState }: AssistantMessageProps) {
14+
const working = useMemo(() => {
15+
if (DONE_STATES.includes(taskState!)) {
16+
return false;
17+
}
18+
for (const job of jobs) {
19+
if (job.state === "input-required") {
20+
return false;
21+
}
22+
if (job.state === "working") {
23+
return true;
24+
}
25+
}
26+
return true;
27+
}, [jobs, taskState]);
28+
1329
return (
1430
<div className={styles.assistant}>
1531
<div className={styles.avatar}>
@@ -19,6 +35,7 @@ export function AssistantMessage({ jobs, taskState }: AssistantMessageProps) {
1935
{jobs.map((job) => (
2036
<NodeJob key={job.id} job={job} taskState={taskState} />
2137
))}
38+
{working && <div className={styles.texting}></div>}
2239
</div>
2340
</div>
2441
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { useMemo } from "react";
2+
import type { Job } from "../../cruise-canvas/interfaces";
3+
import styles from "../NodeJob/NodeJob.module.css";
4+
5+
export interface HumanAdjustPlanResultProps {
6+
job: Job;
7+
}
8+
9+
export function HumanAdjustPlanResult({
10+
job,
11+
}: HumanAdjustPlanResultProps): JSX.Element {
12+
const response = useMemo(() => {
13+
const msg = job.messages?.find((msg) => {
14+
return msg.role === "tool";
15+
});
16+
if (msg) {
17+
const text = msg.parts?.find((part) => part.type === "text")?.text;
18+
if (text) {
19+
try {
20+
return JSON.parse(text) as { type: "plan"; steps: string[] };
21+
} catch {
22+
// Fallback to original text
23+
}
24+
}
25+
}
26+
return null;
27+
}, [job.messages]);
28+
29+
if (response?.type === "plan" && Array.isArray(response.steps)) {
30+
return (
31+
<div className={`${styles.message} ${styles["role-user"]}`}>
32+
<ol
33+
style={{
34+
paddingLeft: `${Math.floor(Math.log10(response.steps.length + 1)) * 0.5 + 1.5}em`,
35+
}}
36+
>
37+
{response.steps.map((step, index) => (
38+
<li key={index}>{step}</li>
39+
))}
40+
</ol>
41+
</div>
42+
);
43+
}
44+
45+
return (
46+
<div className={`${styles.message} ${styles["role-user"]}`}>
47+
Something went wrong.
48+
</div>
49+
);
50+
}

bricks/ai-portal/src/chat-stream/NodeJob/NodeJob.module.css

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,91 @@
11
.heading {
22
display: flex;
33
align-items: center;
4-
margin-bottom: 12px;
4+
padding: 5px 8px;
5+
margin: 0 -8px 12px;
6+
cursor: pointer;
7+
}
8+
9+
.caret {
10+
opacity: 0;
11+
transition: transform 0.2s ease-in-out;
12+
}
13+
14+
.heading:hover {
15+
background: rgba(184, 187, 205, 0.15);
16+
border-radius: 8px;
17+
18+
.caret {
19+
opacity: 1;
20+
}
521
}
622

723
.icon {
8-
width: 26px;
924
display: flex;
1025
align-items: center;
26+
margin-right: 12px;
1127
}
1228

1329
.title {
1430
color: #262626;
1531
font-weight: 500;
32+
flex: 1;
33+
min-width: 0;
34+
overflow: hidden;
35+
text-overflow: ellipsis;
36+
white-space: nowrap;
1637
}
1738

1839
.tool {
19-
background: #e6e9f3;
20-
border-radius: 12px;
21-
border: 1px solid #e2e2ec;
40+
background: #e2e5ef;
41+
border: 1px solid #d4d4de;
42+
border-radius: 13px;
43+
line-height: 24px;
2244
padding: 0 12px;
2345
width: fit-content;
2446
color: #595959;
47+
cursor: pointer;
2548
}
2649

2750
.body {
2851
padding-left: 26px;
2952
margin-bottom: 20px;
3053
}
3154

55+
.collapsed {
56+
.body {
57+
display: none;
58+
}
59+
60+
.caret {
61+
transform: rotate(180deg);
62+
opacity: 1;
63+
}
64+
}
65+
3266
.content {
3367
margin-top: 14px;
3468

3569
.markdown :global(.mermaid) {
36-
background: rgba(255, 255, 255, 0.8);
70+
background: rgba(255, 255, 255, 0.6);
3771
border-radius: 8px;
3872
border: 1px solid rgba(0, 0, 0, 0.1);
3973
backdrop-filter: blur(10px);
4074
padding: 20px;
41-
max-width: 453px;
75+
min-width: var(--cruise-canvas-node-width-medium);
76+
width: fit-content;
77+
max-width: 100%;
4278
}
4379
}
4480

4581
.content:empty {
4682
display: none;
4783
}
4884

85+
.message:first-child > .message-part:first-child > :first-child {
86+
margin-top: 0;
87+
}
88+
4989
.icon {
5090
color: #abaab7;
5191
}

0 commit comments

Comments
 (0)