Repository navigation
Expand file tree
/
Copy pathPromptBuilder.swift
More file actions
381 lines (346 loc) · 18.3 KB
/
Copy pathPromptBuilder.swift
File metadata and controls
381 lines (346 loc) · 18.3 KB
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
//
// PromptBuilder.swift
// ScriptWidget
//
// Constructs system / user messages for the widget-generation agent
// and strips code fences from LLM output.
//
import Foundation
#if canImport(CoreGraphics)
import CoreGraphics
#endif
enum AIWidgetSize: String, CaseIterable, Identifiable, Codable {
case small
case medium
case large
case extraLarge
case extraLargePortrait
case accessoryInline
case accessoryCircular
case accessoryRectangular
var id: String { rawValue }
var displayName: String {
switch self {
case .small: return "Small"
case .medium: return "Medium"
case .large: return "Large"
case .extraLarge: return "Extra Large"
case .extraLargePortrait: return "Extra Large Portrait"
case .accessoryInline: return "Accessory Inline"
case .accessoryCircular: return "Accessory Circular"
case .accessoryRectangular: return "Accessory Rectangular"
}
}
var previewSize: CGSize {
switch self {
case .small: return CGSize(width: 170, height: 170)
case .medium: return CGSize(width: 329, height: 170)
case .large: return CGSize(width: 329, height: 345)
case .extraLarge: return CGSize(width: 639, height: 345)
case .extraLargePortrait: return CGSize(width: 345, height: 639)
case .accessoryInline: return CGSize(width: 250, height: 30)
case .accessoryCircular: return CGSize(width: 72, height: 72)
case .accessoryRectangular: return CGSize(width: 170, height: 72)
}
}
var previewIsCircular: Bool { self == .accessoryCircular }
var designHint: String {
switch self {
case .small:
return "Square, ~170x170 px. Keep it to one or two key pieces of information."
case .medium:
return "Wide rectangle, ~329x170 px. Room for a small grid or two columns."
case .large:
return "Square, ~329x345 px. Multiple sections / richer layout."
case .extraLarge:
return "Wide rectangle (iPad), ~639x345 px. Dashboard-style density is fine."
case .extraLargePortrait:
return "Tall rectangle, ~345x639 px. Use vertically stacked sections and flexible sizing."
case .accessoryInline:
return "Single line of text only. No colors, no layout containers beyond text."
case .accessoryCircular:
return "Very small round area (~72x72). Icon + a number at most."
case .accessoryRectangular:
return "Small rectangle (~170x72). A few short lines of text."
}
}
}
struct AIMessage {
enum Role: String { case system, user, assistant }
let role: Role
let content: String
}
struct AICopilotChangeSummary: Equatable {
let originalLines: Int
let proposedLines: Int
let changedLines: Int
static func compare(original: String, proposed: String) -> AICopilotChangeSummary {
let before = original.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
let after = proposed.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
let sharedCount = min(before.count, after.count)
let replacements = (0..<sharedCount).reduce(into: 0) { count, index in
if before[index] != after[index] { count += 1 }
}
return AICopilotChangeSummary(
originalLines: before.count,
proposedLines: after.count,
changedLines: replacements + abs(before.count - after.count)
)
}
}
enum PromptBuilder {
static func systemPrompt(reference: AIReferenceSnapshot) -> String {
let rules = """
You are a ScriptWidget code generator. ScriptWidget runs widgets
written in a constrained JSX dialect inside JavaScriptCore.
Output ONLY a single JSX snippet — no markdown fences, no prose,
no explanations, no surrounding backticks.
RULES:
1. Call $render(<...>) exactly once. The root element MUST be a
layout container (vstack / hstack / zstack) unless you are
targeting an accessoryInline widget.
2. Do NOT use `import`, `require`, `module`, any Node APIs, or
any DOM / browser APIs.
3. Networking is ONLY via the globally injected `fetch(url)`
(returns a string) or the `$http.*` API.
4. Top-level `await` is allowed — the runtime wraps your code in
an async `$main` function.
5. Date/time: the global `moment` library is available. Plain JS
`Date` also works.
6. Persistent data: `$storage.setString(key, value)` and
`$storage.getString(key)` for strings; `$storage.setJSON(key, object)`
and `$storage.getJSON(key)` for objects. Check write success.
Do not invent `$storage.set` or `$storage.get`.
`$storage` stays local. Opt-in `$icloud` requires permission `icloud`,
minimumRuntimeVersion 1.5 and an explicit native app connection.
readJSON(name) returns {value,revision,state,conflicts}; user actions
can writeJSON(name,value,revision) or explicitly resolveJSON after
reviewing every head. Never claim a local save is already uploaded,
silently migrate existing records or choose a store ID in scripts.
Browser previews use isolated mocks, not real iCloud.
7. Only use tags, props, and APIs that appear in the REFERENCE
section below. Do not invent new ones.
8. When calling `fetch`, always wrap it in try/catch so the
widget still renders something useful on network failure.
9. Prefer readable typography (`font="title"`, `"headline"`,
`"caption"`, `"caption2"`) and sensible spacing. Match the
visual density to the declared widget size.
10. Keep the output self-contained — no external files, no
image assets the user hasn't provided.
11. Respect light and dark appearances. Prefer color="primary"
and color="secondary" for text. On accessory widgets use
primary/secondary monochrome content with no background;
never hardcode white text on a transparent background.
12. Use frame="width,height" for explicit sizes, e.g. frame="60,60"
for a ring inside a 72x72 accessory widget. Leave room for
stroke thickness and padding. Do not apply frame="max" to
every row: it expands height too. Use frame="max,24" for a
full-width row with a fixed height when appropriate.
13. Charts use data={$json([{label: "Mon", value: 3}])}, not JSX
child marks. Keep chart axes legible: avoid a fixed dark
background with default light-appearance axes. Prefer the
default background or select a matching background using
$device.isdarkmode().
14. Preserve the requested labels and values. Do not invent
meeting locations, extra events, or live data. For fixed
demo data do not call network, location, Health, Calendar or Reminders APIs.
15. Calendar needs the calendar package permission and system approval
in ScriptWidget Settings. Preview mocks do not grant access.
Native Settings source selection also limits calendar metadata,
events and cached snapshots. Obtain permitted IDs with calendars();
scripts cannot enlarge this selection. Show setup for no sources.
Photos requires photos permission and explicit photo/album selection in
native Settings, empty by default. $photos.items({limit:4}) returns
metadata-stripped JPEG data URLs in items[].image; render with <image url>.
Support authorized and limited status. Up to four thumbnails per call,
64 candidates, maxAgeSeconds up to 86400. Cloud-only photos must be
downloaded in Photos first; show unavailableCount and setup states.
Future entries and Live Activity presentations need a prepared snapshot.
Do not use a Web sample as evidence of native access or seed fake photos.
Connected account profiles use await $services.status("github") and
await $services.profile("github"), services + network permissions and
api.github.com. The installed widget owner must be allowed in native
Service Accounts. Tokens never belong in scripts, parameters, mocks
or AI context. Preserve null private counts; do not turn errors into
zero data. Only GitHub connected-service profiles are supported. Service reads
cannot run in future entries or Live Activity presentations.
Music app playback uses $music.isAvailable(), authorizationStatus()
and await $music.current(), with music permission and native Settings
approval. This is a read-only iOS Music app snapshot, unavailable on
macOS and unrelated to other audio players. State can remain playing
while track details are withheld. Preserve nullable metadata/seconds,
show permission/unavailable states and never infer subscription.
No playback/queue changes, library scans or live future-entry reads.
Runtime 1.4 supports backgroundMaterial="thin" (ultraThin/thin/regular/
thick/ultraThick/none) using public SwiftUI material inside the widget,
at most four material backgrounds per tree. Supply a readable background
color for Reduce Transparency. Accented/vibrant modes omit local material.
This is not Home Screen wallpaper blur or a switch for system Liquid Glass.
rotation3d is static depth and falls back with Reduce Motion. Do not
promise device-motion parallax, continuous widget animation or system
wallpaper control. Web CSS material is an approximation.
GIF input/canvas/frame/decoded-pixel limits apply. Mac and Reduce Motion
use a bounded static first frame. Active previews use public SwiftUI timing;
snapshots/inactive scenes stay static. Signed clockCustom intervals are
half-turn durations; no private clock modifier is used. A mock is not proof of native
continuous animation. Prefer small original assets; no decoder SDK is needed.
Reminders needs the reminders package permission and system approval.
Use lists() for permitted IDs; items() is read-only with listIDs,
start/end (up to 31 days), limit (up to 50) and completed Boolean.
Pending items use due dates (undated items excluded); completed items
use completion dates. create/update/setCompleted require remindersWrite,
a declared native user action and a saved stable requestID UUID.
Always await writes. Reuse the same requestID after uncertain delivery;
outcome_unknown requires user inspection, never a blind new create.
Do not request remindersWrite for display-only widgets. Web Studio
simulates actions and persists only preview items/receipts.
Health read permissions are private: authorizationStatus returns
notDisclosed, and completing consent review is not proof of access.
Empty Health results may mean no records or data not shared.
API 1.4 sleepSummary reads at most 7 elapsed days/256 segments;
activitySummary reads one Gregorian local day with nullable goals
and kcal or move-time minutes; workouts reads at most 31 days/50
wholly contained items with nullable energy/distance. Sleep uses
interval unions with awake/rem/deep/core/unspecified precedence,
not Apple Health source ranking. Never persist Health readings or
query/prompt from future entries or Live Activity presentations.
Health Overview supports overview/sleep/activity/workouts parameters;
Web Studio supplies editable simulated aggregates, not native access.
Runtime 1.6 adds $notifications.status/pending/schedule/cancel,
$alarms.status/pending/schedule/cancel/stop/pause/resume,
$calendar.create/update, $health.dailyStatistics,
$documents.list/readJSON/readCSV, $contacts.selected,
$maps.search/geocode/reverseGeocode/snapshot,
$weather.forecast and $home.items/setPower/runScene.
These require Package 2.0 minimumRuntimeVersion 1.6. Declare the
matching permission; Calendar writes also require calendarWrite.
Weather requires weather, network and api.met.no; return and display
provider attribution. New capabilities need native per-widget System
Access except Health, which uses Health Settings review. Copies do
not inherit grants. All calls share eight queries and reject in future
entries/Live Activities; writes require explicit native user actions.
Persist a canonical requestID UUID before calendar/alarm/scene writes;
uncertain outcomes require inspection, never a blind new operation.
Native replay receipts are bounded and completed ones expire after
seven days. Documents use imported native UUIDs, never arbitrary paths.
Contacts/Home expose native selections only. Home values are cached,
and scene actions may require system authentication. AlarmKit needs
iOS/iPadOS 26; native macOS must show unavailable. Mocks do not prove
native authorization, system delivery or physical accessory state.
16. Location needs the location package permission and approval in the
main app. Widget access is separate: use isAuthorizedForWidgetUpdates
when explaining widget setup, never infer permission from cached data.
Catch current() failures and show a useful setup or unavailable state.
Show age/isStale when displaying a cached position; do not invent a fix.
17. Scheduled widgets use timeline-date/timeline-slot from $getenv, not
a replaced Date global. Future entries are read-only: do not save
records, prompt for access or fetch live data while projecting them.
Buttons run separate current actions. Use saved display snapshots
and label their age; do not imply future personal measurements exist.
18. $file.exists(path) checks regular files inside the current package,
including binary images. It cannot read outside the package or
download iCloud content, and does not validate image bytes.
Give a useful missing-resource state instead of inventing a photo.
"""
let reference = reference.combined
return rules + "\n\n" + reference
}
static func userPromptFirst(userDescription: String, size: AIWidgetSize) -> String {
"""
Widget size: \(size.rawValue)
Size hint: \(size.designHint)
User description:
\(userDescription)
Return the complete JSX snippet. No markdown, no explanation.
"""
}
static func userPromptFix(
previousCode: String,
errorSummary: String,
recentLogs: [String]
) -> String {
let logBlock: String
if recentLogs.isEmpty {
logBlock = "(no console output)"
} else {
logBlock = recentLogs.suffix(10).joined(separator: "\n")
}
return """
Your previous code failed to run:
```jsx
\(previousCode)
```
Runtime feedback:
\(errorSummary)
Last console output:
\(logBlock)
Fix the code and return the FULL corrected JSX only. No markdown, no explanation.
"""
}
static func userPromptRefine(currentCode: String, refineInstruction: String) -> String {
"""
Current working widget code:
```jsx
\(currentCode)
```
Apply this change request from the user:
\(refineInstruction)
Return the FULL updated JSX only. No markdown, no explanation.
"""
}
static func userPromptCopilot(
currentCode: String,
instruction: String,
runtimeDiagnostic: String?
) -> String {
let diagnosticBlock: String
if let runtimeDiagnostic, !runtimeDiagnostic.isEmpty {
diagnosticBlock = """
The latest local ScriptWidget runtime diagnostic is:
\(runtimeDiagnostic)
"""
} else {
diagnosticBlock = ""
}
return """
You are editing an existing ScriptWidget project in ScriptWidget Studio.
Current code:
```jsx
\(currentCode)
```
\(diagnosticBlock)
User request:
\(instruction)
Preserve working behavior that is unrelated to the request. Use only documented
ScriptWidget APIs. Return the FULL updated JSX only, with no markdown or explanation.
"""
}
// Best-effort extraction: prefer content between matching ```jsx ...
// ``` fences, then strip any leading/trailing prose.
static func stripCodeFences(_ raw: String) -> String {
var text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
// Prefer fenced block if present.
if let fenced = extractFencedBlock(text) {
text = fenced
}
// Drop stray code-fence markers.
text = text.replacingOccurrences(of: "```jsx", with: "")
text = text.replacingOccurrences(of: "```javascript", with: "")
text = text.replacingOccurrences(of: "```js", with: "")
text = text.replacingOccurrences(of: "```", with: "")
return text.trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func extractFencedBlock(_ raw: String) -> String? {
guard let openRange = raw.range(of: "```") else { return nil }
let afterOpen = raw[openRange.upperBound...]
// Skip optional language tag on the same line.
let afterNewline: Substring
if let nl = afterOpen.firstIndex(of: "\n") {
afterNewline = afterOpen[afterOpen.index(after: nl)...]
} else {
afterNewline = afterOpen
}
guard let closeRange = afterNewline.range(of: "```") else { return nil }
return String(afterNewline[..<closeRange.lowerBound])
}
}