Skip to content

Commit 1dd0504

Browse files
authored
Merge pull request #110 from easyops-cn/steve/ai-portal
feat(): new brick: home-container
2 parents 6ddc5c7 + 0ba8c5f commit 1dd0504

File tree

18 files changed

+271
-43
lines changed

18 files changed

+271
-43
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
构件 `ai-portal.home-container`
2+
3+
## Examples
4+
5+
### Basic
6+
7+
```yaml preview
8+
brick: ai-portal.home-container
9+
properties:
10+
textContent: Hello world
11+
```

bricks/ai-portal/src/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
import "./cruise-canvas/index.js";
22
import "./chat-box/index.js";
3+
import "./home-container/index.js";

bricks/ai-portal/src/chat-box/i18n.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { i18n } from "@next-core/i18n";
22

33
export enum K {
4-
HOW_CAN_I_HELP = "HOW_CAN_I_HELP",
4+
ASK_ANY_THING = "ASK_ANYTHING",
55
}
66

77
const en: Locale = {
8-
[K.HOW_CAN_I_HELP]: "How can I help?",
8+
[K.ASK_ANY_THING]: "Ask anything",
99
};
1010

1111
const zh: Locale = {
12-
[K.HOW_CAN_I_HELP]: "有什么可以帮您的?",
12+
[K.ASK_ANY_THING]: "询问任何问题",
1313
};
1414

1515
export const NS = "bricks/ai-portal/chat-box";
101 KB
Loading

bricks/ai-portal/src/chat-box/index.spec.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import { describe, test, expect, jest } from "@jest/globals";
22
import { act } from "react-dom/test-utils";
3+
import { fireEvent } from "@testing-library/dom";
34
import "./";
45
import type { ChatBox } from "./index.js";
56

67
jest.mock("@next-core/theme", () => ({}));
78

9+
customElements.define(
10+
"eo-icon",
11+
class extends HTMLElement {
12+
lib: string | undefined;
13+
prefix: any;
14+
icon: string | undefined;
15+
}
16+
);
17+
818
describe("ai-portal.chat-box", () => {
919
test("basic usage", async () => {
1020
const element = document.createElement("ai-portal.chat-box") as ChatBox;
@@ -21,4 +31,86 @@ describe("ai-portal.chat-box", () => {
2131
});
2232
expect(element.shadowRoot?.childNodes.length).toBe(0);
2333
});
34+
35+
test("auto focus", async () => {
36+
const element = document.createElement("ai-portal.chat-box") as ChatBox;
37+
element.autoFocus = true;
38+
element.placeholder = "Please enter your message";
39+
40+
act(() => {
41+
document.body.appendChild(element);
42+
});
43+
44+
const textarea = element.shadowRoot?.querySelector(
45+
"textarea"
46+
) as HTMLTextAreaElement;
47+
expect(textarea).not.toBeNull();
48+
expect(textarea.placeholder).toBe("Please enter your message");
49+
const focus = jest.spyOn(textarea, "focus");
50+
51+
await act(async () => {
52+
await Promise.resolve();
53+
});
54+
expect(focus).toBeCalled();
55+
56+
act(() => {
57+
document.body.removeChild(element);
58+
});
59+
});
60+
61+
test("submit by click", () => {
62+
const element = document.createElement("ai-portal.chat-box") as ChatBox;
63+
64+
const onSubmit = jest.fn();
65+
element.addEventListener("message.submit", (e: Event) => {
66+
onSubmit((e as CustomEvent).detail);
67+
});
68+
69+
act(() => {
70+
document.body.appendChild(element);
71+
});
72+
73+
const textarea = element.shadowRoot?.querySelector(
74+
"textarea"
75+
) as HTMLTextAreaElement;
76+
expect(textarea).not.toBeNull();
77+
78+
act(() => {
79+
fireEvent.change(textarea, { target: { value: "Hello" } });
80+
fireEvent.click(element.shadowRoot!.querySelector("button")!);
81+
});
82+
expect(onSubmit).toBeCalledWith("Hello");
83+
84+
act(() => {
85+
document.body.removeChild(element);
86+
});
87+
});
88+
89+
test("submit by enter", () => {
90+
const element = document.createElement("ai-portal.chat-box") as ChatBox;
91+
92+
const onSubmit = jest.fn();
93+
element.addEventListener("message.submit", (e: Event) => {
94+
onSubmit((e as CustomEvent).detail);
95+
});
96+
97+
act(() => {
98+
document.body.appendChild(element);
99+
});
100+
101+
const textarea = element.shadowRoot?.querySelector(
102+
"textarea"
103+
) as HTMLTextAreaElement;
104+
expect(textarea).not.toBeNull();
105+
106+
act(() => {
107+
fireEvent.change(textarea, { target: { value: "Hello" } });
108+
fireEvent.keyDown(textarea, { key: "Enter" });
109+
});
110+
expect(onSubmit).toBeCalledWith("Hello");
111+
112+
act(() => {
113+
document.body.removeChild(element);
114+
});
115+
});
24116
});

