// // ChatViewModel.swift // Poche // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // import Foundation import Observation @MainActor @Observable final class ChatViewModel { private(set) var turns: [ChatTurn] = [] var draft = "" private let agent: AgentSession private let confirm: ConfirmCenter private let store: PocheStore private var conversation: ConversationRecord? var isBusy: Bool { agent.isResponding } var contextUsage: Double { agent.usageRatio } init(agent: AgentSession, confirm: ConfirmCenter, store: PocheStore) { self.agent = agent self.confirm = confirm self.store = store // The Confirm layer reports back so (a) the user sees a truthful // "done" notice only after execution, and (b) the model learns the // outcome on its next turn instead of believing its own proposal. confirm.onExecuted = { [weak self] summary in self?.appendSystemNotice("✓ " + summary) self?.agent.noteEvent("Action confirmée et exécutée : \(summary)") } confirm.onDismissed = { [weak self] summary in self?.appendSystemNotice("Proposition annulée — dis-moi ce que tu veux changer.") self?.agent.noteEvent("L'utilisateur a annulé la proposition : \(summary)") } agent.onSummary = { [weak self] summary in guard let self, let conversation = self.conversation else { return } self.store.updateSummary(of: conversation, to: summary) } } /// Sends a given text (welcome suggestions, driving hooks). func send(_ text: String) async { draft = text await send() } func send() async { let text = draft.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty, !agent.isResponding else { return } draft = "" turns.append(ChatTurn(role: .user, text: text, status: .complete)) persist(role: "user", text: text) turns.append(ChatTurn(role: .assistant, text: "", status: .streaming)) let index = turns.count - 1 let outcome = await agent.respond(to: text) { [weak self] partial in self?.turns[index].text = partial } switch outcome { case .completed(let full): turns[index].text = full turns[index].status = .complete persist(role: "assistant", text: full) case .refused(let message): turns[index].text = message turns[index].status = .refused case .failed(let message): turns[index].text = message turns[index].status = .failed } } #if DEBUG /// Visual-verification hook (simulator only): fills the thread with /// representative turns without touching the model. func injectDemoTurns() { turns = [ ChatTurn(role: .user, text: "Rappelle-moi d'appeler le dentiste demain à 9 h", status: .complete), ChatTurn(role: .assistant, text: "Je te propose un rappel « Appeler le dentiste » pour demain à 9 h. Confirme sur la carte ci-dessous.", status: .complete), ChatTurn(role: .system, text: "✓ Rappel « Appeler le dentiste » créé", status: .complete), ChatTurn(role: .user, text: "Parfait, et qu'est-ce que j'ai cette semaine ?", status: .complete), ChatTurn(role: .assistant, text: "", status: .streaming), ] } #endif private func appendSystemNotice(_ text: String) { turns.append(ChatTurn(role: .system, text: text, status: .complete)) persist(role: "system", text: text) } private func persist(role: String, text: String) { if conversation == nil { conversation = store.newConversation() } if let conversation { store.appendTurn(to: conversation, role: role, text: text) } } }