File size: 24,148 Bytes
f592f80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
import SwiftUI
import AppKit
import UniformTypeIdentifiers

func pretty(_ x: Any) -> String { guard let d = try? JSONSerialization.data(withJSONObject: x, options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]) else { return "{}" }; return String(decoding:d,as:UTF8.self) }
@MainActor final class Studio: ObservableObject {
    @Published var geometries: [[String:Any]] = []
    @Published var geometry: [String:Any] = [:]
    @Published var editor = ""
    @Published var showWeights = false
    @Published var source = ""
    @Published var output = ""
    @Published var report = ""
    @Published var status = "Starting AFFECTOR…"
    @Published var busy = false
    @Published var backend = "Local Qwen"
    @Published var rewriteStyle = "Restrained"
    @Published var model = "gpt-6-luna"
    @Published var key = ""
    @Published var evaluator = "Local Qwen"
    @Published var evalModel = "gpt-6-luna"
    @Published var evalKey = ""
    @Published var temperature = 0.0
    @Published var topP = 0.95
    @Published var topK = 40
    @Published var tokens = 512
    @Published var verification: [String:Any] = [:]
    @Published var hasRun = false
    @Published var runSummary = ""
    @Published var support = ""
    @Published var error: String?
    private var process: Process?
    private var input: FileHandle?
    private var buffer = Data()
    func start() {
        let resources = Bundle.main.resourceURL!
        let p = Process(); let stdin = Pipe(); let stdout = Pipe(); let stderr = Pipe()
        p.executableURL = resources.appendingPathComponent("Python.framework/Versions/3.14/bin/python3.14")
        p.arguments = ["-u",resources.appendingPathComponent("engine/bridge.py").path]
        p.currentDirectoryURL = resources.appendingPathComponent("engine")
        var env = ProcessInfo.processInfo.environment
        env["PYTHONHOME"] = resources.appendingPathComponent("Python.framework/Versions/3.14").path
        env["PYTHONPATH"] = resources.appendingPathComponent("packages").path
        env["HF_HOME"] = FileManager.default.urls(for:.cachesDirectory,in:.userDomainMask)[0].appendingPathComponent("AFFECTOR/huggingface").path
        env["AFFECTOR_MODEL"] = resources.appendingPathComponent("engine/.hf/hub/models--Qwen--Qwen3.5-0.8B/snapshots/2fc06364715b967f1860aea9cf38778875588b17").path
        env["PYTHONNOUSERSITE"] = "1"; env["PYTHONDONTWRITEBYTECODE"] = "1"; env["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
        p.environment = env; p.standardInput = stdin; p.standardOutput = stdout; p.standardError = stderr
        input = stdin.fileHandleForWriting; process = p
        stdout.fileHandleForReading.readabilityHandler = { [weak self] h in
            let d = h.availableData
            if !d.isEmpty { Task { @MainActor in self?.receive(d) } }
        }
        stderr.fileHandleForReading.readabilityHandler = { h in _ = h.availableData }
        p.terminationHandler = { [weak self] p in Task { @MainActor in self?.busy = false; self?.status = "Engine stopped (\(p.terminationStatus)). Reopen AFFECTOR to restart." } }
        do { try p.run() } catch { self.error = error.localizedDescription }
    }
    func stopProcess() { input?.closeFile(); process?.terminate() }
    func receive(_ d:Data) {
        buffer.append(d)
        while let end = buffer.firstIndex(of:10) {
            let line = buffer[..<end]; buffer.removeSubrange(...end)
            guard let obj = (try? JSONSerialization.jsonObject(with:Data(line))) as? [String:Any] else { continue }
            let data = obj["data"] as? [String:Any] ?? [:]
            switch obj["event"] as? String {
            case "ready", "result":
                if data["reset_text"] as? Bool == true { source = ""; output = ""; report = ""; verification = [:]; runSummary = ""; hasRun = false; error = nil }
                if let a = data["geometries"] as? [[String:Any]] { geometries = a; if geometry.isEmpty, let first = a.first { select(first) } }
                if let g = data["geometry"] as? [String:Any] { select(g) }
                if let s = data["support"] as? String { support = s }
                if let t = data["text"] as? String { source = t }
                if let t = data["input"] as? String { source = t }
                if let style = data["rewrite_style"] as? String { rewriteStyle = style }
                if let b = data["backend"] as? String { backend = b }
                if let m = data["model"] as? String { model = m }
                if let s = data["sampling"] as? [String:Any] { temperature = s["temperature"] as? Double ?? 0; topP = s["top_p"] as? Double ?? 0.95; topK = s["top_k"] as? Int ?? 40; tokens = s["max_tokens"] as? Int ?? 512 }
                status = data["message"] as? String ?? "Ready"
            case "geometry":
                select(data)
                if let i=geometries.firstIndex(where:{($0["id"] as? String)==(data["id"] as? String)}) {geometries[i]=data}
            case "progress": status = obj["message"] as? String ?? "Working…"
            case "run": runSummary = (data["summary"] as? [String:Any])?["message"] as? String ?? ""; output = obj["output"] as? String ?? ""; report = displayReport(data); verification = data["verification"] as? [String:Any] ?? [:]; hasRun = true
            case "error": error = obj["message"] as? String; status = error ?? "Error"
            case "saved": status = "Transfer checkpoint saved."
            case "idle": busy = false
            default: break
            }
        }
    }
    func select(_ g:[String:Any]) { geometry = g; showWeights = false; var display=g; display.removeValue(forKey:"basis"); editor = pretty(display) }
    func toggleWeights() {
        guard var g=edited() else {return}
        showWeights.toggle()
        if !showWeights {geometry["basis"]=g["basis"];g.removeValue(forKey:"basis")}
        editor=pretty(g)
    }
    func displayReport(_ data:[String:Any])->String {
        var d=data
        if var g=d["geometry"] as? [String:Any] {g["basis"]="Compiled weights included in exported JSON; edit in Geometry + weights.";d["geometry"]=g}
        let result=pretty(d)
        return result.count > 80000 ? String(result.prefix(80000))+"\n… Export the report to view all passages." : result
    }
    func edited() -> [String:Any]? {
        guard let d = editor.data(using:.utf8), var g = (try? JSONSerialization.jsonObject(with:d)) as? [String:Any] else { error = "Geometry must be valid JSON. Check commas and brackets."; return nil }
        if g["basis"] == nil {g["basis"] = geometry["basis"] ?? NSNull()}
        return g
    }
    func send(_ q:[String:Any]) {
        guard let input, let d = try? JSONSerialization.data(withJSONObject:q) else { return }
        if q["op"] as? String != "cancel" && q["op"] as? String != "save_run" { busy = true }
        do { try input.write(contentsOf:d + Data([10])) } catch { busy = false; self.error = error.localizedDescription }
    }
    func open(_ op:String, document:Bool = false) {
        let panel = NSOpenPanel(); panel.canChooseDirectories = false; panel.allowsMultipleSelection = false
        panel.allowedContentTypes = document ? [UTType.plainText,UTType.rtf,UTType(filenameExtension:"docx")!] : [.json]
        if panel.runModal() == .OK, let url = panel.url { send(["op":op,"path":url.path]) }
    }
    func save(_ op:String, document:Bool = false) {
        let panel = NSSavePanel(); panel.nameFieldStringValue = document ? "transformed.txt" : (op == "export_geometry" ? "emotional-shape.json" : "affector-transfer.json")
        panel.allowedContentTypes = document ? [.plainText,.rtf,UTType(filenameExtension:"docx")!] : [.json]
        panel.allowsOtherFileTypes = false
        if panel.runModal() == .OK, let url = panel.url {
            var q:[String:Any] = ["op":op,"path":url.path]
            if op == "export_geometry" { guard let g = edited() else { return }; q["geometry"] = g }
            send(q)
        }
    }
    func transform() {
        guard let g = edited() else { return }
        send(["op":"transform","rewrite_style":rewriteStyle,"geometry":g,"text":source,"backend":backend,"model":model,"key":key,"sampling":["temperature":temperature,"top_p":topP,"top_k":topK,"max_tokens":tokens]])
    }
    func geometryAction(_ op:String) { if let g = edited() { send(["op":op,"geometry":g]) } }
}

struct Trajectory: View {
    @Environment(\.colorScheme) private var colorScheme
    let geometry:[String:Any]
    var body: some View {
        Canvas { ctx,size in
            let points = geometry["points"] as? [[String:Double]] ?? []
            func project(_ x:Double,_ y:Double,_ z:Double)->CGPoint { CGPoint(x:size.width*0.48 + CGFloat(x-y*0.45)*size.width*0.28,y:size.height*0.52 - CGFloat(z+y*0.35)*size.height*0.32) }
            let axes:[(String,(Double,Double,Double))] = [("X · valence",(1.35,0,0)),("Y · direction",(0,1.35,0)),("Z · intensity",(0,0,1.35))]
            for (name,a) in axes { var p=Path(); p.move(to:project(0,0,0)); p.addLine(to:project(a.0,a.1,a.2)); ctx.stroke(p,with:.color(.gray.opacity(0.6)),lineWidth:1); ctx.draw(Text(name).font(.system(size:10,design:.monospaced)),at:project(a.0,a.1,a.2),anchor:a.0 > 0 ? .trailing : .bottom) }
            var path=Path()
            for (i,p) in points.enumerated() { let pos=project(p["v"] ?? 0,p["d"] ?? 0,2*(p["i"] ?? 0.5)-1); if i==0 {path.move(to:pos)} else {path.addLine(to:pos)}; ctx.fill(Path(ellipseIn:CGRect(x:pos.x-3,y:pos.y-3,width:6,height:6)),with:.color(.primary)) }
            ctx.stroke(path,with:.color(.primary),lineWidth:2)
        }.frame(height:205).background(colorScheme == .dark ? Color.black : Color.white).overlay(Rectangle().stroke(.primary.opacity(0.15)))
    }
}
struct ContentView: View {
    @EnvironmentObject var s:Studio
    @AppStorage("appearance") private var appearance = "light"
    private var darkMode: Bool { appearance == "dark" }
    @State private var tab = 0
    @State private var confirmDelete = false
    let providers = ["Local Qwen","OpenAI","VT ARC"]
    var body: some View {
        VStack(alignment:.leading,spacing:0) {
            HStack(alignment:.top) {
                VStack(alignment:.leading,spacing:1) { Text("AFFECTOR").font(.system(size:53,weight:.black,design:.default)).tracking(-3); Text("EMOTIONAL GEOMETRY → WRITTEN FORM").font(.system(size:10,design:.monospaced)).tracking(2) }
                Spacer()
                VStack(alignment:.trailing,spacing:8) { Text(s.status).font(.system(size:12,design:.monospaced)).multilineTextAlignment(.trailing).frame(maxWidth:390,minHeight:35); if s.busy { Button("Stop") { s.send(["op":"cancel"]) } }
                    Picker("Appearance",selection:$appearance) {Text("Light").tag("light");Text("Dark").tag("dark")}.pickerStyle(.segmented).labelsHidden().frame(width:170).accessibilityLabel("Appearance")
                }
            }.padding(24)
            Divider().background(.primary)
            HStack(alignment:.top,spacing:0) {
                VStack(alignment:.leading,spacing:12) {
                    label("SHAPE LIBRARY")
                    ScrollView { VStack(alignment:.leading,spacing:3) { ForEach(s.geometries.indices,id:\.self) { i in
                        let g=s.geometries[i]
                        Button { s.select(g) } label: { VStack(alignment:.leading,spacing:5) { Text(g["name"] as? String ?? "Untitled").font(.system(size:13,weight:.medium)); Text(String((g["updated_at"] as? String ?? "").prefix(19))).font(.system(size:9,design:.monospaced)) }.frame(maxWidth:.infinity,alignment:.leading).padding(10).background((g["id"] as? String)==(s.geometry["id"] as? String) ? Color.primary.opacity(0.10):Color.clear) }.buttonStyle(.plain).disabled(s.busy)
                    } } }.frame(minHeight:100,maxHeight:240)
                    HStack { Button("New") { s.send(["op":"new"]) }; Button("Open…") { s.open("import") } }.disabled(s.busy)
                    HStack { Button("Export…") { s.save("export_geometry") }; Button("Delete") { confirmDelete=true } }.disabled(s.busy || s.geometry.isEmpty)
                    Trajectory(geometry:s.geometry)
                    Text("Import Emotional Atlas sessions or object/evolution JSON with emotional coordinates. The line follows the text from beginning to end.").font(.system(size:11)).foregroundStyle(.secondary)
                    Spacer()
                    Text("LOCAL STEERING · API TRANSFER\nQWEN3.5 / 0.8B\nEXPERIMENTAL RESEARCH SOFTWARE").font(.system(size:9,design:.monospaced)).lineSpacing(4)
                }.padding(20).frame(width:255)
                Divider()
                VStack(alignment:.leading,spacing:16) {
                    Picker("Workspace",selection:$tab) { Text("01  Document").tag(0); Text("02  Geometry + weights").tag(1); Text("03  Verification").tag(2) }.pickerStyle(.segmented)
                    if tab==0 { document }
                    if tab==1 { geometry }
                    if tab==2 { verification }
                }.padding(22).frame(maxWidth:.infinity,maxHeight:.infinity,alignment:.top)
            }
        }.background(darkMode ? Color.black : Color.white).foregroundStyle(.primary).preferredColorScheme(darkMode ? .dark : .light).frame(minWidth:1080,minHeight:770)
        .alert("AFFECTOR",isPresented:Binding(get:{s.error != nil},set:{if !$0 {s.error=nil}})) { Button("OK") {s.error=nil} } message: {Text(s.error ?? "")}
        .alert("Delete this geometry?",isPresented:$confirmDelete) { Button("Delete",role:.destructive) {s.send(["op":"delete","id":s.geometry["id"] ?? ""]);s.geometry=[:];s.editor=""};Button("Cancel",role:.cancel){} } message:{Text("Export a copy first if you want to retain its trajectory and calibrated weights.")}
    }
    func label(_ text:String)->some View { Text(text).font(.system(size:10,weight:.semibold,design:.monospaced)).tracking(1.5) }
    var document: some View {
        VStack(alignment:.leading,spacing:14) {
            HStack { Button("Open document…") {s.open("read_doc",document:true)}; Button("Open transfer…") {s.open("load_run")}; Button("Save transfer…") {s.save("save_run")}.disabled(!s.hasRun); Button("Reset text") {s.send(["op":"reset_text"])}.help("Clear both texts and the automatic checkpoint without saving. Keeps geometry, settings and manually saved files."); Spacer();Button("Export output…") {s.save("export_doc",document:true)}.disabled(!s.hasRun) }.disabled(s.busy)
            if !s.runSummary.isEmpty { Text(s.runSummary).font(.system(size:11,design:.monospaced)).textSelection(.enabled) }
            HStack(alignment:.top,spacing:18) {
                VStack(alignment:.leading) {label("INPUT");TextEditor(text:$s.source).font(.system(size:15,design:.serif)).padding(6).overlay(Rectangle().stroke(.gray.opacity(0.4))).disabled(s.busy)}
                VStack(alignment:.leading) {label("TRANSFORMED TEXT");ScrollView {Text(s.output.isEmpty ? "Your transformed document will appear here." : s.output).font(.system(size:15,design:.serif)).textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading).padding(12)}.frame(maxWidth:.infinity,maxHeight:.infinity).overlay(Rectangle().stroke(.gray.opacity(0.4)))}
            }.frame(minHeight:190)
            Picker("Rewrite style", selection:$s.rewriteStyle) { Text("Minimal").tag("Minimal"); Text("Restrained").tag("Restrained"); Text("Balanced").tag("Balanced"); Text("Creative").tag("Creative") }.pickerStyle(.segmented).disabled(s.busy)
            Text(["Minimal":"Light edits to a few words or phrases; preserve nearly all original wording.", "Restrained":"Preserve meaning and details; adjust emotional tone through wording and rhythm.", "Balanced":"Reshape phrasing and structure with modest imagery; preserve facts and events.", "Creative":"Allow new imagery, perspectives and events; meaning may change."][s.rewriteStyle] ?? "").font(.system(size:11)).foregroundStyle(.secondary)
            Text("TXT / RTF / DOCX formatting is not preserved. Generated drafts are saved, including repetitions and incomplete endings. Requested steering is preserved. Incomplete and repetitive endings are trimmed to complete sentences; raw drafts remain in the report. Output aims for the original length; paragraphs share a 125% ceiling.").font(.system(size:10,design:.monospaced)).foregroundStyle(.secondary)
            Divider()
            HStack { Picker("Transform with",selection:$s.backend) {ForEach(providers,id:\.self){Text($0)}}.frame(width:260);Spacer();Button("Transform / resume") {s.transform()}.buttonStyle(.bordered).foregroundStyle(.primary).disabled(s.busy || s.geometry.isEmpty) }.disabled(s.busy)
            if s.backend != "Local Qwen" { HStack {TextField("Model",text:$s.model);SecureField("API key · kept in memory only",text:$s.key)}.textFieldStyle(.roundedBorder).disabled(s.busy);Text("API mode uses coordinate instructions. Hosted APIs do not expose transformer activations.").font(.system(size:11)) }
            else { HStack {Text("Sampling").font(.system(size:11,design:.monospaced));Text("Temperature");TextField("Temperature",value:$s.temperature,format:.number).frame(width:55);Text("Top-p").padding(.leading,12);TextField("Top-p",value:$s.topP,format:.number).frame(width:55);Text("Top-k").padding(.leading,12);TextField("Top-k",value:$s.topK,format:.number).frame(width:50)}.font(.system(size:11)).disabled(s.busy) }
            HStack {Text("Output tokens").font(.system(size:11));TextField("Output tokens",value:$s.tokens,format:.number).frame(width:65);Text("base token allowance (32–2048); expanded automatically for longer passages").font(.system(size:11));Spacer();Button("Save progress…") {s.save("save_run")}.disabled(!s.hasRun)}
        }.onChange(of:s.backend) { _,b in if b=="VT ARC" {s.model="gpt-oss-120b"} else if b=="OpenAI" {s.model="gpt-6-luna"} }
    }
    var geometry: some View {
        VStack(alignment:.leading,spacing:12) {
            Text("A shape is a trajectory and a transformer module.").font(.system(size:24,weight:.semibold))
            Text("Edit the name, points, selected layers (0–23), controls, or compiled basis vectors below. Save to update the preview. Recalibrate after changing layers or model identity.").font(.system(size:12))
            Text("t = position · v = valence [−1,1] · d = direction [−1,1] · i = intensity [0,1]\nstrength = injection scale · budget = maximum relative RMS · inertia = temporal smoothing\naxis_top_k / axis_mass / axis_temperature select and reshape activation coefficients, independently of token sampling.").font(.system(size:11,design:.monospaced)).lineSpacing(5)
            Text("Extreme values for strength, budget, and inertia may cause generation failures.").font(.system(size:12)).foregroundStyle(.red)
            HStack {
                control("Strength", "strength", 0...1)
                control("Budget", "budget", 0...1)
                control("Inertia", "inertia", 0...0.99)
            }.disabled(s.busy)
            TextEditor(text:$s.editor).font(.system(size:11,design:.monospaced)).padding(6).overlay(Rectangle().stroke(.gray.opacity(0.4))).disabled(s.busy)
            HStack {Button("Save geometry") {s.geometryAction("save")};Button("Calibrate local module") {s.geometryAction("calibrate")};Button(s.showWeights ? "Hide numerical weights" : "Edit numerical weights") {s.toggleWeights()};Spacer();Text("Calibration is experimental; emotional fit is measured separately.").font(.system(size:10)).foregroundStyle(.secondary)}.disabled(s.busy || s.geometry.isEmpty)
        }
    }
    func control(_ title:String,_ key:String,_ range:ClosedRange<Double>)->some View {
        let binding=Binding<Double>(get:{
            let obj=(try? JSONSerialization.jsonObject(with:Data(s.editor.utf8))) as? [String:Any]
            return (obj?["controls"] as? [String:Any])?[key] as? Double ?? 0
        },set:{ value in
            guard var g=(try? JSONSerialization.jsonObject(with:Data(s.editor.utf8))) as? [String:Any],var c=g["controls"] as? [String:Any] else {return}
            c[key]=value;g["controls"]=c;s.editor=pretty(g)
        })
        return VStack(alignment:.leading){Text("\(title)  \(binding.wrappedValue, specifier:"%.2f")").font(.system(size:11,design:.monospaced));Slider(value:binding,in:range).tint(.primary)}
    }
    var verification: some View {
        VStack(alignment:.leading,spacing:14) {
            Text("Measure the change, passage by passage.").font(.system(size:24,weight:.semibold))
            Text("Evaluate original and revised text with the Emotional Atlas scoring prompt. Similarity measures proximity to the requested coordinates; it is not a probability or a measure of semantic fidelity.").font(.system(size:12))
            HStack {Picker("Evaluator",selection:$s.evaluator){ForEach(providers,id:\.self){Text($0)}}.frame(width:260);Spacer();Button("Verify output") {s.send(["op":"verify","provider":s.evaluator,"model":s.evalModel,"key":s.evalKey])}.disabled(s.busy || !s.hasRun);Button("Export report…") {s.save("export_report")}.disabled(s.busy || !s.hasRun)}
            if s.evaluator != "Local Qwen" {HStack {TextField("Evaluator model",text:$s.evalModel);SecureField("Evaluator API key",text:$s.evalKey)}.textFieldStyle(.roundedBorder)}
            HStack(spacing:30) { score("ORIGINAL",s.verification["before_similarity"] as? Double);score("TRANSFORMED",s.verification["after_similarity"] as? Double);score("IMPROVEMENT",s.verification["improvement"] as? Double);VStack(alignment:.leading){label("PAIRED COVERAGE");Text("\(s.verification["paired_count"] as? Int ?? 0) / \(s.verification["total_count"] as? Int ?? 0)").font(.system(size:26,weight:.light))} }.padding(.vertical,12)
            Text("Similarity = 1 − distance / (2√3), with z = 2 × intensity − 1. Unknown directions and failed evaluations are excluded. Local 0.8B scoring may be unreliable; compare with an independent API evaluator.").font(.system(size:11)).foregroundStyle(.secondary)
            label("TRANSFER + EVALUATION RECORD · FULL WEIGHTS IN EXPORT")
            ScrollView {Text(s.report.isEmpty ? "Run a transfer, then verify its output." : s.report).font(.system(size:11,design:.monospaced)).textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading).padding(12)}.frame(maxWidth:.infinity,maxHeight:.infinity).overlay(Rectangle().stroke(.gray.opacity(0.4)))
        }.onChange(of:s.evaluator) {_,b in if b=="VT ARC" {s.evalModel="gpt-oss-120b"} else if b=="OpenAI" {s.evalModel="gpt-6-luna"} }
    }
    func score(_ name:String,_ value:Double?)->some View {VStack(alignment:.leading){label(name);Text(value.map{String(format:"%.3f",$0)} ?? "—").font(.system(size:26,weight:.light,design:.monospaced))}}
}
final class AppDelegate: NSObject, NSApplicationDelegate {
    var studio:Studio?
    func applicationShouldTerminate(_ sender:NSApplication)->NSApplication.TerminateReply {
        if studio?.busy == true {let a=NSAlert();a.messageText="Stop the active operation and quit?";a.informativeText="Generated drafts are saved, including repetitions and incomplete endings. Requested steering is preserved. Incomplete and repetitive endings are trimmed to complete sentences; raw drafts remain in the report. Output aims for the original length; paragraphs share a 125% ceiling. The current passage may need to be repeated.";a.addButton(withTitle:"Quit");a.addButton(withTitle:"Keep working");if a.runModal() != .alertFirstButtonReturn{return .terminateCancel}}
        studio?.stopProcess();return .terminateNow
    }
    func applicationShouldTerminateAfterLastWindowClosed(_ sender:NSApplication)->Bool {true}
}
@main struct AffectorApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
    @StateObject private var studio=Studio()
    var body: some Scene {WindowGroup {ContentView().environmentObject(studio).onAppear{if delegate.studio == nil {delegate.studio=studio;studio.start()}}}.defaultSize(width:1230,height:850).commands {CommandGroup(replacing:.newItem){};CommandGroup(after:.saveItem){Button("Save transfer…"){studio.save("save_run")}.keyboardShortcut("s").disabled(!studio.hasRun)}}}

}