bricks/ai-portal/src/chat-box/index.tsx

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
1-
import React, { useCallback, useRef } from "react";
1+
import React, { useCallback, useEffect, useRef } from "react";
22
import { createDecorators, type EventEmitter } from "@next-core/element";
33
import { ReactNextElement, wrapBrick } from "@next-core/react-element";
4-
import { TextareaAutoResize } from "@next-shared/form";
4+
import {
5+
TextareaAutoResize,
6+
type TextareaAutoResizeRef,
7+
} from "@next-shared/form";
58
import "@next-core/theme";
69
import { initializeI18n } from "@next-core/i18n";
7-
import type { Button, ButtonProps } from "@next-bricks/basic/button";
10+
import type {
11+
GeneralIcon,
12+
GeneralIconProps,
13+
} from "@next-bricks/icons/general-icon";
814
import { K, NS, locales, t } from "./i18n.js";
915
import styleText from "./styles.shadow.css";
1016

1117
initializeI18n(NS, locales);
1218

13-
const WrappedButton = wrapBrick<Button, ButtonProps>("eo-button");
14-
15-
const SEND_ICON: ButtonProps["icon"] = {
16-
lib: "fa",
17-
prefix: "fas",
18-
icon: "arrow-up",
19-
};
19+
const WrappedIcon = wrapBrick<GeneralIcon, GeneralIconProps>("eo-icon");
2020

2121
const { defineElement, property, event } = createDecorators();
2222

2323
export interface ChatBoxProps {
2424
disabled?: boolean;
25+
placeholder?: string;
26+
autoFocus?: boolean;
2527
}
2628

