Skip to content

Commit 94171b0

Browse files
authored
Merge pull request #122 from easyops-cn/steve/ai-portal
feat(): new brick: chat-history
2 parents af320c0 + 48f8f43 commit 94171b0

File tree

10 files changed

+380
-5
lines changed

10 files changed

+380
-5
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
构件 `ai-portal.chat-history`
2+
3+
## Examples
4+
5+
### Basic
6+
7+
```yaml preview
8+
brick: ai-portal.chat-history
9+
properties:
10+
list:
11+
- id: task_x
12+
title: 纳管 X 系统
13+
startTime: <% (+moment()) / 1000 %>
14+
state: working
15+
- id: task_y
16+
title: 纳管 Y 系统
17+
startTime: <% (+moment().subtract(2, "hours")) / 1000 %>
18+
state: completed
19+
- id: task_z
20+
title: 纳管 Z 系统
21+
startTime: <% (+moment().subtract(1, "days")) / 1000 %>
22+
state: completed
23+
actions:
24+
- icon:
25+
lib: antd
26+
icon: upload
27+
text: 分享
28+
isDropdown: true
29+
event: share
30+
- icon:
31+
lib: antd
32+
icon: edit
33+
text: 重命名
34+
isDropdown: true
35+
event: rename
36+
- icon:
37+
lib: antd
38+
icon: delete
39+
text: 删除
40+
isDropdown: true
41+
danger: true
42+
event: delete
43+
events:
44+
action.click:
45+
- action: console.log
46+
- if: <% EVENT.detail.action.event === "delete" %>
47+
useProvider: basic.show-dialog
48+
args:
49+
- type: delete
50+
title: 确定删除这一条任务记录吗?
51+
content: 删除后任务记录无法恢复,请谨慎操作
52+
```

