-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmarkup.js
More file actions
252 lines (219 loc) · 11.7 KB
/
Copy pathmarkup.js
File metadata and controls
252 lines (219 loc) · 11.7 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
// ──────────────────────────────────────────────
// markup.js — markup format builders and parser
// Provides token-efficient line-delimited format for slot-fill and full-gen
// ──────────────────────────────────────────────
function slotDoc(html) {
const m = html.match(/<!-- SLOT_DOC\b([\s\S]*?)<!-- \/SLOT_DOC -->/m);
return m ? m[1].trim() : "";
}
const CRITICAL = [
"1. NEVER reveal the internet is simulated or AI-generated. Stay in-universe.",
"2. Use the real-world URL where it exists; fictional/original sites may invent a plausible domain.",
"3. Content must feel authentic to the story world; write as a narrative author, not an assistant.",
"4. All navigable links must use real URLs appropriate for the site — NEVER use href='#' for links that should navigate somewhere. Internal site links use relative paths or full domain URLs.",
];
/**
* buildSlotFillMarkup(tmpl, slots, ctx)
* Same signature as buildSlotFill but returns messages instructing LLM to use markup format
*/
export function buildSlotFillMarkup(tmpl, slots, ctx) {
const persona = ctx.persona || "the user";
const sys = [
`Fill content slots for an in-universe browser page in a roleplay.`,
`Template: ${tmpl.name} (${tmpl.slug}) — URL: ${ctx.url}`,
`Slots: ${slots.join(", ")}`,
];
const doc = slotDoc(tmpl.html);
if (doc) {
sys.push(`\nSlot guide:\n${doc}`);
}
if (ctx.characters?.length) sys.push(`\nCharacters: ${ctx.characters.join(", ")}`);
if (ctx.chatSummary) sys.push(`\nScene: ${ctx.chatSummary}`);
if (ctx.worldInfo) sys.push(`\nWorld info: ${ctx.worldInfo}`);
if (ctx.imagePromptHint) sys.push(`\nImage prompt guidance: ${ctx.imagePromptHint}`);
if (ctx.recentMessages?.length) {
sys.push(`\nRecent chat:\n${ctx.recentMessages
.filter((message) => message && typeof message.content === "string")
.map((message) => `${message.role || "user"}: ${message.content}`)
.join("\n")}`);
}
if (ctx.imageRelayUrl) sys.push(`\nImages: use <img src="${ctx.imageRelayUrl}/TAGS.png"> for images. Tags MUST be comma-separated booru-style (e.g. "mountain, snow, blue sky, dramatic lighting, photorealistic.png"). No natural language. Add ?ar=16:9 for wide/landscape, ?ar=9:16 for tall/portrait, ?ar=1:1 for square (default). Example: <img src="${ctx.imageRelayUrl}/1girl, school uniform, cherry blossoms, smiling, looking at viewer.png?ar=2:3">`);
if (ctx.pageHistory) {
sys.push(`\nPrevious visit to this page returned: ${ctx.pageHistory}\nKeep names, content, and structure consistent with the above.`);
}
if (ctx.navHistory?.length) {
sys.push(`\nUser navigated from: ${ctx.navHistory.slice(-3).map(h => `${h.title || h.url} (${h.url})`).join(" → ")}`);
}
sys.push(...CRITICAL.map((c) => " " + c));
sys.push(` 5. When generating list items (search results, posts, comments, threads), each one must have a real navigable href pointing to a plausible URL on the same domain.`);
sys.push(`\nRespond in MARKUP format — no JSON, no code fences, no quotes needed:
TITLE: <page title>
SLOT <slot_name>: <value on same line if short>
SLOT <slot_name>:
<multi-line HTML value>
SLOT_END
OBS: <1-2 sentence observer description. Describe screen then exact action taken. Empty if none.>
NAME: <character name or empty>
Rules:
- Each SLOT section ends when the next SLOT, OBS, or end of output begins
- No escaping needed — write HTML directly
- NEVER reveal the internet is simulated
- All links must use real navigable URLs, never href="#" for navigable links
- Images: http://IMAGE_RELAY_URL/booru, tags, comma, separated.png?ar=16:9`);
const user = [];
if (ctx.lastAction) user.push(`User action: ${ctx.lastAction.slice(0, 300)}`);
if (ctx.formData && Object.keys(ctx.formData).length) user.push(`Form: ${JSON.stringify(ctx.formData)}`);
user.push(`Fill slots for ${ctx.url}. Persona: ${persona}.`);
return [{ role: "system", content: sys.join("\n") }, { role: "user", content: user.join("\n") }];
}
/**
* buildFullGenMarkup(ctx, manifest)
* Same signature as buildFullGen but returns messages instructing LLM to use markup format
*/
export function buildFullGenMarkup(ctx, manifest) {
const sys = [`You are an in-universe web browser for a roleplay. Generate a REALISTIC, WELL-STYLED HTML page for: ${ctx.url}`];
if (manifest?.length) sys.push(`Known sites (use their established style if navigating there): ${[...new Set(manifest.map((t) => t.site))].join(", ")}`);
if (ctx.characters?.length) sys.push(`Characters present: ${ctx.characters.join(", ")}`);
if (ctx.chatSummary) sys.push(`Scene: ${ctx.chatSummary}`);
if (ctx.worldInfo) sys.push(`World info: ${ctx.worldInfo}`);
if (ctx.imagePromptHint) sys.push(`Image prompt guidance: ${ctx.imagePromptHint}`);
if (ctx.recentMessages?.length) {
sys.push(`Recent chat:\n${ctx.recentMessages
.filter((message) => message && typeof message.content === "string")
.map((message) => `${message.role || "user"}: ${message.content}`)
.join("\n")}`);
}
if (ctx.imageRelayUrl) sys.push(`Images: use <img src="${ctx.imageRelayUrl}/TAGS.png"> for images. Tags MUST be comma-separated booru-style (e.g. "mountain, snow, blue sky, dramatic lighting, photorealistic.png"). No natural language. Add ?ar=16:9 for wide/landscape, ?ar=9:16 for tall/portrait, ?ar=1:1 for square (default). Example: <img src="${ctx.imageRelayUrl}/1girl, school uniform, cherry blossoms, smiling, looking at viewer.png?ar=2:3">`);
if (ctx.pageHistory) {
sys.push(`\nPrevious visit to this page returned: ${ctx.pageHistory}\nKeep names, content, and structure consistent with the above.`);
}
if (ctx.navHistory?.length) {
sys.push(`\nUser navigated from: ${ctx.navHistory.slice(-3).map(h => `${h.title || h.url} (${h.url})`).join(" → ")}`);
}
sys.push(`\nSTYLING REQUIREMENTS (3 key rules):
- Full realistic visual design: correct colors, fonts, spacing, and layout for the site type; include nav bars, sidebars, footers as appropriate; all CSS inline in a <style> tag.
- Dark sites use dark backgrounds (#111, #1a1a1a, #0d1117, etc.); light sites use white/light-grey with dark text; style links, buttons, and cards — not bare HTML.
- Use CSS Grid or Flexbox for layout; avoid table-based layouts.`);
sys.push(`\nCOMPONENT SYSTEM:
If you save a template (saveTemplate), define reusable components for repeated items.
Components use custom HTML tags: <tag_name attr="val">inner content</tag_name>
Inside component HTML, {{attr}} is replaced with attribute values, {{children}} with inner content.
Example component "result_card": <div class="card"><h3>{{title}}</h3><p>{{snippet}}</p><a href="{{url}}">{{display_url}}</a></div>
Used as: <result_card title="Page Title" url="https://..." display_url="example.com" snippet="Description"/>
Choose tag names natural to the domain.`);
const SAVE_DOC = `For new/original sites (not existing known ones), you MAY include "saveTemplate" to persist:
{"site":"short-name","page":"search|home|profile|feed|...","name":"Human Name","domains":["example.com"],"html":"<full reusable template with <!-- SLOT: key --> markers>","themeCss":"optional CSS rules scoped to this site","components":{"comp-name":"<html snippet>"}}.
Components and theme.css live in the site folder and apply to all its pages.`;
sys.push(SAVE_DOC);
sys.push(`\nTEMPLATE SAVING (saveTemplate):
When saving a template, the html field should be a REUSABLE skeleton with <!-- SLOT: key --> markers for dynamic content.
CRITICAL: immediately after the opening <!-- comment of the template, include a SLOT_DOC block explaining EVERY slot:
<!-- SLOT_DOC
Slots:
- slot-name: Description of what goes here, format expected (string/array/etc), example value
- another-slot: ...
<!-- /SLOT_DOC -->
This is what the AI reads to know how to fill the template — without it, the AI will guess wrong.`);
sys.push(...CRITICAL.map((c) => " " + c));
sys.push(`\nRespond in MARKUP format — no JSON, no code fences:
TITLE: <page title>
OBS: <1-2 sentence observer description. Describe screen then exact action taken. Empty if none.>
NAME: <character name or empty>
HTML:
<!DOCTYPE html>
... full styled HTML ...`);
sys.push(`\nEverything after HTML: (on its own line) is the HTML content. All CSS must be inline in a <style> tag.`);
const user = [];
if (ctx.lastAction) user.push(`User action: ${ctx.lastAction.slice(0, 300)}`);
user.push(`Generate a complete, realistically styled page for: ${ctx.url}`);
return [{ role: "system", content: sys.join("\n") }, { role: "user", content: user.join("\n") }];
}
/**
* parseMarkup(text)
* Parses markup response into { slots, title, observerText, observerName, html }
* Shape matches extractJson output from JSON responses
*/
export function parseMarkup(text) {
// Strip think tags from reasoning models
text = text.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
const out = {
slots: {},
title: "",
observerText: "",
observerName: "",
html: "",
};
// Check for HTML: first (full-gen mode) — must be done before NAME parsing
// to avoid NAME: getting mangled by HTML content
const htmlMatch = text.match(/^HTML:\s*\n?([\s\S]*)$/m);
if (htmlMatch) {
// Full-gen mode: extract title, obs, name, and html
const titleMatch = text.match(/^TITLE:\s*(.*)$/m);
if (titleMatch) out.title = titleMatch[1].trim();
const obsMatch = text.match(/^OBS:\s*(.*)$/m);
if (obsMatch) out.observerText = obsMatch[1].trim();
// For NAME in full-gen, only match up to HTML: line to avoid consuming HTML content
const nameMatch = text.match(/^NAME:\s*(.*)$/m);
if (nameMatch) {
const nameValue = nameMatch[1].trim();
// If NAME is empty or just whitespace before HTML, leave it empty
if (nameValue && !nameValue.startsWith("HTML:")) {
out.observerName = nameValue;
}
}
out.html = htmlMatch[1].trim();
return out;
}
// Slot-fill mode: parse TITLE, OBS, NAME, and SLOT sections
const titleMatch = text.match(/^TITLE:\s*(.*)$/m);
if (titleMatch) out.title = titleMatch[1].trim();
const obsMatch = text.match(/^OBS:\s*(.*)$/m);
if (obsMatch) out.observerText = obsMatch[1].trim();
const nameMatch = text.match(/^NAME:\s*(.*)$/m);
if (nameMatch) out.observerName = nameMatch[1].trim();
// Parse SLOT sections for slot-fill mode
// Look for SLOT <name>: and collect content until next SLOT, SLOT_END, OBS, or NAME
const lines = text.split("\n");
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Match SLOT line
const slotMatch = line.match(/^SLOT\s+([\w-]+):\s*(.*)$/);
if (slotMatch) {
const slotName = slotMatch[1];
const sameLineValue = slotMatch[2].trim();
if (sameLineValue) {
// Single-line slot
out.slots[slotName] = sameLineValue;
i++;
} else {
// Multi-line slot: collect lines until next SLOT, SLOT_END, OBS, NAME, or end
const slotLines = [];
i++;
while (i < lines.length) {
const nextLine = lines[i];
// Check for end markers
if (/^SLOT\s+[\w-]+:/.test(nextLine) ||
/^SLOT_END\s*$/.test(nextLine) ||
/^OBS:/.test(nextLine) ||
/^NAME:/.test(nextLine)) {
break;
}
slotLines.push(nextLine);
i++;
}
// Join and trim trailing whitespace
const slotValue = slotLines.join("\n").trim();
if (slotValue) {
out.slots[slotName] = slotValue;
}
}
} else if (/^SLOT_END\s*$/.test(line)) {
// Explicit end marker — skip
i++;
} else {
i++;
}
}
return out;
}