feat: context fill grade in chat UI
This commit is contained in:
@@ -96,7 +96,7 @@ enum PromptBuilder {
|
||||
additionalContext: additionalContext
|
||||
)
|
||||
|
||||
let estimatedPromptTokens = (instructions.count + chatMessages.reduce(0) { $0 + $1.content.count }) * 10 / 35
|
||||
let estimatedPromptTokens = estimatePromptTokens(instructions: instructions, chatMessages: chatMessages)
|
||||
|
||||
return PreparedPrompt(
|
||||
instructions: instructions,
|
||||
@@ -111,6 +111,13 @@ enum PromptBuilder {
|
||||
)
|
||||
}
|
||||
|
||||
static func estimatePromptTokens(instructions: String, chatMessages: [Chat.Message]) -> Int {
|
||||
let characterCount = instructions.count + chatMessages.reduce(0) { partial, message in
|
||||
partial + message.content.count
|
||||
}
|
||||
return max(0, characterCount * 10 / 35)
|
||||
}
|
||||
|
||||
private static func imageFingerprint(_ source: String) -> UInt64 {
|
||||
var hash: UInt64 = 14_695_981_039_346_656_037
|
||||
for byte in source.utf8 {
|
||||
|
||||
@@ -49,6 +49,34 @@ final class ChatViewModel {
|
||||
hasUnsavedChanges ? "\(documentDisplayName) *" : documentDisplayName
|
||||
}
|
||||
|
||||
var currentContextLength: Int {
|
||||
modelManager.currentModel?.contextLength ?? 0
|
||||
}
|
||||
|
||||
var estimatedPromptTokens: Int {
|
||||
let draft = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var chatMessages = conversation.messages.compactMap(historyMessage(from:))
|
||||
if !draft.isEmpty {
|
||||
chatMessages.append(Chat.Message(role: .user, content: draft))
|
||||
}
|
||||
return PromptBuilder.estimatePromptTokens(
|
||||
instructions: effectiveSystemPrompt,
|
||||
chatMessages: chatMessages
|
||||
)
|
||||
}
|
||||
|
||||
var contextUsedTokens: Int {
|
||||
if isGenerating && (promptTokens > 0 || generationTokens > 0) {
|
||||
return promptTokens + generationTokens
|
||||
}
|
||||
return estimatedPromptTokens
|
||||
}
|
||||
|
||||
var contextFillRatio: Double {
|
||||
guard currentContextLength > 0 else { return 0 }
|
||||
return min(max(Double(contextUsedTokens) / Double(currentContextLength), 0), 1)
|
||||
}
|
||||
|
||||
/// Ensure a ChatSession exists for the current model.
|
||||
private func ensureSession() {
|
||||
guard let container = modelManager.modelContainer else { return }
|
||||
|
||||
@@ -31,6 +31,10 @@ struct StatusBarView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if let model = modelManager.currentModel, model.contextLength > 0 {
|
||||
contextFillView(totalContext: model.contextLength)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// GPU memory
|
||||
@@ -78,4 +82,43 @@ struct StatusBarView: View {
|
||||
.padding(.vertical, 4)
|
||||
.background(.bar)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func contextFillView(totalContext: Int) -> some View {
|
||||
let usedTokens = viewModel.contextUsedTokens
|
||||
let ratio = viewModel.contextFillRatio
|
||||
let percent = Int((ratio * 100).rounded())
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Capsule()
|
||||
.fill(.quaternary)
|
||||
.frame(width: 48, height: 6)
|
||||
.overlay(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(contextFillColor(for: ratio))
|
||||
.frame(width: max(4, 48 * ratio), height: 6)
|
||||
}
|
||||
|
||||
Text("Ctx \(percent)%")
|
||||
.font(.caption.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.help("Approximate context usage: \(formatTokenCount(usedTokens)) of \(formatTokenCount(totalContext)) tokens")
|
||||
}
|
||||
|
||||
private func contextFillColor(for ratio: Double) -> Color {
|
||||
if ratio >= 0.9 { return .red }
|
||||
if ratio >= 0.7 { return .orange }
|
||||
return .blue
|
||||
}
|
||||
|
||||
private func formatTokenCount(_ count: Int) -> String {
|
||||
if count >= 1_000_000 {
|
||||
return String(format: "%.1fM", Double(count) / 1_000_000)
|
||||
}
|
||||
if count >= 1_000 {
|
||||
return String(format: "%.1fk", Double(count) / 1_000)
|
||||
}
|
||||
return "\(count)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,20 @@ final class PromptBuilderTests: XCTestCase {
|
||||
XCTAssertEqual(prepared.additionalContext?["enable_thinking"] as? Bool, legacy.additionalContext?["enable_thinking"] as? Bool)
|
||||
}
|
||||
|
||||
func testEstimatePromptTokensMatchesSharedCharacterHeuristic() {
|
||||
let messages = [
|
||||
Chat.Message(role: .user, content: "1234567890"),
|
||||
Chat.Message(role: .assistant, content: "abcdefghij")
|
||||
]
|
||||
|
||||
let estimated = PromptBuilder.estimatePromptTokens(
|
||||
instructions: "system12345",
|
||||
chatMessages: messages
|
||||
)
|
||||
|
||||
XCTAssertEqual(estimated, 8)
|
||||
}
|
||||
|
||||
func testBuildAggregatesInstructionsAndMessages() {
|
||||
let request = APIChatCompletionRequest(
|
||||
model: "gemma",
|
||||
|
||||
Reference in New Issue
Block a user