|
1 | 1 | @inherits ElementComponentBase |
| 2 | +@inject ElementXRequestService RequestService |
2 | 3 |
|
3 | 4 | <section class="x-ai-demo"> |
4 | 5 | <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" /> |
7 | 18 | </aside> |
8 | 19 |
|
9 | 20 | <main class="x-ai-demo__main"> |
|
18 | 29 |
|
19 | 30 | <ElXPrompts Items="@Prompts" OnSelect="UsePrompt" /> |
20 | 31 |
|
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;" /> |
22 | 36 |
|
23 | 37 | <ElXThinking Title="模型思考" |
24 | 38 | Duration="1.2s" |
|
27 | 41 |
|
28 | 42 | <ElXThoughtChain Items="@Thoughts" /> |
29 | 43 |
|
| 44 | + <ElXAttachments Items="@Attachments" |
| 45 | + ItemsChanged="items => Attachments = items.ToList()" |
| 46 | + Drag="true" |
| 47 | + AutoUpload="false" |
| 48 | + UploadText="添加附件" |
| 49 | + EmptyContent="@EmptyAttachmentContent" /> |
| 50 | + |
30 | 51 | <ElXSender Value="@Draft" |
31 | 52 | ValueChanged="value => Draft = value" |
32 | 53 | OnSubmit="SendAsync" |
33 | 54 | OnStop="StopAsync" |
34 | 55 | Loading="@Sending" |
| 56 | + ShowClearButton="true" |
| 57 | + ClearOnSubmit="true" |
| 58 | + SubmitOnEnter="true" |
| 59 | + SubmitOnShiftEnter="false" |
| 60 | + HelpText="Enter 发送,Shift+Enter 换行;停止会取消当前流式请求。" |
35 | 61 | Placeholder="输入消息,Enter 发送,Shift+Enter 换行"> |
36 | 62 | <Prefix> |
37 | 63 | <ElIcon Name="chat-line-round" /> |
38 | 64 | </Prefix> |
39 | 65 | <Footer> |
40 | | - <ElButton Text="true" Icon="el-icon-paperclip">附件</ElButton> |
41 | 66 | <ElButton Text="true" Icon="el-icon-magic-stick">优化</ElButton> |
42 | 67 | </Footer> |
43 | 68 | </ElXSender> |
|
85 | 110 | private string Draft = "请继续实现 Sender 和附件上传。"; |
86 | 111 | private bool Sending; |
87 | 112 | private bool Thinking = true; |
| 113 | + private string activeRequestId; |
88 | 114 |
|
89 | | - private readonly List<XConversationItem> Conversations = new() |
| 115 | + private List<XConversationItem> Conversations = new() |
90 | 116 | { |
91 | 117 | new XConversationItem { Id = "roadmap", Title = "X 组件路线", Description = "组件矩阵与防偏约束", Group = "Today", Pinned = true }, |
92 | 118 | new XConversationItem { Id = "demo", Title = "AI 工作台", Description = "ruoyi-element-ai 复刻", Group = "Today" }, |
|
118 | 144 | } |
119 | 145 | }; |
120 | 146 |
|
| 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 | + |
121 | 152 | private readonly List<XThoughtItem> Thoughts = new() |
122 | 153 | { |
123 | 154 | new XThoughtItem { Title = "锁定依赖边界", Description = "只使用 Element-Blazor 和 Blazor 基础能力。", Duration = "0.2s", Status = "done", Type = "success", Icon = "check" }, |
124 | 155 | new XThoughtItem { Title = "拆分组件矩阵", Description = "展示、输入、附件、流式服务分阶段交付。", Duration = "0.6s", Status = "done", Type = "success", Icon = "tickets" }, |
125 | 156 | new XThoughtItem { Title = "构建离线演示", Description = "默认模拟会话和响应,不依赖真实后端。", Duration = "1.2s", Status = "running", Type = "primary", Icon = "loading" } |
126 | 157 | }; |
127 | 158 |
|
| 159 | + private RenderFragment EmptyAttachmentContent => builder => |
| 160 | + { |
| 161 | + builder.OpenElement(0, "span"); |
| 162 | + builder.AddContent(1, "可拖拽文件到这里,演示默认离线保存附件列表。"); |
| 163 | + builder.CloseElement(); |
| 164 | + }; |
| 165 | + |
128 | 166 | private void OnConversationChanged(string id) |
129 | 167 | { |
130 | 168 | ActiveConversationId = id; |
131 | 169 | } |
132 | 170 |
|
| 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 | + |
133 | 203 | private void UsePrompt(XPromptItem item) |
134 | 204 | { |
135 | 205 | Draft = item.Description; |
136 | 206 | } |
137 | 207 |
|
138 | 208 | private async Task SendAsync(string value) |
139 | 209 | { |
| 210 | + if (Sending || string.IsNullOrWhiteSpace(value)) |
| 211 | + { |
| 212 | + return; |
| 213 | + } |
| 214 | + |
140 | 215 | Sending = true; |
141 | 216 | 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 | + } |
147 | 259 | } |
148 | 260 |
|
149 | 261 | private Task StopAsync() |
150 | 262 | { |
| 263 | + if (!string.IsNullOrWhiteSpace(activeRequestId)) |
| 264 | + { |
| 265 | + RequestService.Cancel(activeRequestId); |
| 266 | + } |
| 267 | + |
151 | 268 | Sending = false; |
152 | 269 | Thinking = false; |
153 | 270 | return Task.CompletedTask; |
|
0 commit comments