Files
happy-life-star/EmotionMuseum/EmotionMuseum/Views/InsightView.swift
T

1307 lines
45 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// InsightView.swift
// EmotionMuseum
//
// Created by EmotionMuseum on 2024/01/01.
//
import SwiftUI
import Foundation
import AVFoundation
#if canImport(UIKit)
import UIKit
#endif
#if canImport(AppKit)
import AppKit
#endif
// MARK: -
struct ThemeColors {
static let wechatGreen = Color(red: 0.1, green: 0.7, blue: 0.3)
static let lightBackground = Color(red: 0.95, green: 0.95, blue: 0.97)
static let darkBackground = Color(red: 0.1, green: 0.1, blue: 0.12)
#if canImport(UIKit)
static let cardBackground = Color(UIColor.systemBackground)
static let secondaryBackground = Color(UIColor.secondarySystemBackground)
static let textPrimary = Color(UIColor.label)
static let textSecondary = Color(UIColor.secondaryLabel)
#elseif canImport(AppKit)
static let cardBackground = Color(NSColor.controlBackgroundColor)
static let secondaryBackground = Color(NSColor.unemphasizedSelectedContentBackgroundColor)
static let textPrimary = Color(NSColor.labelColor)
static let textSecondary = Color(NSColor.secondaryLabelColor)
#else
static let cardBackground = Color.white
static let secondaryBackground = Color.gray.opacity(0.1)
static let textPrimary = Color.primary
static let textSecondary = Color.secondary
#endif
static let accent = Color.accentColor
}
// MARK: -
struct MoodData: Identifiable {
let id = UUID()
let emoji: String
let name: String
let color: Color
}
// MARK: - AI
struct AIConversation: Identifiable {
let id = UUID()
let date: Date
let userMessage: String
let aiResponse: String
let mood: String
let tags: [String]
}
// MARK: -
struct MoodPickerSheet: View {
@Binding var selectedMood: String
@Binding var isPresented: Bool
let selectedDate: Date
let moods: [MoodData] = [
MoodData(emoji: "😊", name: "开心", color: .yellow),
MoodData(emoji: "😢", name: "难过", color: .blue),
MoodData(emoji: "😡", name: "愤怒", color: .red),
MoodData(emoji: "😴", name: "疲惫", color: .gray),
MoodData(emoji: "🤔", name: "思考", color: .purple),
MoodData(emoji: "😍", name: "兴奋", color: .pink),
MoodData(emoji: "😰", name: "焦虑", color: .orange),
MoodData(emoji: "😌", name: "平静", color: .green)
]
var body: some View {
NavigationView {
VStack(spacing: 24) {
VStack(spacing: 8) {
Text(DateFormatter.fullDate.string(from: selectedDate))
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(ThemeColors.textPrimary)
Text("选择今日心情")
.font(.subheadline)
.foregroundColor(ThemeColors.textSecondary)
}
.padding(.top)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) {
ForEach(moods) { mood in
Button(action: {
selectedMood = mood.emoji
isPresented = false
}) {
VStack(spacing: 12) {
Text(mood.emoji)
.font(.system(size: 40))
Text(mood.name)
.font(.headline)
.foregroundColor(ThemeColors.textPrimary)
}
.frame(maxWidth: .infinity)
.padding(20)
.background(mood.color.opacity(0.1))
.cornerRadius(16)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(mood.color.opacity(0.3), lineWidth: 1)
)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(.horizontal)
Spacer()
}
.background(ThemeColors.lightBackground.ignoresSafeArea())
.navigationTitle("")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
#if os(iOS)
ToolbarItem(placement: .topBarTrailing) {
Button("取消") {
isPresented = false
}
.foregroundColor(ThemeColors.wechatGreen)
}
#else
ToolbarItem(placement: .primaryAction) {
Button("取消") {
isPresented = false
}
.foregroundColor(ThemeColors.wechatGreen)
}
#endif
}
}
}
}
// MARK: -
struct SingleRowCalendar: View {
@Binding var selectedDate: Date
@State private var currentWeekOffset = 0
@State private var isExpanded = false
let onDateTap: (Date) -> Void
private var calendar: Calendar {
Calendar.current
}
private var weekDays: [Date] {
if isExpanded {
//
let today = Date()
let startOfMonth = calendar.date(byAdding: .day, value: -30, to: today) ?? today
return (0..<31).compactMap {
calendar.date(byAdding: .day, value: $0, to: startOfMonth)
}
} else {
//
let startOfWeek = calendar.dateInterval(of: .weekOfYear, for: selectedDate)?.start ?? Date()
return (0..<7).compactMap {
calendar.date(byAdding: .day, value: $0 + currentWeekOffset * 7, to: startOfWeek)
}
}
}
var body: some View {
VStack(spacing: 12) {
HStack {
if !isExpanded {
Button(action: { currentWeekOffset -= 1 }) {
Image(systemName: "chevron.left")
.foregroundColor(ThemeColors.wechatGreen)
}
}
Spacer()
Text(DateFormatter.monthYear.string(from: selectedDate))
.font(.headline)
.foregroundColor(ThemeColors.textPrimary)
Spacer()
if !isExpanded {
Button(action: { currentWeekOffset += 1 }) {
Image(systemName: "chevron.right")
.foregroundColor(ThemeColors.wechatGreen)
}
}
Button(action: {
withAnimation(.spring()) {
isExpanded.toggle()
}
}) {
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
.foregroundColor(ThemeColors.wechatGreen)
.padding(.leading, 8)
}
}
if isExpanded {
//
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 8) {
//
ForEach(["", "", "", "", "", "", ""], id: \.self) { weekday in
Text(weekday)
.font(.caption)
.foregroundColor(ThemeColors.textSecondary)
}
//
ForEach(weekDays, id: \.self) { date in
VStack(spacing: 4) {
Button(action: {
selectedDate = date
onDateTap(date)
//
withAnimation(.spring()) {
isExpanded = false
}
}) {
ZStack {
Circle()
.fill(calendar.isDate(date, inSameDayAs: selectedDate) ?
ThemeColors.wechatGreen : Color.clear)
.frame(width: 36, height: 36)
Text("\(calendar.component(.day, from: date))")
.font(.system(size: 16, weight: .medium))
.foregroundColor(
calendar.isDate(date, inSameDayAs: selectedDate) ?
.white : ThemeColors.textPrimary
)
}
}
//
Text(moodIconForDate(date))
.font(.caption)
}
}
}
} else {
//
HStack(spacing: 0) {
ForEach(weekDays, id: \.self) { date in
VStack(spacing: 4) {
Text(DateFormatter.weekday.string(from: date))
.font(.caption)
.foregroundColor(ThemeColors.textSecondary)
Button(action: {
selectedDate = date
onDateTap(date)
}) {
ZStack {
Circle()
.fill(calendar.isDate(date, inSameDayAs: selectedDate) ?
ThemeColors.wechatGreen : Color.clear)
.frame(width: 36, height: 36)
Text("\(calendar.component(.day, from: date))")
.font(.system(size: 16, weight: .medium))
.foregroundColor(
calendar.isDate(date, inSameDayAs: selectedDate) ?
.white : ThemeColors.textPrimary
)
}
}
//
Text(moodIconForDate(date))
.font(.caption)
}
.frame(maxWidth: .infinity)
}
}
}
}
.padding()
.background(ThemeColors.cardBackground)
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
.animation(.spring(), value: currentWeekOffset)
}
private func moodIconForDate(_ date: Date) -> String {
//
let day = Calendar.current.component(.day, from: date)
let moods = ["😊", "😢", "😡", "😴", "🤔", "😍", "😰"]
return day % 3 == 0 ? moods[day % moods.count] : ""
}
}
// MARK: - AI
struct AIConversationCard: View {
let conversation: AIConversation
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(DateFormatter.conversationDate.string(from: conversation.date))
.font(.caption)
.foregroundColor(ThemeColors.textSecondary)
Spacer()
Text(conversation.mood)
.font(.title3)
}
VStack(alignment: .leading, spacing: 8) {
Text("我: \(conversation.userMessage)")
.font(.body)
.foregroundColor(ThemeColors.textPrimary)
.padding(12)
.background(ThemeColors.secondaryBackground)
.cornerRadius(12)
Text("AI: \(conversation.aiResponse)")
.font(.body)
.foregroundColor(ThemeColors.textPrimary)
.padding(12)
.background(ThemeColors.wechatGreen.opacity(0.1))
.cornerRadius(12)
}
if !conversation.tags.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(conversation.tags, id: \.self) { tag in
Text(tag)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(ThemeColors.accent.opacity(0.2))
.cornerRadius(8)
}
}
.padding(.horizontal)
}
}
}
.padding()
.background(ThemeColors.cardBackground)
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
}
}
// MARK: -
class AudioManager: NSObject, ObservableObject, AVAudioRecorderDelegate {
static let shared = AudioManager()
@Published var isRecording = false
@Published var recordingDuration: TimeInterval = 0
@Published var currentTime: TimeInterval = 0
private var audioRecorder: AVAudioRecorder?
private var audioPlayer: AVAudioPlayer?
private var displayLink: CADisplayLink?
private var recordingStartTime: Date?
private var durationTimer: Timer?
override init() {
super.init()
setupAudioSession()
}
private func setupAudioSession() {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord, mode: .default)
try session.setActive(true)
} catch {
print("音频会话设置失败: \(error)")
}
}
func startRecording() {
let audioFilename = getDocumentsDirectory().appendingPathComponent("\(UUID().uuidString).m4a")
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
do {
audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
audioRecorder?.delegate = self
audioRecorder?.record()
isRecording = true
recordingStartTime = Date()
//
startDurationTimer()
} catch {
print("录音失败: \(error)")
}
}
func stopRecording() -> (URL, TimeInterval)? {
audioRecorder?.stop()
let url = audioRecorder?.url
let duration = recordingDuration
audioRecorder = nil
isRecording = false
//
stopDurationTimer()
recordingDuration = 0
if let url = url {
return (url, duration)
}
return nil
}
private func startDurationTimer() {
durationTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
guard let self = self else { return }
if let startTime = self.recordingStartTime {
self.recordingDuration = Date().timeIntervalSince(startTime)
}
}
}
private func stopDurationTimer() {
durationTimer?.invalidate()
durationTimer = nil
recordingStartTime = nil
}
func playAudio(url: URL, completion: @escaping () -> Void) {
stopCurrentAudio()
do {
audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer?.delegate = self
audioPlayer?.play()
//
startPlaybackTimer()
//
DispatchQueue.main.asyncAfter(deadline: .now() + (audioPlayer?.duration ?? 0)) {
completion()
}
} catch {
print("音频播放失败: \(error)")
}
}
func stopCurrentAudio() {
audioPlayer?.stop()
audioPlayer = nil
stopPlaybackTimer()
currentTime = 0
}
private func startPlaybackTimer() {
displayLink = CADisplayLink(target: self, selector: #selector(updatePlaybackTime))
displayLink?.add(to: .main, forMode: .common)
}
private func stopPlaybackTimer() {
displayLink?.invalidate()
displayLink = nil
}
@objc private func updatePlaybackTime() {
currentTime = audioPlayer?.currentTime ?? 0
}
private func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
}
extension AudioManager: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
stopCurrentAudio()
}
}
// MARK: -
struct AudioMessageView: View {
let url: URL
let duration: TimeInterval
@StateObject private var audioManager = AudioManager.shared
@State private var isPlaying = false
@State private var progress: CGFloat = 0
@State private var isAnimating = false
var body: some View {
Button(action: {
if isPlaying {
audioManager.stopCurrentAudio()
isPlaying = false
} else {
isPlaying = true
audioManager.playAudio(url: url) {
isPlaying = false
}
}
}) {
HStack(spacing: 8) {
// /
Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
.font(.title2)
.foregroundColor(ThemeColors.wechatGreen)
//
HStack(spacing: 2) {
ForEach(0..<5) { index in
RoundedRectangle(cornerRadius: 1)
.frame(width: 2, height: isPlaying ? 15 : 8)
.animation(
Animation.easeInOut(duration: 0.5)
.repeatForever()
.delay(Double(index) * 0.1),
value: isPlaying
)
}
}
.frame(width: 80)
//
Text(formatDuration(duration))
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.background(Color.secondary.opacity(0.1))
.cornerRadius(12)
}
}
private func formatDuration(_ duration: TimeInterval) -> String {
let minutes = Int(duration) / 60
let seconds = Int(duration) % 60
return String(format: "%d:%02d", minutes, seconds)
}
}
// MARK: -
struct ChatMessage: Identifiable {
let id: UUID
let content: String
let isUser: Bool
let timestamp: Date
let messageType: MessageType
let audioURL: URL?
let audioDuration: TimeInterval
enum MessageType {
case text
case audio
}
init(id: UUID = UUID(), content: String, isUser: Bool, timestamp: Date, messageType: MessageType, audioURL: URL? = nil, audioDuration: TimeInterval = 0) {
self.id = id
self.content = content
self.isUser = isUser
self.timestamp = timestamp
self.messageType = messageType
self.audioURL = audioURL
self.audioDuration = audioDuration
}
}
// MARK: -
struct ChatMessageView: View {
let message: ChatMessage
var body: some View {
HStack {
if message.isUser {
Spacer()
}
VStack(alignment: message.isUser ? .trailing : .leading, spacing: 4) {
if message.messageType == .audio {
if let audioURL = message.audioURL {
AudioMessageView(url: audioURL, duration: message.audioDuration)
}
} else {
Text(message.content)
.padding(12)
.background(
message.isUser ?
ThemeColors.wechatGreen : ThemeColors.secondaryBackground
)
.foregroundColor(message.isUser ? .white : ThemeColors.textPrimary)
.cornerRadius(16)
}
Text(DateFormatter.conversationDate.string(from: message.timestamp))
.font(.caption2)
.foregroundColor(ThemeColors.textSecondary)
}
if !message.isUser {
Spacer()
}
}
}
}
// MARK: - AI
struct AIChatView: View {
@Binding var isPresented: Bool
@State private var isVoiceMode = false
@State private var inputText = ""
@State private var chatMessages: [ChatMessage] = []
@StateObject private var audioManager = AudioManager.shared
let initialMessage: String
@State private var recordingFeedback = false
private let aiResponses = [
"我理解你现在的感受。让我们一起探讨这个问题,看看有什么可以帮助你的方式。",
"你说得很有道理。这种情况下,我建议你可以试着换个角度来看待这个问题。",
"听起来这确实是个令人困扰的情况。不过别担心,我们可以一步一步地来解决它。",
"你的感受是完全正常的。在这种情况下,很多人都会有类似的反应。",
"谢谢你愿意跟我分享这些。让我们一起来分析一下,看看有什么可以改善的地方。"
]
var body: some View {
NavigationView {
VStack(spacing: 0) {
//
ScrollView {
LazyVStack(spacing: 16) {
ForEach(chatMessages) { message in
ChatMessageView(message: message)
}
}
.padding()
}
//
VStack(spacing: 12) {
if isVoiceMode {
//
voiceButton
} else {
//
HStack(spacing: 12) {
TextField("输入消息...", text: $inputText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.vertical, 8)
Button(action: sendTextMessage) {
Image(systemName: "arrow.up.circle.fill")
.font(.title2)
.foregroundColor(ThemeColors.wechatGreen)
}
.disabled(inputText.isEmpty)
}
.padding()
}
}
.background(Color(UIColor.systemBackground))
.shadow(color: .black.opacity(0.05), radius: 8, y: -4)
}
.navigationTitle("AI疗愈师")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: { isVoiceMode.toggle() }) {
Image(systemName: isVoiceMode ? "keyboard" : "mic")
.foregroundColor(ThemeColors.wechatGreen)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { isPresented = false }) {
Image(systemName: "chevron.down")
.foregroundColor(ThemeColors.textSecondary)
}
}
}
.onAppear {
if !initialMessage.isEmpty {
let userMessage = ChatMessage(
id: UUID(),
content: initialMessage,
isUser: true,
timestamp: Date(),
messageType: .text,
audioURL: nil,
audioDuration: 0
)
chatMessages.append(userMessage)
sendAIResponse()
}
}
}
}
private var voiceButton: some View {
Button(action: handleVoiceButton) {
ZStack {
Circle()
.fill(audioManager.isRecording ? Color.red : ThemeColors.wechatGreen)
.frame(width: 60, height: 60)
.scaleEffect(recordingFeedback ? 1.2 : 1.0)
.animation(.spring(response: 0.3), value: recordingFeedback)
Image(systemName: audioManager.isRecording ? "stop.fill" : "mic.fill")
.foregroundColor(.white)
.font(.title2)
}
}
.padding(.vertical)
.overlay(
Group {
if audioManager.isRecording {
VStack {
Text(formatDuration(audioManager.recordingDuration))
.font(.caption)
.foregroundColor(.secondary)
.padding(.vertical, 4)
//
HStack(spacing: 4) {
ForEach(0..<3) { index in
Circle()
.fill(Color.red)
.frame(width: 6, height: 6)
.scaleEffect(recordingFeedback ? 1 : 0.5)
.animation(
Animation.easeInOut(duration: 0.5)
.repeatForever()
.delay(Double(index) * 0.2),
value: recordingFeedback
)
}
}
}
.offset(y: -40)
}
}
)
}
private func handleVoiceButton() {
if audioManager.isRecording {
if let (audioURL, duration) = audioManager.stopRecording() {
let message = ChatMessage(
id: UUID(),
content: "",
isUser: true,
timestamp: Date(),
messageType: .audio,
audioURL: audioURL,
audioDuration: duration
)
chatMessages.append(message)
sendAIResponse()
}
recordingFeedback = false
} else {
audioManager.startRecording()
recordingFeedback = true
}
}
private func sendTextMessage() {
guard !inputText.isEmpty else { return }
let message = ChatMessage(
id: UUID(),
content: inputText,
isUser: true,
timestamp: Date(),
messageType: .text,
audioURL: nil,
audioDuration: 0
)
chatMessages.append(message)
inputText = ""
sendAIResponse()
}
private func sendAIResponse() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let aiMessage = ChatMessage(
id: UUID(),
content: generateAIResponse(),
isUser: false,
timestamp: Date(),
messageType: .text,
audioURL: nil,
audioDuration: 0
)
chatMessages.append(aiMessage)
}
}
private func generateAIResponse() -> String {
return aiResponses.randomElement() ?? aiResponses[0]
}
private func formatDuration(_ duration: TimeInterval) -> String {
let minutes = Int(duration) / 60
let seconds = Int(duration) % 60
return String(format: "%d:%02d", minutes, seconds)
}
}
// MARK: -
struct InsightView: View {
@State private var selectedDate = Date()
@State private var selectedMood = ""
@State private var emotionText = ""
@State private var emotionScore = 5.0
@State private var selectedTags: Set<String> = []
@State private var showingAIAnalysis = false
@State private var isAnalyzing = false
@State private var aiResponse = ""
@State private var showingSettings = false
@State private var showingMoodPicker = false
@State private var conversations: [AIConversation] = []
@State private var showingConversationHistory = false
@State private var showingAIChat = false
let availableTags = ["工作", "学习", "家庭", "朋友", "健康", "爱情", "财务", "娱乐"]
var body: some View {
NavigationView {
mainContent
}
.sheet(isPresented: $showingSettings) {
InsightSettingsView()
}
.sheet(isPresented: $showingMoodPicker) {
MoodPickerSheet(
selectedMood: $selectedMood,
isPresented: $showingMoodPicker,
selectedDate: selectedDate
)
}
.sheet(isPresented: $showingConversationHistory) {
NavigationView {
ScrollView {
ConversationHistoryView(conversations: conversations)
}
.navigationTitle("AI对话历史")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("关闭") {
showingConversationHistory = false
}
}
}
}
}
.fullScreenCover(isPresented: $showingAIChat) {
AIChatView(isPresented: $showingAIChat, initialMessage: emotionText)
}
}
private var mainContent: some View {
ScrollView {
VStack(spacing: 16) {
//
navigationHeader
.padding(.horizontal)
//
calendarSection
.padding(.horizontal)
//
tagsSection
.padding(.horizontal)
//
emotionInputSection
.padding(.horizontal)
// AI
aiAnalysisButton
.padding(.horizontal)
//
if showingAIAnalysis {
aiAnalysisResultSection
.padding(.horizontal)
}
Spacer(minLength: 20)
}
}
.background(ThemeColors.lightBackground.ignoresSafeArea())
}
private var calendarSection: some View {
SingleRowCalendar(selectedDate: $selectedDate) { date in
selectedDate = date
showingMoodPicker = true
}
}
// MARK: -
private var navigationHeader: some View {
HStack {
Button(action: { showingConversationHistory = true }) {
Image(systemName: "book.fill")
.font(.title2)
.foregroundColor(ThemeColors.wechatGreen)
}
Spacer()
Text("情感洞察")
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(ThemeColors.textPrimary)
Spacer()
Button(action: { showingSettings = true }) {
Image(systemName: "gearshape.fill")
.font(.title2)
.foregroundColor(ThemeColors.textSecondary)
}
}
.padding(.horizontal)
}
// MARK: -
private var tagsSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("相关标签(AI疗愈师3D IP")
.font(.headline)
.foregroundColor(ThemeColors.textPrimary)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 8) {
ForEach(availableTags, id: \.self) { tag in
Button(action: {
if selectedTags.contains(tag) {
selectedTags.remove(tag)
} else {
selectedTags.insert(tag)
}
}) {
Text(tag)
.font(.caption)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(
selectedTags.contains(tag) ?
ThemeColors.wechatGreen : ThemeColors.secondaryBackground
)
.foregroundColor(
selectedTags.contains(tag) ? .white : ThemeColors.textPrimary
)
.cornerRadius(12)
}
}
}
}
.padding()
.background(ThemeColors.cardBackground)
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
}
// MARK: -
private var emotionInputSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("描述你的感受")
.font(.headline)
.foregroundColor(ThemeColors.textPrimary)
TextEditor(text: $emotionText)
.frame(height: 240)
.padding(8)
.background(ThemeColors.secondaryBackground)
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(ThemeColors.wechatGreen.opacity(0.3), lineWidth: 1)
)
}
.padding()
.background(ThemeColors.cardBackground)
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
}
// MARK: - AI
private var aiAnalysisButton: some View {
Button(action: {
if !emotionText.isEmpty {
showingAIChat = true
}
}) {
HStack {
Image(systemName: "brain.head.profile")
.font(.title3)
Text("AI情感分析")
.font(.headline)
.fontWeight(.semibold)
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(
LinearGradient(
colors: [ThemeColors.wechatGreen, ThemeColors.wechatGreen.opacity(0.8)],
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(25)
.shadow(color: ThemeColors.wechatGreen.opacity(0.3), radius: 8, x: 0, y: 4)
}
.disabled(emotionText.isEmpty)
.opacity(emotionText.isEmpty ? 0.6 : 1.0)
}
// MARK: - AI
private var aiAnalysisResultSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: "sparkles")
.foregroundColor(ThemeColors.wechatGreen)
Text("AI分析结果")
.font(.headline)
.foregroundColor(ThemeColors.textPrimary)
}
Text(aiResponse)
.font(.body)
.foregroundColor(ThemeColors.textPrimary)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(ThemeColors.wechatGreen.opacity(0.1))
.cornerRadius(12)
Button("保存记录") {
saveEmotionRecord()
}
.font(.subheadline)
.foregroundColor(ThemeColors.wechatGreen)
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(ThemeColors.wechatGreen.opacity(0.1))
.cornerRadius(12)
}
.padding()
.background(ThemeColors.cardBackground)
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
}
// MARK: -
private func getRandomGreeting() -> String {
let greetings = [
"今天的心情如何?",
"让我们一起探索内心世界",
"记录此刻的感受",
"你的情感值得被倾听",
"每一种情绪都有它的意义"
]
return greetings.randomElement() ?? greetings[0]
}
private func analyzeEmotion() {
guard !emotionText.isEmpty else { return }
isAnalyzing = true
// AI
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
aiResponse = generateAIResponse()
showingAIAnalysis = true
isAnalyzing = false
}
}
private func generateAIResponse() -> String {
let responses = [
"从你的描述中,我感受到了复杂的情感层次。这种感受是完全正常的,建议你可以通过深呼吸来缓解当前的情绪状态。",
"你的情感表达很真诚。每个人都会经历情绪的起伏,重要的是学会接纳和理解自己的感受。",
"我注意到你提到的几个关键词,这些都反映了你内心的真实想法。建议你可以尝试写日记来进一步整理思绪。",
"你的情感很丰富,这说明你是一个敏感且有深度的人。建议适当的运动和音乐可以帮助调节情绪。"
]
return responses.randomElement() ?? responses[0]
}
private func saveEmotionRecord() {
let newConversation = AIConversation(
date: selectedDate,
userMessage: emotionText,
aiResponse: aiResponse,
mood: selectedMood,
tags: Array(selectedTags)
)
conversations.insert(newConversation, at: 0)
//
emotionText = ""
selectedMood = ""
selectedTags.removeAll()
emotionScore = 5.0
showingAIAnalysis = false
aiResponse = ""
}
}
// MARK: -
struct InsightSettingsView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedTheme = 0
@State private var musicVolume = 0.5
@State private var soundEffectVolume = 0.7
let themes = ["自动", "浅色", "深色"]
var body: some View {
NavigationView {
List {
Section("主题设置") {
SettingRow(title: "界面主题", value: themes[selectedTheme]) {
//
}
}
Section("音频设置") {
VStack {
SettingRow(title: "背景音乐", value: "\(Int(musicVolume * 100))%") {}
Slider(value: $musicVolume, in: 0...1)
.accentColor(ThemeColors.wechatGreen)
}
VStack {
SettingRow(title: "音效音量", value: "\(Int(soundEffectVolume * 100))%") {}
Slider(value: $soundEffectVolume, in: 0...1)
.accentColor(ThemeColors.wechatGreen)
}
}
}
.navigationTitle("设置")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItem(placement: {
#if os(iOS)
return .topBarTrailing
#elseif os(macOS)
return .automatic
#else
return .automatic
#endif
}()) {
Button("完成") {
dismiss()
}
.foregroundColor(ThemeColors.wechatGreen)
}
}
}
}
}
// MARK: -
struct SettingRow: View {
let title: String
let value: String
let action: () -> Void
var body: some View {
HStack {
Text(title)
.foregroundColor(ThemeColors.textPrimary)
Spacer()
Text(value)
.foregroundColor(ThemeColors.textSecondary)
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(ThemeColors.textSecondary)
}
.contentShape(Rectangle())
.onTapGesture(perform: action)
}
}
// MARK: -
struct EmotionRecordCard: View {
let record: AIConversation
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(record.mood)
.font(.title2)
Spacer()
Text(DateFormatter.shortDate.string(from: record.date))
.font(.caption)
.foregroundColor(ThemeColors.textSecondary)
}
Text(record.userMessage)
.font(.body)
.foregroundColor(ThemeColors.textPrimary)
.lineLimit(3)
}
.padding()
.background(ThemeColors.cardBackground)
.cornerRadius(12)
.shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2)
}
}
// MARK: -
extension DateFormatter {
static let monthYear: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy年M月"
return formatter
}()
static let weekday: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "E"
return formatter
}()
static let conversationDate: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "M月d日 HH:mm"
return formatter
}()
static let fullDate: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy年M月d日"
return formatter
}()
static let shortDate: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "M/d"
return formatter
}()
}
// MARK: -
struct InsightView_Previews: PreviewProvider {
static var previews: some View {
InsightView()
.preferredColorScheme(.light)
InsightView()
.preferredColorScheme(.dark)
}
}
// MARK: -
struct ConversationHistoryView: View {
let conversations: [AIConversation]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if conversations.isEmpty {
VStack {
Image(systemName: "message.circle")
.font(.largeTitle)
.foregroundColor(ThemeColors.textSecondary)
Text("暂无对话记录")
.font(.subheadline)
.foregroundColor(ThemeColors.textSecondary)
}
.frame(maxWidth: .infinity)
.padding(40)
} else {
ForEach(conversations) { conversation in
AIConversationCard(conversation: conversation)
}
}
}
.padding()
.background(ThemeColors.cardBackground)
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
}
}