我目前正在利用 OpenAI API 为 iOS 应用程序开发聊天机器人。我面临的挑战之一是动态创建表格网格。我的目标是在动态生成的网格中显示每个 NSAttributedString,镜像此处描述的结构:
我现在从我的聊天机器人响应(openai api)中得到的是像这样的换行符“---|---|---|---|--- |
”这不专业:
表格网格的代码需要出现在:
mutating internal func visitTable(_ table: Table) -> NSAttributedString {
let result = NSMutableAttributedString()
return result
}
这些是我当前的代码,我使用 Markdownosaur 来突出显示功能部分。我的问题集中在分隔行和列的线上。我正在寻求改进以实现专业外观的表格网格,类似于第一张图片中描绘的表格网格。如何增强表格线条的呈现,使外观更加美观?
//
// MarkdownAttributedStringParser.swift
// Besa
//
// Created by Artit on 12/01/24.
//
import Foundation
import UIKit
import Markdown
import Highlighter
/// Based on the source code from Christian Selig
/// https://github.com/christianselig/Markdownosaur/blob/main/Sources/Markdownosaur/Markdownosaur.swift
public struct MarkdownAttributedStringParser: MarkupVisitor {
let baseFontSize: CGFloat = UIFont.preferredFont(forTextStyle: .body).pointSize
let highlighter: Highlighter = {
let highlighter = Highlighter()!
highlighter.setTheme("stackoverflow-dark")
return highlighter
}()
let newLineFontSize: CGFloat = 12
public init() {}
public mutating func attributedString(from document: Document) -> NSAttributedString {
return visit(document)
}
mutating func parserResults(from document: Document) -> [ParserResult] {
var results = [ParserResult]()
var currentAttrString = NSMutableAttributedString()
func appendCurrentAttrString(tableData: [[NSAttributedString]]) {
if !currentAttrString.string.isEmpty {
let currentAttrStringToAppend = (try? AttributedString(currentAttrString, including: \.uiKit)) ?? AttributedString(stringLiteral: currentAttrString.string)
results.append(.init(attributedString: currentAttrStringToAppend, isCodeBlock: false, codeBlockLanguage: nil))
}
}
document.children.forEach { markup in
let attrString = visit(markup)
if let codeBlock = markup as? CodeBlock {
appendCurrentAttrString(tableData: [])
let attrStringToAppend = (try? AttributedString(attrString, including: \.uiKit)) ?? AttributedString(stringLiteral: attrString.string)
results.append(.init(attributedString: attrStringToAppend, isCodeBlock: true, codeBlockLanguage: codeBlock.language))
currentAttrString = NSMutableAttributedString()
} else {
currentAttrString.append(attrString)
}
}
appendCurrentAttrString(tableData: [])
return results
}
mutating public func defaultVisit(_ markup: Markup) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in markup.children {
result.append(visit(child))
}
return result
}
mutating public func visitText(_ text: Text) -> NSAttributedString {
return NSAttributedString(string: text.plainText, attributes: [.font: UIFont.systemFont(ofSize: baseFontSize, weight: .regular)])
}
mutating public func visitEmphasis(_ emphasis: Emphasis) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in emphasis.children {
result.append(visit(child))
}
result.applyEmphasis()
return result
}
mutating internal func visitTable(_ table: Table) -> NSAttributedString {
let result = NSMutableAttributedString()
return result
}
mutating public func visitStrong(_ strong: Strong) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in strong.children {
result.append(visit(child))
}
result.applyStrong()
return result
}
mutating public func visitParagraph(_ paragraph: Paragraph) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in paragraph.children {
result.append(visit(child))
}
if paragraph.hasSuccessor {
result.append(paragraph.isContainedInList ? .singleNewline(withFontSize: newLineFontSize) : .doubleNewline(withFontSize: newLineFontSize))
}
return result
}
mutating public func visitHeading(_ heading: Heading) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in heading.children {
result.append(visit(child))
}
result.applyHeading(withLevel: heading.level)
if heading.hasSuccessor {
result.append(.doubleNewline(withFontSize: newLineFontSize))
}
return result
}
mutating public func visitLink(_ link: Link) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in link.children {
result.append(visit(child))
}
let url = link.destination != nil ? URL(string: link.destination!) : nil
result.applyLink(withURL: url)
return result
}
mutating public func visitInlineCode(_ inlineCode: InlineCode) -> NSAttributedString {
return NSAttributedString(string: inlineCode.code, attributes: [.font: UIFont.monospacedSystemFont(ofSize: baseFontSize - 1.0, weight: .regular), .foregroundColor: UIColor.systemPink])
}
public func visitCodeBlock(_ codeBlock: CodeBlock) -> NSAttributedString {
let result = NSMutableAttributedString(attributedString: highlighter.highlight(codeBlock.code, as: codeBlock.language) ?? NSAttributedString(string: codeBlock.code))
if codeBlock.hasSuccessor {
result.append(.singleNewline(withFontSize: newLineFontSize))
}
return result
}
mutating public func visitStrikethrough(_ strikethrough: Strikethrough) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in strikethrough.children {
result.append(visit(child))
}
result.applyStrikethrough()
return result
}
mutating public func visitUnorderedList(_ unorderedList: UnorderedList) -> NSAttributedString {
let result = NSMutableAttributedString()
let font = UIFont.systemFont(ofSize: baseFontSize, weight: .regular)
for listItem in unorderedList.listItems {
var listItemAttributes: [NSAttributedString.Key: Any] = [:]
let listItemParagraphStyle = NSMutableParagraphStyle()
let baseLeftMargin: CGFloat = 15.0
let leftMarginOffset = baseLeftMargin + (20.0 * CGFloat(unorderedList.listDepth))
let spacingFromIndex: CGFloat = 8.0
let bulletWidth = ceil(NSAttributedString(string: "•", attributes: [.font: font]).size().width)
let firstTabLocation = leftMarginOffset + bulletWidth
let secondTabLocation = firstTabLocation + spacingFromIndex
listItemParagraphStyle.tabStops = [
NSTextTab(textAlignment: .right, location: firstTabLocation),
NSTextTab(textAlignment: .left, location: secondTabLocation)
]
listItemParagraphStyle.headIndent = secondTabLocation
listItemAttributes[.paragraphStyle] = listItemParagraphStyle
listItemAttributes[.font] = UIFont.systemFont(ofSize: baseFontSize, weight: .regular)
listItemAttributes[.listDepth] = unorderedList.listDepth
let listItemAttributedString = visit(listItem).mutableCopy() as! NSMutableAttributedString
listItemAttributedString.insert(NSAttributedString(string: "\t•\t", attributes: listItemAttributes), at: 0)
result.append(listItemAttributedString)
}
if unorderedList.hasSuccessor {
result.append(.doubleNewline(withFontSize: newLineFontSize))
}
return result
}
mutating public func visitListItem(_ listItem: ListItem) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in listItem.children {
result.append(visit(child))
}
if listItem.hasSuccessor {
result.append(.singleNewline(withFontSize: newLineFontSize))
}
return result
}
mutating public func visitOrderedList(_ orderedList: OrderedList) -> NSAttributedString {
let result = NSMutableAttributedString()
for (index, listItem) in orderedList.listItems.enumerated() {
var listItemAttributes: [NSAttributedString.Key: Any] = [:]
let font = UIFont.systemFont(ofSize: baseFontSize, weight: .regular)
let numeralFont = UIFont.monospacedDigitSystemFont(ofSize: baseFontSize, weight: .regular)
let listItemParagraphStyle = NSMutableParagraphStyle()
// Implement a base amount to be spaced from the left side at all times to better visually differentiate it as a list
let baseLeftMargin: CGFloat = 15.0
let leftMarginOffset = baseLeftMargin + (20.0 * CGFloat(orderedList.listDepth))
// Grab the highest number to be displayed and measure its width (yes normally some digits are wider than others but since we're using the numeral mono font all will be the same width in this case)
let highestNumberInList = orderedList.childCount
let numeralColumnWidth = ceil(NSAttributedString(string: "\(highestNumberInList).", attributes: [.font: numeralFont]).size().width)
let spacingFromIndex: CGFloat = 8.0
let firstTabLocation = leftMarginOffset + numeralColumnWidth
let secondTabLocation = firstTabLocation + spacingFromIndex
listItemParagraphStyle.tabStops = [
NSTextTab(textAlignment: .right, location: firstTabLocation),
NSTextTab(textAlignment: .left, location: secondTabLocation)
]
listItemParagraphStyle.headIndent = secondTabLocation
listItemAttributes[.paragraphStyle] = listItemParagraphStyle
listItemAttributes[.font] = font
listItemAttributes[.listDepth] = orderedList.listDepth
let listItemAttributedString = visit(listItem).mutableCopy() as! NSMutableAttributedString
// Same as the normal list attributes, but for prettiness in formatting we want to use the cool monospaced numeral font
var numberAttributes = listItemAttributes
numberAttributes[.font] = numeralFont
let numberAttributedString = NSAttributedString(string: "\t\(index + 1).\t", attributes: numberAttributes)
listItemAttributedString.insert(numberAttributedString, at: 0)
result.append(listItemAttributedString)
}
if orderedList.hasSuccessor {
result.append(orderedList.isContainedInList ? .singleNewline(withFontSize: newLineFontSize) : .doubleNewline(withFontSize: newLineFontSize))
}
return result
}
mutating public func visitBlockQuote(_ blockQuote: BlockQuote) -> NSAttributedString {
let result = NSMutableAttributedString()
for child in blockQuote.children {
var quoteAttributes: [NSAttributedString.Key: Any] = [:]
let quoteParagraphStyle = NSMutableParagraphStyle()
let baseLeftMargin: CGFloat = 15.0
let leftMarginOffset = baseLeftMargin + (20.0 * CGFloat(blockQuote.quoteDepth))
quoteParagraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: leftMarginOffset)]
quoteParagraphStyle.headIndent = leftMarginOffset
quoteAttributes[.paragraphStyle] = quoteParagraphStyle
quoteAttributes[.font] = UIFont.systemFont(ofSize: baseFontSize, weight: .regular)
quoteAttributes[.listDepth] = blockQuote.quoteDepth
let quoteAttributedString = visit(child).mutableCopy() as! NSMutableAttributedString
quoteAttributedString.insert(NSAttributedString(string: "\t", attributes: quoteAttributes), at: 0)
quoteAttributedString.addAttribute(.foregroundColor, value: UIColor.systemGray)
result.append(quoteAttributedString)
}
if blockQuote.hasSuccessor {
result.append(.doubleNewline(withFontSize: newLineFontSize))
}
return result
}
}
// MARK: - Extensions Land
extension NSMutableAttributedString {
func applyEmphasis() {
enumerateAttribute(.font, in: NSRange(location: 0, length: length), options: []) { value, range, stop in
guard let font = value as? UIFont else { return }
let newFont = font.apply(newTraits: .traitItalic)
addAttribute(.font, value: newFont, range: range)
}
}
func applyStrong() {
enumerateAttribute(.font, in: NSRange(location: 0, length: length), options: []) { value, range, stop in
guard let font = value as? UIFont else { return }
let newFont = font.apply(newTraits: .traitBold)
addAttribute(.font, value: newFont, range: range)
}
}
func applyLink(withURL url: URL?) {
addAttribute(.foregroundColor, value: UIColor.systemBlue)
if let url = url {
addAttribute(.link, value: url)
}
}
func applyBlockquote() {
addAttribute(.foregroundColor, value: UIColor.systemGray)
}
func applyHeading(withLevel headingLevel: Int) {
enumerateAttribute(.font, in: NSRange(location: 0, length: length), options: []) { value, range, stop in
guard let font = value as? UIFont else { return }
let newFont = font.apply(newTraits: .traitBold, newPointSize: 28.0 - CGFloat(headingLevel * 2))
addAttribute(.font, value: newFont, range: range)
}
}
func applyStrikethrough() {
addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue)
}
}
extension UIFont {
func apply(newTraits: UIFontDescriptor.SymbolicTraits, newPointSize: CGFloat? = nil) -> UIFont {
var existingTraits = fontDescriptor.symbolicTraits
existingTraits.insert(newTraits)
guard let newFontDescriptor = fontDescriptor.withSymbolicTraits(existingTraits) else { return self }
return UIFont(descriptor: newFontDescriptor, size: newPointSize ?? pointSize)
}
}
extension ListItemContainer {
/// Depth of the list if nested within others. Index starts at 0.
var listDepth: Int {
var index = 0
var currentElement = parent
while currentElement != nil {
if currentElement is ListItemContainer {
index += 1
}
currentElement = currentElement?.parent
}
return index
}
}
extension BlockQuote {
/// Depth of the quote if nested within others. Index starts at 0.
var quoteDepth: Int {
var index = 0
var currentElement = parent
while currentElement != nil {
if currentElement is BlockQuote {
index += 1
}
currentElement = currentElement?.parent
}
return index
}
}
extension NSAttributedString.Key {
static let listDepth = NSAttributedString.Key("ListDepth")
static let quoteDepth = NSAttributedString.Key("QuoteDepth")
}
extension NSMutableAttributedString {
func addAttribute(_ name: NSAttributedString.Key, value: Any) {
addAttribute(name, value: value, range: NSRange(location: 0, length: length))
}
func addAttributes(_ attrs: [NSAttributedString.Key : Any]) {
addAttributes(attrs, range: NSRange(location: 0, length: length))
}
}
extension Markup {
/// Returns true if this element has sibling elements after it.
var hasSuccessor: Bool {
guard let childCount = parent?.childCount else { return false }
return indexInParent < childCount - 1
}
var isContainedInList: Bool {
var currentElement = parent
while currentElement != nil {
if currentElement is ListItemContainer {
return true
}
currentElement = currentElement?.parent
}
return false
}
}
extension NSAttributedString {
static func singleNewline(withFontSize fontSize: CGFloat) -> NSAttributedString {
return NSAttributedString(string: "\n", attributes: [.font: UIFont.systemFont(ofSize: fontSize)])
}
static func doubleNewline(withFontSize fontSize: CGFloat) -> NSAttributedString {
return NSAttributedString(string: "\n\n", attributes: [.font: UIFont.systemFont(ofSize: fontSize, weight: .regular)])
}
}
这是您正在使用的 API,因此我建议您使用更典型的格式。
我做了类似的事情,这使得客户端 iOS 应用程序更容易使用。
const result = await openai.chat.completions.create({
messages: [
{role: "user", content: message},
{role: "system", content: systemMessage},
],
model: "gpt-3.5-turbo",
});
用户消息是从聊天中输入的内容。 下面是一个与用户消息一致的 systemMessage 示例。 关键点,这确保返回给客户端的响应是 JSON 格式。
You will be provided with unstructured data on subject the user wants to learn about. Gather information on the subjects and provide 10 questions with multiple choices in the following json format
{"subjects": [ {"name":"<Subject Name>","questions": [ {"question":"<Actual Information>","answer":"<Answer Value>","answerOptions": ["<Possible Answers>" ] } ] } ]}
(这是一个多项选择琐事生成器,但我相信您可以想出一些满足您场景的东西。)
之后,我只是序列化数据,并将强类型数据加载到我的视图中。