Skip to content

Commit acee725

Browse files
committed
feat: 完善 X 组件矩阵示例,添加完整矩阵和流式服务功能
1 parent d92984c commit acee725

8 files changed

Lines changed: 549 additions & 28 deletions

File tree

X/AiWorkspace.razor

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
@inherits ElementComponentBase
2+
@inject ElementXRequestService RequestService
23

34
<section class="x-ai-demo">
45
<aside class="x-ai-demo__aside">
5-
<ElButton Type="@ButtonType.Primary" Icon="el-icon-plus" Style="width:100%;">新建会话</ElButton>
6-
<ElXConversations Items="@Conversations" ActiveId="@ActiveConversationId" ActiveIdChanged="OnConversationChanged" />
6+
<ElXConversations Items="@Conversations"
7+
ItemsChanged="OnConversationsChanged"
8+
ActiveId="@ActiveConversationId"
9+
ActiveIdChanged="OnConversationChanged"
10+
ShowCreate="true"
11+
ShowActions="true"
12+
CreateInline="true"
13+
RenameInline="true"
14+
DeleteRemovesItem="true"
15+
OnCreateConversation="OnConversationCreated"
16+
OnRenameConversation="OnConversationRenamed"
17+
OnDeleteConversation="OnConversationDeleted" />
718
</aside>
819

920
<main class="x-ai-demo__main">
@@ -18,7 +29,10 @@
1829

1930
<ElXPrompts Items="@Prompts" OnSelect="UsePrompt" />
2031

21-
<ElXBubbleList Items="@Messages" Style="min-height:360px;" />
32+
<ElXBubbleList Items="@Messages"
33+
AutoScrollOnStreaming="true"
34+
LockAutoScrollWhenUserScrolls="true"
35+
Style="min-height:360px;max-height:460px;" />
2236

2337
<ElXThinking Title="模型思考"
2438
Duration="1.2s"
@@ -27,17 +41,28 @@
2741

2842
<ElXThoughtChain Items="@Thoughts" />
2943

44+
<ElXAttachments Items="@Attachments"
45+
ItemsChanged="items => Attachments = items.ToList()"
46+
Drag="true"
47+
AutoUpload="false"
48+
UploadText="添加附件"
49+
EmptyContent="@EmptyAttachmentContent" />
50+
3051
<ElXSender Value="@Draft"
3152
ValueChanged="value => Draft = value"
3253
OnSubmit="SendAsync"
3354
OnStop="StopAsync"
3455
Loading="@Sending"
56+
ShowClearButton="true"
57+
ClearOnSubmit="true"
58+
SubmitOnEnter="true"
59+
SubmitOnShiftEnter="false"
60+
HelpText="Enter 发送,Shift+Enter 换行;停止会取消当前流式请求。"
3561
Placeholder="输入消息,Enter 发送,Shift+Enter 换行">
3662
<Prefix>
3763
<ElIcon Name="chat-line-round" />
3864
</Prefix>
3965
<Footer>
40-
<ElButton Text="true" Icon="el-icon-paperclip">附件</ElButton>
4166
<ElButton Text="true" Icon="el-icon-magic-stick">优化</ElButton>
4267
</Footer>
4368
</ElXSender>
@@ -85,8 +110,9 @@
85110
private string Draft = "请继续实现 Sender 和附件上传。";
86111
private bool Sending;
87112
private bool Thinking = true;
113+
private string activeRequestId;
88114

89-
private readonly List<XConversationItem> Conversations = new()
115+
private List<XConversationItem> Conversations = new()
90116
{
91117
new XConversationItem { Id = "roadmap", Title = "X 组件路线", Description = "组件矩阵与防偏约束", Group = "Today", Pinned = true },
92118
new XConversationItem { Id = "demo", Title = "AI 工作台", Description = "ruoyi-element-ai 复刻", Group = "Today" },
@@ -118,36 +144,127 @@
118144
}
119145
};
120146