2729
/**
@@ -35,6 +37,12 @@ class ChatBox extends ReactNextElement implements ChatBoxProps {
3537
@property({ type: Boolean })
3638
accessor disabled: boolean | undefined;
3739

40+
@property()
41+
accessor placeholder: string | undefined;
42+
43+
@property({ type: Boolean })
44+
accessor autoFocus: boolean | undefined;
45+
3846
@event({ type: "message.submit" })
3947
accessor #messageSubmit!: EventEmitter<string>;
4048

@@ -46,6 +54,8 @@ class ChatBox extends ReactNextElement implements ChatBoxProps {
4654
return (
4755
<ChatBoxComponent
4856
disabled={this.disabled}
57+
placeholder={this.placeholder}
58+
autoFocus={this.autoFocus}
4959
onSubmit={this.#handleMessageSubmit}
5060
/>
5161
);
@@ -59,9 +69,12 @@ export interface ChatBoxComponentProps extends ChatBoxProps {
5969

6070
export function ChatBoxComponent({
6171
disabled,
72+
placeholder,
73+
autoFocus,
6274
onSubmit,
6375
}: ChatBoxComponentProps) {
6476
const containerRef = useRef<HTMLDivElement>(null);
77+
const textareaRef = useRef<TextareaAutoResizeRef>(null);
6578
const valueRef = useRef("");
6679

6780
const handleSubmit = useCallback(
@@ -82,26 +95,31 @@ export function ChatBoxComponent({
8295
onSubmit?.(valueRef.current);
8396
}, [onSubmit]);
8497

98+
useEffect(() => {
99+
if (autoFocus) {
100+
Promise.resolve().then(() => {
101+
textareaRef.current?.focus();
102+
});
103+
}
104+
}, []);
105+
85106
return (
86107
<div className="container" ref={containerRef}>
87108
<TextareaAutoResize
88109
containerRef={containerRef}
110+
ref={textareaRef}
89111
minRows={5}
90-
paddingSize={24}
112+
paddingSize={20}
91113
autoResize
92114
disabled={disabled}
93-
placeholder={t(K.HOW_CAN_I_HELP)}
115+
placeholder={placeholder ?? t(K.ASK_ANY_THING)}
94116
submitWhen="enter-without-shift"
95117
onSubmit={handleSubmit}
96118
onChange={handleChange}
97119
/>
98-
<WrappedButton
99-
className="btn-send"
100-
shape="circle"
101-
icon={SEND_ICON}
102-
disabled={disabled}
103-
onClick={handleSubmitClick}
104-
/>
120+
<button className="btn-send" onClick={handleSubmitClick}>
121+
<WrappedIcon lib="fa" prefix="fas" icon="arrow-up" />
122+
</button>
105123
</div>
106124
);
107125
}
Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
:host {
22
display: block;
3+
color: #262626;
34
}
45

56
:host([hidden]) {
@@ -12,38 +13,58 @@
1213

1314
.container {
1415
position: relative;
16+
background-color: #e1e3ec;
17+
background-image: url("./images/[email protected]");
18+
background-size: 694px 144px;
19+
background-position: top left;
20+
background-repeat: no-repeat;
21+
box-shadow: inset 0px 2px 5px 1px rgba(42, 46, 59, 0.1);
22+
border-radius: 20px;
23+
padding: 12px;
1524
}
1625

1726
textarea {
1827
display: block;
1928
width: 100%;
20-
padding: 12px 18px;
29+
padding: 10px 16px;
2130
color: var(--antd-input-color);
22-
background: var(--antd-input-bg);
23-
box-sizing: border-box;
24-
border-radius: 1em;
25-
border: 1px solid var(--antd-input-border-color);
31+
background: rgba(255, 255, 255, 0.8);
32+
border-radius: 14px;
33+
backdrop-filter: blur(10px);
34+
border: 1px solid rgba(255, 255, 255, 0.5);
2635
font-size: 14px;
2736
line-height: 22px;
28-
height: 32px;
29-
}
30-
31-
textarea:not(:disabled):hover {
32-
border: 1px solid var(--antd-input-hover-border-color);
37+
height: 132px;
3338
}
3439

35-
textarea:not(:disabled):focus {
36-
border: 1px solid var(--antd-input-focus-border-color);
37-
outline: 0;
38-
box-shadow: 0 0 0 2px rgb(0 113 235 / 20%);
40+
textarea::placeholder {
41+
color: rgba(0, 0, 0, 0.4);
3942
}
4043

41-
textarea::placeholder {
42-
color: var(--antd-input-placeholder-color);
44+
textarea:focus-visible {
45+
outline: none;
4346
}
4447

4548
.btn-send {
4649
position: absolute;
47-
bottom: 1em;
48-
right: 1em;
50+
bottom: 26px;
51+
right: 26px;
52+
width: 28px;
53+
height: 28px;
54+
display: flex;
55+
align-items: center;
56+
justify-content: center;
57+
background: rgba(0, 0, 0, 0.06);
58+
border: none;
59+
border-radius: 8px;
60+
cursor: pointer;
61+
color: inherit;
62+
}
63+
64+
.btn-send:hover {
65+
background: rgba(0, 0, 0, 0.08);
66+
}
67+
68+
.btn-send:active {
69+
background: rgba(0, 0, 0, 0.15);
4970
}

bricks/ai-portal/src/cruise-canvas/NodeInstruction/NodeInstruction.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.node-instruction {
22
max-width: 437px;
3+
max-width: min(437px, 90vw);
34
background: rgba(255, 255, 255, 0.2);
45
box-shadow: 0px 4px 10px 0px rgba(54, 64, 80, 0.08);
56
border-radius: 9999px;

bricks/ai-portal/src/cruise-canvas/NodeJob/NodeJob.module.css

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
.node-job {
22
width: 330px;
3+
max-width: 90vw;
34
background: linear-gradient(180deg, #f5f8ff 0%, #edf0f9 100%);
45
box-shadow:
56
0px 4px 10px 0px rgba(177, 149, 207, 0.4),
67
0px 2px 4px 0px rgba(0, 0, 0, 0.06);
78
border-radius: 12px;
8-
border: 1px solid #ffffff;
9+
border: 1px solid #fff;
910
padding: 8px;
1011
}
1112

13+
.ask-user {
14+
background: rgba(255, 255, 255, 0.4);
15+
box-shadow: 0px 4px 10px 0px rgba(54, 64, 80, 0.18);
16+
border-color: rgba(255, 255, 255, 0.5);
17+
backdrop-filter: blur(28px);
18+
}
19+
1220
.error {
1321
box-shadow: inset 0px 0px 8px 2px rgba(242, 76, 37, 0.6);
1422
border-color: rgba(242, 76, 37, 0.6);

0 commit comments

Comments
 (0)