Spaces:
Sleeping
Sleeping
File size: 12,236 Bytes
e5e233a 9b49194 e5e233a 9d479ea e5e233a 9b49194 e5e233a 93ef16f e5e233a 9b49194 363fbdc e5e233a 363fbdc e5e233a 9b49194 e5e233a 9b49194 e5e233a 9b49194 e5e233a 9b49194 e5e233a 2ad2aff e5e233a 363fbdc e5e233a 363fbdc e5e233a 1f1507e e5e233a 2ad2aff e5e233a 9b49194 e5e233a 26dfc7b e5e233a 9b49194 e5e233a 26dfc7b e5e233a 9d479ea e5e233a | 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 | import * as Y from 'yjs'
import * as store from './store.js'
import { newId, b64encode, docNameFor, projectIdOf, pageSlugOf } from './util.js'
import { markdownToBlocks } from './md.js'
import { hocuspocus, buildYBlock, createSuggestion, createPageProposal, deleteDoc } from './collab.js'
const FIELD = 'default'
// A personal test project seeded with every construct the suggestion/comment
// system supports, so the owner can play with accepts/rejects/merges and reset
// back to this exact state at any time.
async function withDoc(docName, fn) {
const conn = await hocuspocus.openDirectConnection(projectIdOf(docName), { user: { username: '__server__' } })
try {
let result
await conn.transact(document => {
result = fn(document)
})
return result
} finally {
// see collab.js: an immediate unload races with clients connecting
await conn.disconnect({ unloadImmediately: false })
}
}
const pageField = docName => (pageSlugOf(docName) === 'home' ? FIELD : `page:${pageSlugOf(docName)}`)
const pageMap = (document, base, docName) =>
document.getMap(pageSlugOf(docName) === 'home' ? base : `${base}:${pageSlugOf(docName)}`)
function svgMarkup(label, color) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="480" height="240" viewBox="0 0 480 240">
<rect width="480" height="240" rx="12" fill="${color}"/>
<polyline points="40,190 120,150 200,168 280,96 360,120 440,60" fill="none" stroke="white" stroke-width="5" stroke-linecap="round"/>
<text x="40" y="46" fill="white" font-family="sans-serif" font-size="22" font-weight="bold">${label}</text>
</svg>`
}
function svgUpload(label, color) {
return '/files/' + store.saveUpload(Buffer.from(svgMarkup(label, color)), 'image/svg+xml')
}
// self-contained data: URI so a sandboxed embed can show it with no network access
function svgDataUri(label, color) {
return 'data:image/svg+xml;base64,' + Buffer.from(svgMarkup(label, color)).toString('base64')
}
function seedPage(docName, markdown) {
return withDoc(docName, document => {
const fragment = document.getXmlFragment(pageField(docName))
if (fragment.length) fragment.delete(0, fragment.length)
// the doc may still be loaded in memory from before the reset — the file
// deletion alone doesn't clear its maps
for (const mapName of ['suggestions', 'threads']) {
const map = pageMap(document, mapName, docName)
for (const key of [...map.keys()]) map.delete(key)
}
fragment.insert(0, markdownToBlocks(markdown).map(buildYBlock))
})
}
// server-side comment thread on a character range of block `blockIdx`
function seedThread(document, docName, { blockIdx, from, to, messages, resolved = false, suggestionId = null }) {
const fragment = document.getXmlFragment(pageField(docName))
const block = fragment.get(blockIdx)
const text = block?.get(0)
if (!(text instanceof Y.XmlText)) return null
const len = text.length
const a = Math.min(from, len)
const b = Math.min(to, len)
const threadId = newId(10)
const ythread = new Y.Map()
pageMap(document, 'threads', docName).set(threadId, ythread)
ythread.set('anchorStart', b64encode(Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, a))))
ythread.set('anchorEnd', b64encode(Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, b))))
ythread.set('excerpt', text.toString().slice(a, b))
ythread.set('resolved', resolved)
if (suggestionId) ythread.set('suggestionId', suggestionId)
const arr = new Y.Array()
ythread.set('messages', arr)
arr.push(messages.map(m => ({ id: newId(10), ts: Date.now(), ...m })))
return threadId
}
const HOME_MD = `# Playground
This project is a safe sandbox: every kind of suggestion and comment lives here. Accept, reject, undo, merge — then hit **Reset** in the header to restore this exact state.
## Plain text
The quick brown fox jumps over the lazy dog, and the document keeps calm. This paragraph has a small wording suggestion attached to it.
This second paragraph carries two competing suggestions at once — both stay visible and either one can be accepted.
Formatting survives suggestions: **bold**, *italic*, [links](https://huggingface.co) and \`inline code\` all round-trip.
## Lists
- alpha
- beta
- gamma
1. first step
2. second step
## Checklist
- [x] task lists render with checkboxes
- [ ] this one has a suggestion attached
## Table
| Feature | Status |
| --- | --- |
| Suggestions | done |
| Tables | needs a suggestion |
## Code
\`\`\`python
def greet(name):
message = "Hello, " + name
return message
\`\`\`
> A blockquote to iterate on: this quote has a three-generation revision chain — use the arrows on its card.
## Live HTML embed
Arbitrary HTML renders in a sandboxed frame — ideal for aligning figures side by side or dropping in an animated explainer. Hover it and hit **Edit HTML** to tweak the markup. It runs with no access to the page, your account, or the network.
\`\`\`html-embed
<style>
body { font-family: system-ui, -apple-system, sans-serif; color: #1a1a1a; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
figure { margin: 0; }
figure img { width: 100%; border-radius: 10px; display: block; }
figcaption { font-size: 13px; color: #666; text-align: center; margin-top: 6px; }
.bar { height: 10px; border-radius: 5px; margin-top: 18px;
background: linear-gradient(90deg, #1d5c45, #7c4a9c, #1d5c45);
background-size: 200% 100%; animation: slide 3s linear infinite; }
@keyframes slide { to { background-position: -200% 0; } }
</style>
<div class="row">
<figure><img src="__EMBED_IMG1__"><figcaption>Figure A — aligned</figcaption></figure>
<figure><img src="__EMBED_IMG2__"><figcaption>Figure B — aligned</figcaption></figure>
</div>
<div class="bar"></div>
\`\`\`
## Images
Select the image below to try resizing and alignment.
The paragraph after the images carries an image suggestion.
That is all — happy testing!`
const NOTES_MD = `# Notes
A second page, to try the sidebar, page navigation, and cross-page agent work.
- the structure page defines the tree
- suggest changes to it like any text`
export async function seedPlayground(username, existingId = null) {
const id = existingId || newId(8)
if (existingId) await deleteDoc(existingId)
store.upsertRegistry(id, {
title: 'Playground',
createdBy: username,
createdAt: Date.now(),
updatedAt: Date.now(),
playground: true,
pages: {},
})
const img1 = svgUpload('sample figure', '#1d5c45')
const img2 = svgUpload('suggested chart', '#7c4a9c')
const home = HOME_MD.replace('Select the image below', `\n\nSelect the image above`)
.replace('__EMBED_IMG1__', svgDataUri('figure A', '#1d5c45'))
.replace('__EMBED_IMG2__', svgDataUri('figure B', '#7c4a9c'))
await seedPage(id, home)
await seedPage(docNameFor(id, 'notes'), NOTES_MD)
store.upsertPageMeta(id, 'notes', { title: 'Notes', createdAt: Date.now() })
await seedPage(docNameFor(id, '_structure'), '```yaml\n- home\n- notes\n```')
store.upsertPageMeta(id, '_structure', { title: 'Structure', createdAt: Date.now() })
// block indices in HOME_MD (after image insertion): find dynamically
const blocks = markdownToBlocks(home)
const idxOf = needle => blocks.findIndex(b => JSON.stringify(b).includes(needle))
const agent = { author: 'demo-agent', authorType: 'agent' }
const mk = (blockIdx, replacement, rationale, extra = {}) =>
createSuggestion(id, { blockIndex: blockIdx, replacementMarkdown: replacement, rationale, ...agent, ...extra })
// 1. small inline rewrite
await mk(
idxOf('quick brown fox'),
'The quick brown fox leaps over the lazy dog, and the document stays calm. This paragraph has a small wording suggestion attached to it.',
'Tighter verbs: jumps → leaps, keeps → stays.'
)
// 2. two competing suggestions on one paragraph
const competing = idxOf('two competing suggestions')
await mk(competing, 'This second paragraph carries two rival suggestions at once — both stay visible and either one can be accepted.', 'Option A: "rival".')
await mk(competing, 'This second paragraph demonstrates overlapping suggestions — both stay visible and either one can be accepted.', 'Option B: "demonstrates overlapping".')
// 3. list: add an item
await mk(idxOf('gamma'), '- alpha\n- beta\n- gamma\n- delta', 'Adds the missing fourth item.')
// task-list suggestion: check off the open item
await mk(
idxOf('this one has a suggestion attached'),
'- [x] task lists render with checkboxes\n- [x] this one has a suggestion attached',
'Mark the second task as done.'
)
// table suggestion: fill in the pending status
await mk(
idxOf('needs a suggestion'),
'| Feature | Status |\n| --- | --- |\n| Suggestions | done |\n| Tables | done |',
'Update the table: tables are done.'
)
// 4. new section insertion (formatted panel)
await mk(
idxOf('second step'),
'1. first step\n2. second step\n\n## Checklist\n\n- [links](https://huggingface.co) render in panels\n- **bold** does too',
'Inserts a new section after the numbered list.'
)
// 5. codeblock rewrite (word diff inside code)
await mk(
idxOf('greet'),
'```python\ndef greet(name):\n message = f"Hello, {name}!"\n return message\n```',
'Use an f-string and add the exclamation mark.'
)
// 6. three-generation revision chain on the blockquote
const quoteIdx = idxOf('three-generation')
const s1 = await mk(quoteIdx, '> A blockquote worth iterating on: generation one of the revision chain.', 'First attempt.')
const s2 = await createSuggestion(id, {
supersedes: s1.suggestion.id,
replacementMarkdown: '> A blockquote worth iterating on: generation two, a bit sharper.',
rationale: 'Second attempt — sharper.',
...agent,
})
await createSuggestion(id, {
supersedes: s2.suggestion.id,
replacementMarkdown: '> A blockquote worth iterating on: generation three, the keeper. Use ‹ › on the card to compare.',
rationale: 'Third attempt — the keeper.',
...agent,
})
// 7. image suggestion
await mk(
idxOf('carries an image suggestion'),
`The paragraph after the images carries an image suggestion.\n\n`,
'Adds the missing chart below this paragraph.'
)
// 8. threads: one open with an attached suggestion, one resolved
const attachRes = await withDoc(id, document => {
const openThread = seedThread(document, id, {
blockIdx: idxOf('happy testing'),
from: 0,
to: 16,
messages: [
{ author: username, authorType: 'user', text: 'Can we end on something stronger? @demo-agent' },
{ author: 'demo-agent', authorType: 'agent', text: 'Proposed a punchier closing line — see the related suggestion.' },
],
})
seedThread(document, id, {
blockIdx: idxOf('Formatting survives'),
from: 0,
to: 20,
resolved: true,
messages: [
{ author: username, authorType: 'user', text: 'Do links work in here?' },
{ author: 'demo-agent', authorType: 'agent', text: 'They do — resolved.' },
],
})
return { openThread }
})
await createSuggestion(id, {
blockIndex: idxOf('happy testing'),
replacementMarkdown: 'That is all — go break things, the Reset button has your back!',
rationale: 'Punchier closing, as requested in the thread.',
originThreadId: attachRes.openThread,
...agent,
})
// 9. a proposed NEW page (shows as pending in the sidebar until accepted)
await createPageProposal(id, {
title: 'Proposed Page',
contentMarkdown:
'# Proposed Page\n\nAn agent proposed this whole page in one suggestion. Accept it from the sidebar to create it, or reject it.\n\n- [ ] try accepting me\n\n| Try | Result |\n| --- | --- |\n| Accept | page is created |',
rationale: 'Demonstrates one-step new-page proposals.',
...agent,
})
return { id }
}
export function findPlayground(username) {
for (const [id, meta] of Object.entries(store.getRegistry())) {
if (meta.playground && meta.createdBy === username) return id
}
return null
}
|