Created
May 23, 2025 14:05
-
-
Save hanrw/dd1acac47ebbc9a7a2f686d1335ead78 to your computer and use it in GitHub Desktop.
ThinkingIndicatorView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct ThinkingIndicatorView: View { | |
@Binding var isExpanded: Bool | |
let streamingText: String | |
let thinkingDuration: TimeInterval | |
@State private var animationOffset: CGFloat = 0 | |
var body: some View { | |
VStack(alignment: .leading, spacing: 12) { | |
// Thinking bubble header | |
HStack(spacing: 12) { | |
// AI avatar | |
Circle() | |
.fill( | |
LinearGradient( | |
colors: [.blue, .purple], | |
startPoint: .topLeading, | |
endPoint: .bottomTrailing | |
) | |
) | |
.frame(width: 32, height: 32) | |
.overlay( | |
Image(systemName: "brain.head.profile") | |
.font(.system(size: 16, weight: .medium)) | |
.foregroundStyle(.white) | |
) | |
VStack(alignment: .leading, spacing: 2) { | |
HStack { | |
Text("Thought for \(Int(thinkingDuration)) second\(thinkingDuration >= 2 ? "s" : "")") | |
.font(.callout.weight(.medium)) | |
.foregroundStyle(.blue) | |
// Animated thinking dots | |
HStack(spacing: 2) { | |
ForEach(0..<3, id: \.self) { index in | |
Circle() | |
.fill(.blue.opacity(0.6)) | |
.frame(width: 4, height: 4) | |
.scaleEffect(1 + sin(animationOffset + Double(index) * 0.5) * 0.5) | |
} | |
} | |
.onAppear { | |
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { | |
animationOffset = .pi * 2 | |
} | |
} | |
} | |
Button(action: { | |
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) { | |
isExpanded.toggle() | |
} | |
}) { | |
HStack(spacing: 4) { | |
Text("Tap to read my mind") | |
.font(.caption) | |
.foregroundStyle(.secondary) | |
Image(systemName: isExpanded ? "chevron.up" : "chevron.down") | |
.font(.caption2) | |
.foregroundStyle(.secondary) | |
} | |
} | |
.buttonStyle(.plain) | |
} | |
Spacer() | |
} | |
// Expandable content area | |
if isExpanded { | |
VStack(alignment: .leading, spacing: 8) { | |
HStack { | |
Rectangle() | |
.fill(.blue.opacity(0.3)) | |
.frame(width: 3) | |
VStack(alignment: .leading, spacing: 8) { | |
Text("Thinking Process") | |
.font(.caption.weight(.semibold)) | |
.foregroundStyle(.secondary) | |
if !streamingText.isEmpty { | |
StreamingTextView(text: streamingText) | |
} else { | |
Text("Analyzing your request and formulating a response...") | |
.font(.caption) | |
.foregroundStyle(.secondary) | |
.italic() | |
} | |
} | |
Spacer() | |
} | |
} | |
.padding(.vertical, 12) | |
.padding(.horizontal, 16) | |
.background( | |
RoundedRectangle(cornerRadius: 12) | |
.fill(.blue.opacity(0.05)) | |
.overlay( | |
RoundedRectangle(cornerRadius: 12) | |
.stroke(.blue.opacity(0.2), lineWidth: 1) | |
) | |
) | |
.transition(.asymmetric( | |
insertion: .scale(scale: 0.95).combined(with: .opacity), | |
removal: .scale(scale: 0.95).combined(with: .opacity) | |
)) | |
} | |
} | |
.padding(.horizontal, 16) | |
.padding(.vertical, 12) | |
.background( | |
RoundedRectangle(cornerRadius: 16) | |
.fill(.regularMaterial) | |
.overlay( | |
RoundedRectangle(cornerRadius: 16) | |
.stroke(.blue.opacity(0.3), lineWidth: 1.5) | |
) | |
) | |
.shadow(color: .blue.opacity(0.1), radius: 8, x: 0, y: 4) | |
} | |
} | |
// MARK: - Streaming Text View | |
struct StreamingTextView: View { | |
let text: String | |
@State private var displayedText = "" | |
@State private var showCursor = true | |
var body: some View { | |
HStack(alignment: .top) { | |
Text(displayedText) | |
.font(.caption) | |
.foregroundStyle(.primary) | |
.textSelection(.enabled) | |
// Typing cursor | |
if showCursor { | |
Rectangle() | |
.fill(.blue) | |
.frame(width: 2, height: 12) | |
.opacity(showCursor ? 1 : 0) | |
.animation(.easeInOut(duration: 0.5).repeatForever(autoreverses: true), value: showCursor) | |
} | |
Spacer() | |
} | |
.onChange(of: text) { newText in | |
displayedText = newText | |
} | |
.onAppear { | |
startCursorAnimation() | |
} | |
} | |
private func startCursorAnimation() { | |
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in | |
showCursor.toggle() | |
} | |
} | |
} | |
#Preview { | |
ThinkingIndicatorView( | |
isExpanded: .constant(true), | |
streamingText: "This is a sample streaming text that would appear as the AI thinks through the problem...", | |
thinkingDuration: 3.5 | |
) | |
.padding() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment