| 1 | #nullable enable |
| 2 | using CascadeIDE.Views.SkiaKit; |
| 3 | using SkiaSharp; |
| 4 | |
| 5 | namespace CascadeIDE.Views.Chat.Skia; |
| 6 | |
| 7 | /// <summary> |
| 8 | /// Карточка темы в картотеке (overview). Отдельный chrome: <see cref="SkiaTopicCard"/>, |
| 9 | /// не <see cref="SkiaChatBubbleKind.CardPanel"/> (product spine — см. README.md). |
| 10 | /// </summary> |
| 11 | internal sealed class SkiaChatTopicCard( |
| 12 | SkiaTopicCardModel model, |
| 13 | Guid threadId) : ISkiaChatEntity, IGridTileSkiaChatEntity |
| 14 | { |
| 15 | public static SkiaChatTopicCard ForThread( |
| 16 | string title, |
| 17 | string summary, |
| 18 | string? tagFooter, |
| 19 | bool isFocused, |
| 20 | Guid threadId) => |
| 21 | new( |
| 22 | SkiaTopicCard.Create(title, summary, tagFooter, isFocused, maxChars: 80), |
| 23 | threadId); |
| 24 | |
| 25 | public static SkiaChatTopicCard ForSpine( |
| 26 | string lineTitle, |
| 27 | string currentFocus, |
| 28 | IReadOnlyList<string> milestones, |
| 29 | bool includeInAgentContext) => |
| 30 | new( |
| 31 | SkiaTopicCard.CreateSpine(lineTitle, currentFocus, milestones, includeInAgentContext, maxChars: 80), |
| 32 | Guid.Empty); |
| 33 | |
| 34 | public bool IsGridTile => threadId != Guid.Empty; |
| 35 | |
| 36 | public SkiaChatMeasuredLayout Measure(SkiaChatMeasureContext context) |
| 37 | { |
| 38 | var maxChars = Math.Max(16, (int)(context.ContentWidth / 7.1f)); |
| 39 | var measuredModel = RemeasureModel(maxChars); |
| 40 | var cardLayout = SkiaTopicCard.Measure(measuredModel); |
| 41 | return new SkiaChatMeasuredLayout( |
| 42 | cardLayout.Height, |
| 43 | cardLayout.GapAfter, |
| 44 | TopicCard: measuredModel, |
| 45 | TopicCardLayout: cardLayout); |
| 46 | } |
| 47 | |
| 48 | public void Draw(SkiaChatDrawContext context, float top, in SkiaChatMeasuredLayout layout) |
| 49 | { |
| 50 | var model = layout.TopicCard!; |
| 51 | var cardLayout = layout.TopicCardLayout!.Value; |
| 52 | var rect = new SKRect(context.ContentLeft, top, context.ContentLeft + context.ContentWidth, top + layout.Height); |
| 53 | SkiaTopicCard.Draw( |
| 54 | context.Canvas, |
| 55 | context.Theme, |
| 56 | rect, |
| 57 | context.ContentLeft + cardLayout.ContentInsetX, |
| 58 | context.ContentWidth - cardLayout.ContentInsetX * 2, |
| 59 | model, |
| 60 | cardLayout, |
| 61 | new SkiaTopicCardDrawState(context.IsHovered, model.IsFocused)); |
| 62 | } |
| 63 | |
| 64 | public SkiaChatHit? CreateHit(in SkiaChatMeasuredLayout layout) => |
| 65 | threadId == Guid.Empty |
| 66 | ? null |
| 67 | : new(MessageIndex: null, SelectThreadId: threadId, ResetDetailMode: false); |
| 68 | |
| 69 | private SkiaTopicCardModel RemeasureModel(int maxChars) |
| 70 | { |
| 71 | var sections = model.Card.Sections; |
| 72 | if (threadId == Guid.Empty) |
| 73 | { |
| 74 | var title = sections.Count > 0 && sections[0].Lines.Count > 0 |
| 75 | ? sections[0].Lines[0].TrimStart('↗', ' ') |
| 76 | : ""; |
| 77 | var tagSection = sections.Count > 1 ? sections[1].Lines : []; |
| 78 | var summary = sections.Count > 2 ? string.Join(' ', sections[2].Lines) : ""; |
| 79 | var milestones = tagSection |
| 80 | .Where(t => t is not "—" and not "в контексте агента" and not "не в контексте агента") |
| 81 | .ToList(); |
| 82 | var include = tagSection.Contains("в контексте агента"); |
| 83 | return SkiaTopicCard.CreateSpine(title, summary, milestones, include, maxChars); |
| 84 | } |
| 85 | |
| 86 | var topicTitle = sections.Count > 0 && sections[0].Lines.Count > 0 ? sections[0].Lines[0] : ""; |
| 87 | var summaryText = sections.Count > 2 ? string.Join(' ', sections[2].Lines) : ""; |
| 88 | var tagLines = sections.Count > 1 ? sections[1].Lines : []; |
| 89 | var tags = tagLines.Count == 1 && tagLines[0] == "—" |
| 90 | ? null |
| 91 | : string.Join(" · ", tagLines); |
| 92 | return SkiaTopicCard.Create(topicTitle, summaryText, tags, model.IsFocused, maxChars); |
| 93 | } |
| 94 | } |
| 95 | |