bricks/ai-portal/src/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import "./cruise-canvas/index.js";
22
import "./chat-box/index.js";
33
import "./home-container/index.js";
4+
import "./chat-history/index.js";
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { i18n } from "@next-core/i18n";
2+
3+
export enum K {
4+
TODAY = "TODAY",
5+
YESTERDAY = "YESTERDAY",
6+
PREVIOUS_7_DAYS = "PREVIOUS_7_DAYS",
7+
PREVIOUS_30_DAYS = "PREVIOUS_30_DAYS",
8+
}
9+
10+
const en: Locale = {
11+
[K.TODAY]: "Today",
12+
[K.YESTERDAY]: "Yesterday",
13+
[K.PREVIOUS_7_DAYS]: "Previous 7 days",
14+
[K.PREVIOUS_30_DAYS]: "Previous 30 days",
15+
};
16+
17+
const zh: Locale = {
18+
[K.TODAY]: "今天",
19+
[K.YESTERDAY]: "昨天",
20+
[K.PREVIOUS_7_DAYS]: "过去7天",
21+
[K.PREVIOUS_30_DAYS]: "过去30天",
22+
};
23+
24+
export const NS = "bricks/ai-portal/chat-history";
25+
26+
export const locales = { en, zh };
27+
28+
export const t = i18n.getFixedT(null, NS);
29+
30+
type Locale = { [k in K]: string } & {
31+
[k in K as `${k}_plural`]?: string;
32+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, test, expect, jest } from "@jest/globals";
2+
import { act } from "react-dom/test-utils";
3+
import "./";
4+
import type { ChatHistory } from "./index.js";
5+
6+
jest.mock("@next-core/theme", () => ({}));
7+
8+
describe("ai-portal.chat-history", () => {
9+
test("basic usage", async () => {
10+
const element = document.createElement(
11+
"ai-portal.chat-history"
12+
) as ChatHistory;
13+
14+
expect(element.shadowRoot).toBeFalsy();
15+
16+
act(() => {
17+
document.body.appendChild(element);
18+
});
19+
expect(element.shadowRoot?.childNodes.length).toBeGreaterThan(1);
20+
21+
act(() => {
22+
document.body.removeChild(element);
23+
});
24+
expect(element.shadowRoot?.childNodes.length).toBe(0);
25+
});
26+
});
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// istanbul ignore file: experimental
2+
import React, { useMemo, useState } from "react";
3+
import { createDecorators, type EventEmitter } from "@next-core/element";
4+
import { ReactNextElement, wrapBrick } from "@next-core/react-element";
5+
import moment from "moment";
6+
import type { Link, LinkProps } from "@next-bricks/basic/link";
7+
import type {
8+
ActionType,
9+
EoMiniActions,
10+
EoMiniActionsEvents,
11+
EoMiniActionsEventsMapping,
12+
EoMiniActionsProps,
13+
SimpleActionType,
14+
} from "@next-bricks/basic/mini-actions";
15+
import "@next-core/theme";
16+
import { initializeI18n } from "@next-core/i18n";
17+
import { K, NS, locales, t } from "./i18n.js";
18+
import styleText from "./styles.shadow.css";
19+
import type { TaskState } from "../cruise-canvas/interfaces.js";
20+
import classNames from "classnames";
21+
import { DONE_STATES } from "../cruise-canvas/constants.js";
22+
23+
initializeI18n(NS, locales);
24+
25+
const WrappedLink = wrapBrick<Link, LinkProps>("eo-link");
26+
const WrappedMiniActions = wrapBrick<
27+
EoMiniActions,
28+
EoMiniActionsProps,
29+
EoMiniActionsEvents,
30+
EoMiniActionsEventsMapping
31+
>("eo-mini-actions", {
32+
onActionClick: "action.click",
33+
onVisibleChange: "visible.change",
34+
});
35+
36+
const { defineElement, property, event } = createDecorators();
37+
38+
export interface ChatHistoryProps {
39+
list?: HistoryItem[];
40+
actions?: ActionType[];
41+
}
42+
43+
export interface HistoryItem {
44+
id: string;
45+
title: string;
46+
startTime: number;
47+
state?: TaskState;
48+
}
49+
50+
export interface ActionClickDetail {
51+
action: SimpleActionType;
52+
item: HistoryItem;
53+
}
54+
55+
/**
56+
* 构件 `ai-portal.chat-history`
57+
*/
58+
export
59+
@defineElement("ai-portal.chat-history", {
60+
styleTexts: [styleText],
61+
})
62+
class ChatHistory extends ReactNextElement implements ChatHistoryProps {
63+
@property({ attribute: false })
64+
accessor list: HistoryItem[] | undefined;
65+
66+
@property({ attribute: false })
67+
accessor actions: ActionType[] | undefined;
68+
69+
@event({ type: "action.click" })
70+
accessor #actionClick!: EventEmitter<ActionClickDetail>;
71+
72+
#handleActionClick = (detail: ActionClickDetail) => {
73+
this.#actionClick.emit(detail);
74+
};
75+
76+
render() {
77+
return (
78+
<ChatHistoryComponent
79+
list={this.list}
80+
actions={this.actions}
81+
onActionClick={this.#handleActionClick}
82+
/>
83+
);
84+
}
85+
}
86+
87+
export interface ChatHistoryComponentProps extends ChatHistoryProps {
88+
onActionClick?: (detail: ActionClickDetail) => void;
89+
}
90+
91+
interface GroupedHistory {
92+
title: string;
93+
items: HistoryItem[];
94+
}
95+
96+
export function ChatHistoryComponent({
97+
list,
98+
actions,
99+
onActionClick,
100+
}: ChatHistoryComponentProps) {
101+
const groups = useMemo(() => {
102+
const groupMap = new Map<string, GroupedHistory>();
103+
// Group history by
104+
// - today
105+
// - yesterday
106+
// - previous 7 days
107+
// - previous 30 days
108+
// - each month this year
109+
// - each year before.
110+
const now = moment();
111+
const startOfDay = now.startOf("day");
112+
const yesterday = startOfDay.clone().subtract(1, "day");
113+
const sevenDaysAgo = startOfDay.clone().subtract(7, "days");
114+
const thirtyDaysAgo = startOfDay.clone().subtract(30, "days");
115+
const thisYear = now.year();
116+
117+
const timestamps = {
118+
startOfDay: +startOfDay / 1000,
119+
yesterday: +yesterday / 1000,
120+
sevenDaysAgo: +sevenDaysAgo / 1000,
121+
thirtyDaysAgo: +thirtyDaysAgo / 1000,
122+
thisYear,
123+
};
124+
for (const item of list ?? []) {
125+
let groupKey: string;
126+
if (item.startTime >= timestamps.startOfDay) {
127+
groupKey = t(K.TODAY);
128+
} else if (item.startTime >= timestamps.yesterday) {
129+
groupKey = t(K.YESTERDAY);
130+
} else if (item.startTime >= timestamps.sevenDaysAgo) {
131+
groupKey = t(K.PREVIOUS_7_DAYS);
132+
} else if (item.startTime >= timestamps.thirtyDaysAgo) {
133+
groupKey = t(K.PREVIOUS_30_DAYS);
134+
} else if (item.startTime >= timestamps.thisYear) {
135+
groupKey = moment(item.startTime * 1000).format("MMMM");
136+
} else {
137+
groupKey = moment(item.startTime * 1000).format("YYYY");
138+
}
139+
let group = groupMap.get(groupKey);
140+
if (!group) {
141+
groupMap.set(groupKey, (group = { title: groupKey, items: [] }));
142+
}
143+
group.items.push(item);
144+
}
145+
146+
return [...groupMap.values()];
147+
}, [list]);
148+
149+
const [actionsVisible, setActionsVisible] = useState<string | null>(null);
150+
151+
return (
152+
<ul>
153+
{groups.map((group) => (
154+
<li key={group.title} className="group">
155+
<div className="group-title">{group.title}</div>
156+
<ul className="items">
157+
{group.items.map((item) => (
158+
<li key={item.id}>
159+
<WrappedLink
160+
className={classNames("item", {
161+
active: actionsVisible === item.id,
162+
})}
163+
url={`/${item.id}`}
164+
>
165+
<div className="item-title">{item.title}</div>
166+
<WrappedMiniActions
167+
className="actions"
168+
actions={actions}
169+
onActionClick={(e) => {
170+
onActionClick?.({ action: e.detail, item });
171+
}}
172+
onVisibleChange={(e) => {
173+
setActionsVisible(e.detail ? item.id : null);
174+
}}
175+
/>
176+
{!DONE_STATES.includes(item.state!) && (
177+
<div className="working"></div>
178+
)}
179+
</WrappedLink>
180+
</li>
181+
))}
182+
</ul>
183+
</li>
184+
))}
185+
</ul>
186+
);
187+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
:host {
2+
display: block;
3+
}
4+
5+
:host([hidden]) {
6+
display: none;
7+
}
8+
9+
* {
10+
box-sizing: border-box;
11+
}
12+
13+
ul {
14+
list-style: none;
15+
margin: 0;
16+
padding: 0;
17+
}
18+
19+
.group + .group {
20+
margin-top: 15px;
21+
}
22+
23+
.group-title {
24+
font-weight: 500;
25+
font-size: 12px;
26+
color: #000;
27+
padding: 8px;
28+
height: 36px;
29+
line-height: 20px;
30+
}
31+
32+
.item {
33+
display: block;
34+
}
35+
36+
.item::part(link) {
37+
display: flex;
38+
align-items: center;
39+
padding: 0 8px;
40+
height: 36px;
41+
border-radius: 8px;
42+
color: #000;
43+
}
44+
45+
.item::part(link):hover {
46+
background: rgba(0, 0, 0, 0.04);
47+
}
48+
49+
.item-title {
50+
flex: 1;
51+
overflow: hidden;
52+
white-space: nowrap;
53+
text-overflow: ellipsis;
54+
}
55+
56+
.actions {
57+
cursor: auto;
58+
}
59+
60+
.item:not(:hover):not(.active) .actions {
61+
display: none;
62+
}
63+
64+
.working {
65+
width: 8px;
66+
height: 8px;
67+
border-radius: 8px;
68+
background: #2540ff;
69+
margin: 0 7px;
70+
}
71+
72+
.item:hover .working,
73+
.item.active .working {
74+
display: none;
75+
}

bricks/api-market/src/apis-directory-tree/wrapped-bricks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const WrappedMiniActions = wrapBrick<
5050
EoMiniActionsEventsMapping
5151
>("eo-mini-actions", {
5252
onActionClick: "action.click",
53+
onVisibleChange: "visible.change",
5354
});
5455
export const WrappedSearch = wrapBrick<
5556
GeneralSearch,

bricks/presentational/src/card-item/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const WrappedMiniActions = wrapBrick<
3030
EoMiniActionsEventsMapping
3131
>("eo-mini-actions", {
3232
onActionClick: "action.click",
33+
onVisibleChange: "visible.change",
3334
});
3435

3536
const ALLOWED_SHOW_ACTIONS = ["always", "hover"] as const;

0 commit comments

Comments
 (0)