namespace AIStudio.Chat; public static class IContentExtensions { /// The content of a message in the chat. extension(IContent content) { /// /// Detaches whoever listens to the stream of this content. /// /// /// The streaming handlers are closures over the component which registered them. A content /// object belongs to the chat thread and therefore outlives every component which renders it, /// so handlers left behind would keep those components alive for as long as the thread exists. /// Whoever registers a handler calls this when it is no longer needed. /// public void ResetStreamingHandlers() { content.StreamingEvent = IContent.NO_STREAMING_HANDLER; content.StreamingDone = IContent.NO_STREAMING_HANDLER; } /// /// Reads this content as the Markdown text the AI produced. /// /// /// Only text content carries Markdown. Everything else, an image for example, has no text /// representation at all, which is why this reports failure instead of returning a placeholder: /// a caller which writes files must not put an excuse into the file it writes. This is the text /// the model wrote and nothing else: whoever reads a table out of a message wants exactly that, /// while whoever writes a file wants the sources along with it and asks for the export reading. /// /// The Markdown text, or an empty string when there is none. /// True, when this content carries Markdown text. public bool TryGetMarkdownText(out string markdown) { if (content is ContentText text) { markdown = text.Text; return true; } markdown = string.Empty; return false; } /// /// Reads this content the way it leaves AI Studio, as a file or through the clipboard. /// /// /// What the user sees is the answer together with the sources AI Studio collected for it, and /// that is what a document has to hold as well: an answer built on a web page a tool read, or on /// a document of the user, is worth little when the reader cannot tell which one it was. Those /// sources are not part of the text the model wrote, they hang on the content, which is why /// every path out of the app asks for this and not for the text alone. /// /// The Markdown text including its sources, or an empty string when there is none. /// Whether a link into a local file may name its page. Only a /// format whose reader stumbles over such a link says no here; the clipboard and every text /// format keep the page. /// True, when this content carries Markdown text. public bool TryGetExportMarkdown(out string markdown, bool keepPageAnchors = true) { if (content is not ContentText text) { markdown = string.Empty; return false; } markdown = AppendSources(text.Text.Trim(), text.Sources.ToExportMarkdown(keepPageAnchors)); return true; } /// /// Reads one file out of this content the way it leaves AI Studio, together with the sources /// the answer rests on. /// /// /// A document saved on its own came out of the same answer, so it rests on the same sources /// and takes them along. How depends on the format. Markdown is what the source list is written /// in, so a Markdown text gets it just as the entire answer does. A web page or a LaTeX document /// gets it as a comment at its end: anything visible would have to be woven into markup the /// model wrote. A fragment has no body to put it in, a page may hide whatever lies outside its /// layout, and one underscore in a title is enough to stop a LaTeX run. A comment breaks /// neither, and whoever opens the file finds it. A table gets no sources at all, since it has /// no column a link list would fit into. Neither does code: a program, a script, or a /// configuration is meant for a compiler or a tool, and a comment of ours would be the one part /// of it nobody asked for. In some languages, such as JSON, it would even break the file. /// /// Apart from that, the file is what the model wrote, scripts of a web page included. Saving it /// is what the user chose to do; the chat still never renders it. Code only gets what its tools /// expect of a file, see ToSourceCodeFile. /// /// The file, as PlainFileExport.ExtractFiles read it out of this content. /// The content of the file to write. public string ToExportContent(MessageFile file) { if (file.Format.IsTabular()) return file.Content; if (file.Format is FileExportFormat.SOURCE_CODE) return ToSourceCodeFile(file); var sources = content.Sources.ToExportMarkdown(file.Format.FollowsPageAnchors()); if (file.Format is FileExportFormat.MARKDOWN) return AppendSources(file.Content, sources); if (sources.Length is 0 || !file.Format.TryToComment(sources, out var comment)) return file.Content; return $"{file.Content}{Environment.NewLine}{Environment.NewLine}{comment}"; } } /// /// Turns a code block into the file its tools expect. /// /// /// Markdig hands us the lines of a block without a line break after the last one. Compilers /// and shells read such a file, but Git marks it, and some tools drop the last line altogether. /// Between the lines, Markdig keeps the line breaks the answer came with: LF, CR LF, or a lone /// CR, the three that Markdown knows. Every line ends with LF, except where the language needs /// CR LF, so that the file never mixes two kinds. Only these three count as line breaks, unlike /// in ReplaceLineEndings: a form feed or a U+2028 may stand inside a string of the code. /// /// The code block. /// The content of the file to write. private static string ToSourceCodeFile(MessageFile file) { var text = file.Content.Replace("\r\n", "\n").Replace('\r', '\n'); var code = text.EndsWith('\n') ? text : $"{text}\n"; return file.Language.NeedsWindowsLineEndings ? code.Replace("\n", "\r\n") : code; } /// /// Puts the source list below a Markdown text. /// /// The Markdown text. /// The source list as SourceExtensions.ToExportMarkdown writes it, or an /// empty string when there are no sources. /// The text followed by its sources. private static string AppendSources(string markdown, string sources) => Markdown.AppendBlock(markdown, sources); }