147+
private List<XAttachmentItem> Attachments = new()
148+
{
149+
new XAttachmentItem { Id = "roadmap", FileName = "x-component-roadmap.md", Size = "9 KB", Type = "code", Status = XAttachmentStatus.Success }
150+
};
151+
121152
private readonly List<XThoughtItem> Thoughts = new()
122153
{
123154
new XThoughtItem { Title = "锁定依赖边界", Description = "只使用 Element-Blazor 和 Blazor 基础能力。", Duration = "0.2s", Status = "done", Type = "success", Icon = "check" },
124155
new XThoughtItem { Title = "拆分组件矩阵", Description = "展示、输入、附件、流式服务分阶段交付。", Duration = "0.6s", Status = "done", Type = "success", Icon = "tickets" },
125156
new XThoughtItem { Title = "构建离线演示", Description = "默认模拟会话和响应,不依赖真实后端。", Duration = "1.2s", Status = "running", Type = "primary", Icon = "loading" }
126157
};
127158

159+
private RenderFragment EmptyAttachmentContent => builder =>
160+
{
161+
builder.OpenElement(0, "span");
162+
builder.AddContent(1, "可拖拽文件到这里,演示默认离线保存附件列表。");
163+
builder.CloseElement();
164+
};
165+
128166
private void OnConversationChanged(string id)
129167
{
130168
ActiveConversationId = id;
131169
}
132170

171+
private void OnConversationsChanged(IEnumerable<XConversationItem> items)
172+
{
173+
Conversations = items?.ToList() ?? new List<XConversationItem>();
174+
}
175+
176+
private void OnConversationCreated(XConversationCreateEventArgs args)
177+
{
178+
if (args?.Item == null)
179+
{
180+
return;
181+
}
182+
183+
args.Item.Group = "Today";
184+
args.Item.Description = "新建的本地会话";
185+
}
186+
187+
private void OnConversationRenamed(XConversationRenameEventArgs args)
188+
{
189+
if (args?.Item != null)
190+
{
191+
args.Item.Description = "已重命名";
192+
}
193+
}
194+
195+
private void OnConversationDeleted(XConversationDeleteEventArgs args)
196+
{
197+
if (!string.IsNullOrWhiteSpace(args?.NextActiveId))
198+
{
199+
ActiveConversationId = args.NextActiveId;
200+
}
201+
}
202+
133203
private void UsePrompt(XPromptItem item)
134204
{
135205
Draft = item.Description;
136206
}
137207

138208
private async Task SendAsync(string value)
139209
{
210+
if (Sending || string.IsNullOrWhiteSpace(value))
211+
{
212+
return;
213+
}
214+
140215
Sending = true;
141216
Thinking = true;
142-
Messages.Add(new XMessageItem { Role = XMessageRole.User, Header = "You", Content = value, Footer = "sending" });
143-
await Task.Delay(200);
144-
Messages.Add(new XMessageItem { Role = XMessageRole.Assistant, Header = "Element X", Content = "这是本地模拟回复:后续会接入 ElementXStreamService 形成真正的流式输出。", Footer = "mock", AvatarIcon = "cpu" });
145-
Sending = false;
146-
Thinking = false;
217+
activeRequestId = Guid.NewGuid().ToString("N");
218+
var request = new XRequest
219+
{
220+
RequestId = activeRequestId,
221+
ConversationId = ActiveConversationId,
222+
Prompt = value,
223+
Messages = Messages.ToList(),
224+
Attachments = Attachments.ToList(),
225+
UserName = "You",
226+
AssistantName = "Element X"
227+
};
228+
229+
Messages.Add(RequestService.CreateUserMessage(request));
230+
var assistantMessage = RequestService.CreateAssistantMessage(request);
231+
Messages.Add(assistantMessage);
232+
233+
try
234+
{
235+
await foreach (var chunk in RequestService.StreamAsync(request))
236+
{
237+
if (!string.IsNullOrEmpty(chunk.Content))
238+
{
239+
assistantMessage.Content += chunk.Content;
240+
assistantMessage.Loading = false;
241+
await InvokeAsync(StateHasChanged);
242+
}
243+
}
244+
245+
assistantMessage.Footer = "streamed";
246+
}
247+
catch (OperationCanceledException)
248+
{
249+
assistantMessage.Footer = "stopped";
250+
}
251+
finally
252+
{
253+
assistantMessage.Loading = false;
254+
assistantMessage.Typing = false;
255+
Sending = false;
256+
Thinking = false;
257+
activeRequestId = null;
258+
}
147259
}
148260

149261
private Task StopAsync()
150262
{
263+
if (!string.IsNullOrWhiteSpace(activeRequestId))
264+
{
265+
RequestService.Cancel(activeRequestId);
266+
}
267+
151268
Sending = false;
152269
Thinking = false;
153270
return Task.CompletedTask;

0 commit comments

Comments
 (0)