From d81b212de543ec2ed98bace0bece0e47f6d0bbec Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Sun, 14 Jun 2026 12:38:28 +0100 Subject: [PATCH 1/2] Add writer schema request copy and response import --- .../src/tests/jest/writer-schema-page.test.js | 119 +++++++- apps/web/src/writer-schema-page.mjs | 253 ++++++++++++++---- apps/web/styles.css | 18 +- apps/web/writer-schema.html | 24 ++ 4 files changed, 358 insertions(+), 56 deletions(-) diff --git a/apps/web/src/tests/jest/writer-schema-page.test.js b/apps/web/src/tests/jest/writer-schema-page.test.js index cdf6f41c..28a7b7e4 100644 --- a/apps/web/src/tests/jest/writer-schema-page.test.js +++ b/apps/web/src/tests/jest/writer-schema-page.test.js @@ -12,6 +12,7 @@ import { describe('writer schema prototype page', () => { let dom; + let navigatorObj; beforeEach(() => { dom = new JSDOM( @@ -28,11 +29,20 @@ describe('writer schema prototype page', () => {
No raw Writer response yet.
No errors yet.
  1. No generation activity yet.
+ + + +
`, { url: 'https://example.test/writer-schema.html' } ); + navigatorObj = { + clipboard: { + writeText: jest.fn(async () => {}), + }, + }; }); afterEach(() => { @@ -170,6 +180,57 @@ describe('writer schema prototype page', () => { expect(writer.destroy).toHaveBeenCalledTimes(1); }); + test('runWriterSchemaGeneration accumulates streaming Writer chunks into structured output', async () => { + const writer = { + destroy: jest.fn(), + writeStreaming: jest.fn(async function* () { + yield '{"schemaFields":['; + yield '{"name":"Book Title","sourceType":"domain","command":"commerce.productName"},'; + yield '{"name":"Genre","sourceType":"enum","values":["Fiction","Non-fiction"]}'; + yield ']}'; + }), + }; + const WriterCtor = { + create: jest.fn(async () => writer), + }; + + const result = await runWriterSchemaGeneration({ + WriterCtor, + promptText: DEFAULT_PROMPT, + domainCommands: ['commerce.productName', 'person.fullName'], + sampleSchemaText: 'Name\nperson.fullName', + onStatus: jest.fn(), + }); + + expect(WriterCtor.create).toHaveBeenCalledTimes(1); + expect(writer.writeStreaming).toHaveBeenCalledWith( + DEFAULT_PROMPT, + expect.objectContaining({ + context: expect.any(String), + expectedInputLanguages: ['en'], + expectedContextLanguages: ['en'], + outputLanguage: 'en', + }) + ); + expect(result.parsedPayload.schemaFields).toHaveLength(2); + expect(result.requestDetails).toMatchObject({ + promptText: DEFAULT_PROMPT, + taskContext: expect.any(String), + writeOptions: expect.objectContaining({ + outputLanguage: 'en', + }), + createOptions: expect.objectContaining({ + sharedContext: expect.any(String), + }), + }); + expect(result.schemaRows).toMatchObject([ + { name: 'Book Title', sourceType: 'domain', command: 'commerce.productName' }, + { name: 'Genre', sourceType: 'enum', value: '"Fiction","Non-fiction"' }, + ]); + expect(result.normalizationErrors).toEqual([]); + expect(writer.destroy).toHaveBeenCalledTimes(1); + }); + test('bootstrap warns when Writer API support is unavailable', async () => { const schemaComponent = { destroy: jest.fn(), @@ -184,6 +245,7 @@ describe('writer schema prototype page', () => { await bootstrapWriterSchemaPage({ documentObj: dom.window.document, WriterCtor: null, + navigatorObj, createThemeToggleComponentFn: () => ({ destroy: jest.fn() }), createSharedSchemaDefinitionComponentFn: () => schemaComponent, }); @@ -219,6 +281,7 @@ describe('writer schema prototype page', () => { const page = await bootstrapWriterSchemaPage({ documentObj: dom.window.document, WriterCtor, + navigatorObj, createThemeToggleComponentFn: () => ({ destroy: jest.fn() }), createSharedSchemaDefinitionComponentFn: () => schemaComponent, }); @@ -233,7 +296,7 @@ describe('writer schema prototype page', () => { ); expect(schemaComponent.syncTextFromRows).toHaveBeenCalledTimes(1); expect(dom.window.document.getElementById('writer-schema-generation-status').textContent).toContain( - 'Generated 1 schema fields' + 'Processed Writer API output into 1 schema fields.' ); expect(dom.window.document.getElementById('writer-schema-json-output').textContent).toContain('Book Title'); expect(dom.window.document.getElementById('writer-schema-request-output').textContent).toContain( @@ -244,7 +307,19 @@ describe('writer schema prototype page', () => { ); expect(dom.window.document.getElementById('writer-schema-error-output').textContent).toBe('No errors yet.'); expect(dom.window.document.getElementById('writer-schema-progress-output').textContent).toContain( - 'Schema generation completed successfully.' + 'Processed Writer API output successfully.' + ); + + await page.copyLatestRequestJson(); + await page.copyLatestRequestAsPrompt(); + + expect(navigatorObj.clipboard.writeText).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('"promptText": "Create 10 fields that represent the inventory of a bookshop"') + ); + expect(navigatorObj.clipboard.writeText).toHaveBeenNthCalledWith( + 2, + expect.stringContaining('Generate an AnyWayData schema response using the following request details.') ); }); @@ -272,6 +347,7 @@ describe('writer schema prototype page', () => { const page = await bootstrapWriterSchemaPage({ documentObj: dom.window.document, WriterCtor, + navigatorObj, createThemeToggleComponentFn: () => ({ destroy: jest.fn() }), createSharedSchemaDefinitionComponentFn: () => schemaComponent, }); @@ -325,6 +401,7 @@ describe('writer schema prototype page', () => { const page = await bootstrapWriterSchemaPage({ documentObj: dom.window.document, WriterCtor, + navigatorObj, createThemeToggleComponentFn: () => ({ destroy: jest.fn() }), createSharedSchemaDefinitionComponentFn: () => schemaComponent, }); @@ -341,7 +418,43 @@ describe('writer schema prototype page', () => { 'unsupported command "commerce.publisher"' ); expect(dom.window.document.getElementById('writer-schema-progress-output').textContent).toContain( - 'Completed with partial recovery.' + 'Processed Writer API output with partial recovery.' + ); + }); + + test('bootstrap processes a pasted AI response into schema rows', async () => { + const schemaComponent = { + destroy: jest.fn(), + replaceRows: jest.fn(), + setTextMode: jest.fn(), + render: jest.fn(), + syncTextFromRows: jest.fn(), + validateRows: jest.fn(() => ({ errors: [] })), + getSchemaText: jest.fn(() => 'Book Title\ncommerce.productName()'), + }; + + const page = await bootstrapWriterSchemaPage({ + documentObj: dom.window.document, + WriterCtor: null, + navigatorObj, + createThemeToggleComponentFn: () => ({ destroy: jest.fn() }), + createSharedSchemaDefinitionComponentFn: () => schemaComponent, + }); + + dom.window.document.getElementById('writer-schema-process-response').value = JSON.stringify({ + schemaFields: [{ name: 'Book Title', sourceType: 'domain', command: 'commerce.productName' }], + }); + + await page.createSchemaFromResponse(); + + expect(schemaComponent.replaceRows).toHaveBeenCalledWith( + expect.arrayContaining([expect.objectContaining({ name: 'Book Title', command: 'commerce.productName' })]) + ); + expect(dom.window.document.getElementById('writer-schema-generation-status').textContent).toContain( + 'Processed pasted AI response into 1 schema fields.' + ); + expect(dom.window.document.getElementById('writer-schema-raw-output').textContent).toContain( + 'commerce.productName' ); }); }); diff --git a/apps/web/src/writer-schema-page.mjs b/apps/web/src/writer-schema-page.mjs index c170d8d1..8e4412b8 100644 --- a/apps/web/src/writer-schema-page.mjs +++ b/apps/web/src/writer-schema-page.mjs @@ -22,6 +22,7 @@ import { } from '../../../packages/core-ui/js/gui_components/app/test-data-grid/schema/test-data-command-catalog.js'; import { getDomainCommandHelp } from '../../../packages/core-ui/js/gui_components/shared/domain-command-help-metadata.js'; import { getVisibleDomainCommands } from '../../../packages/core-ui/js/gui_components/shared/test-data/help/domain-command-provider.js'; +import { renderIconHtml } from '../../../packages/core-ui/js/gui_components/shared/primitives/icon/icon-core.js'; const DEFAULT_PROMPT = 'Create 10 fields that represent the inventory of a bookshop'; const WRITER_SUPPORTED_SOURCE_TYPES = new Set(['domain', 'enum', 'literal', 'regex']); @@ -129,6 +130,39 @@ function buildWriterRequestDetails({ promptText, sharedContext, taskContext, wri }; } +function createCopyApiRequestPrompt(requestDetails = {}) { + const promptText = String(requestDetails.promptText || '').trim(); + const sharedContext = String(requestDetails.sharedContext || '').trim(); + const taskContext = String(requestDetails.taskContext || '').trim(); + const writeOptionsJson = JSON.stringify(requestDetails.writeOptions || {}, null, 2); + + return [ + 'Generate an AnyWayData schema response using the following request details.', + 'Return JSON only, with no markdown fences or explanation.', + '', + 'User prompt:', + promptText, + '', + 'Shared context:', + sharedContext, + '', + 'Task context:', + taskContext, + '', + 'Write options:', + writeOptionsJson, + ].join('\n'); +} + +async function copyTextToClipboard(text, { navigatorObj = globalThis.navigator } = {}) { + const clipboard = navigatorObj?.clipboard; + if (typeof clipboard?.writeText !== 'function') { + throw new Error('Clipboard copy is not available in this browser session.'); + } + + await clipboard.writeText(String(text ?? '')); +} + function createBlankRowFactory(prefix = 'writer-schema-row') { let counter = 0; return () => ({ @@ -659,6 +693,7 @@ async function runWriterSchemaGeneration({ WriterCtor, promptText, domainCommand async function bootstrapWriterSchemaPage({ documentObj = globalThis.document, WriterCtor = globalThis.Writer, + navigatorObj = globalThis.navigator, createThemeToggleComponentFn = createThemeToggleComponent, createSharedSchemaDefinitionComponentFn = createSharedSchemaDefinitionComponent, } = {}) { @@ -673,11 +708,24 @@ async function bootstrapWriterSchemaPage({ const errorOutputElement = documentObj.getElementById('writer-schema-error-output'); const progressOutputElement = documentObj.getElementById('writer-schema-progress-output'); const requestOutputElement = documentObj.getElementById('writer-schema-request-output'); + const processResponseElement = documentObj.getElementById('writer-schema-process-response'); const promptElement = documentObj.getElementById('writer-schema-prompt'); const generateButton = documentObj.getElementById('writer-schema-generate'); const examplePromptButton = documentObj.getElementById('writer-schema-example-prompt'); + const copyRequestButton = documentObj.getElementById('writer-schema-copy-request'); + const copyPromptButton = documentObj.getElementById('writer-schema-copy-prompt'); + const createSchemaFromResponseButton = documentObj.getElementById('writer-schema-create-schema'); const schemaRoot = documentObj.getElementById('writer-schema-editor-root'); + copyRequestButton?.insertAdjacentHTML( + 'afterbegin', + `${renderIconHtml('copy', { className: 'app-icon writer-schema-action-icon' })}Copy JSON` + ); + copyPromptButton?.insertAdjacentHTML( + 'afterbegin', + `${renderIconHtml('clipboard-paste', { className: 'app-icon writer-schema-action-icon' })}Copy Prompt` + ); + const schemaDefinitionProps = createSchemaDefinitionProps(); const schemaComponent = createSharedSchemaDefinitionComponentFn({ root: schemaRoot, @@ -737,6 +785,77 @@ async function bootstrapWriterSchemaPage({ } let isGenerating = false; + let latestRequestDetails = null; + + function applySchemaResult(result, { sourceLabel = 'schema response', keepRawResponse = true } = {}) { + setJsonOutput(jsonOutputElement, result.parsedPayload); + if (keepRawResponse) { + setTextOutput(rawOutputElement, result.responseText, 'No raw Writer response yet.'); + } + + const normalizationErrors = Array.isArray(result.normalizationErrors) ? result.normalizationErrors : []; + setTextOutput( + errorOutputElement, + normalizationErrors.length > 0 ? normalizationErrors.map((item) => item.message).join('\n\n') : '', + 'No errors yet.' + ); + if (result.requestDetails) { + latestRequestDetails = result.requestDetails; + setJsonOutput(requestOutputElement, result.requestDetails); + } + schemaComponent.setTextMode?.(false); + schemaComponent.replaceRows?.(result.schemaRows); + schemaComponent.render?.(); + schemaComponent.syncTextFromRows?.(); + + const validation = schemaComponent.validateRows?.() || { errors: [] }; + const renderedSchemaText = + typeof schemaComponent.getSchemaText === 'function' + ? schemaComponent.getSchemaText() + : result.schemaRows.map((row) => `${row.name}\n${buildRuleSpecFromSchemaRow(row)}`).join('\n\n'); + + if (Array.isArray(validation.errors) && validation.errors.length > 0) { + throw new Error(validation.errors.map((error) => error?.message || String(error)).join('; ')); + } + + if (normalizationErrors.length > 0) { + setStatus( + generationStatusElement, + `Processed ${sourceLabel} into ${result.schemaRows.length} schema fields. ${normalizationErrors.length} generated field${normalizationErrors.length === 1 ? '' : 's'} could not be mapped and were left out.`, + { severity: 'warning' } + ); + appendProgressOutput( + progressOutputElement, + `Processed ${sourceLabel} with partial recovery. ${normalizationErrors.length} field${normalizationErrors.length === 1 ? '' : 's'} were rejected during normalization.`, + documentObj + ); + } else { + setStatus(generationStatusElement, `Processed ${sourceLabel} into ${result.schemaRows.length} schema fields.`, { + severity: 'info', + }); + appendProgressOutput(progressOutputElement, `Processed ${sourceLabel} successfully.`, documentObj); + } + + return { + ...result, + renderedSchemaText, + }; + } + + function parseSchemaFromAiResponse(responseText) { + const parsedPayload = parseWriterStructuredOutput(responseText); + const { schemaRows, normalizationErrors } = normalizeStructuredSchemaPayload(parsedPayload, { + allowedDomainCommands: schemaDefinitionProps.writerContextDomainCommands, + }); + + return { + parsedPayload, + schemaRows, + normalizationErrors, + responseText, + requestDetails: latestRequestDetails, + }; + } async function generateFromPrompt() { const promptText = String(promptElement?.value || '').trim(); @@ -784,57 +903,7 @@ async function bootstrapWriterSchemaPage({ } }, }); - - setJsonOutput(jsonOutputElement, result.parsedPayload); - setTextOutput(rawOutputElement, result.responseText, 'No raw Writer response yet.'); - const normalizationErrors = Array.isArray(result.normalizationErrors) ? result.normalizationErrors : []; - setTextOutput( - errorOutputElement, - normalizationErrors.length > 0 - ? normalizationErrors.map((item) => item.message).join('\n\n') - : '', - 'No errors yet.' - ); - setJsonOutput(requestOutputElement, result.requestDetails); - schemaComponent.setTextMode?.(false); - schemaComponent.replaceRows?.(result.schemaRows); - schemaComponent.render?.(); - schemaComponent.syncTextFromRows?.(); - - const validation = schemaComponent.validateRows?.() || { errors: [] }; - const renderedSchemaText = - typeof schemaComponent.getSchemaText === 'function' - ? schemaComponent.getSchemaText() - : result.schemaRows.map((row) => `${row.name}\n${buildRuleSpecFromSchemaRow(row)}`).join('\n\n'); - - if (Array.isArray(validation.errors) && validation.errors.length > 0) { - throw new Error(validation.errors.map((error) => error?.message || String(error)).join('; ')); - } - - if (normalizationErrors.length > 0) { - setStatus( - generationStatusElement, - `Generated ${result.schemaRows.length} schema fields. ${normalizationErrors.length} generated field${normalizationErrors.length === 1 ? '' : 's'} could not be mapped and were left out.`, - { severity: 'warning' } - ); - appendProgressOutput( - progressOutputElement, - `Completed with partial recovery. ${normalizationErrors.length} field${normalizationErrors.length === 1 ? '' : 's'} were rejected during normalization.`, - documentObj - ); - } else { - setStatus( - generationStatusElement, - `Generated ${result.schemaRows.length} schema fields and populated the shared schema editor.`, - { severity: 'info' } - ); - appendProgressOutput(progressOutputElement, 'Schema generation completed successfully.', documentObj); - } - - return { - ...result, - renderedSchemaText, - }; + return applySchemaResult(result, { sourceLabel: 'Writer API output' }); } catch (error) { const message = error instanceof Error ? error.message : String(error); const fullErrorText = @@ -857,19 +926,101 @@ async function bootstrapWriterSchemaPage({ } } + async function copyLatestRequestJson() { + if (!latestRequestDetails) { + setStatus(generationStatusElement, 'Generate a schema before copying the full request.', { + severity: 'warning', + }); + return false; + } + + await copyTextToClipboard(JSON.stringify(latestRequestDetails, null, 2), { navigatorObj }); + setStatus(generationStatusElement, 'Copied the full request JSON to the clipboard.', { + severity: 'info', + }); + return true; + } + + async function copyLatestRequestAsPrompt() { + if (!latestRequestDetails) { + setStatus(generationStatusElement, 'Generate a schema before copying the request as a prompt.', { + severity: 'warning', + }); + return false; + } + + await copyTextToClipboard(createCopyApiRequestPrompt(latestRequestDetails), { navigatorObj }); + setStatus(generationStatusElement, 'Copied the API request as a reusable prompt.', { + severity: 'info', + }); + return true; + } + + async function createSchemaFromResponse() { + const responseText = String(processResponseElement?.value || '').trim(); + if (!responseText) { + setStatus(generationStatusElement, 'Paste an AI response before creating a schema.', { + severity: 'warning', + }); + return null; + } + + setTextOutput(rawOutputElement, responseText, 'No raw Writer response yet.'); + appendProgressOutput(progressOutputElement, 'Processing pasted AI response into schema rows.', documentObj); + + try { + const result = parseSchemaFromAiResponse(responseText); + return applySchemaResult(result, { sourceLabel: 'pasted AI response' }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + setTextOutput( + errorOutputElement, + error instanceof Error && error.stack ? error.stack : `Error: ${message}`, + 'No errors yet.' + ); + appendProgressOutput(progressOutputElement, `Processing pasted AI response failed: ${message}`, documentObj); + setStatus(generationStatusElement, `Unable to process the pasted AI response: ${message}`, { + severity: 'error', + }); + throw error; + } + } + examplePromptButton?.addEventListener('click', () => { promptElement.value = DEFAULT_PROMPT; promptElement.focus(); }); + copyRequestButton?.addEventListener('click', () => { + void copyLatestRequestJson().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + setStatus(generationStatusElement, `Unable to copy the full request: ${message}`, { + severity: 'error', + }); + }); + }); + copyPromptButton?.addEventListener('click', () => { + void copyLatestRequestAsPrompt().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + setStatus(generationStatusElement, `Unable to copy the request prompt: ${message}`, { + severity: 'error', + }); + }); + }); generateButton?.addEventListener('click', () => { void generateFromPrompt().catch(() => {}); }); + createSchemaFromResponseButton?.addEventListener('click', () => { + void createSchemaFromResponse().catch(() => {}); + }); return { destroy() { themeToggle?.destroy?.(); schemaComponent?.destroy?.(); }, + copyLatestRequestAsPrompt, + copyLatestRequestJson, + createSchemaFromResponse, generateFromPrompt, getSchemaComponent() { return schemaComponent; diff --git a/apps/web/styles.css b/apps/web/styles.css index a427cfef..050ed60f 100644 --- a/apps/web/styles.css +++ b/apps/web/styles.css @@ -280,6 +280,12 @@ body.theme-dark .dragdropzone { margin: 0; } +.writer-schema-inline-actions { + display: inline-flex; + flex-wrap: wrap; + gap: 0.5rem; +} + .writer-schema-label { display: inline-block; margin-bottom: 0.5rem; @@ -302,7 +308,7 @@ body.theme-dark .dragdropzone { .writer-schema-json-output { overflow: auto; white-space: pre-wrap; - word-break: break-word; + overflow-wrap: break-word; font-family: 'Courier New', Courier, monospace; line-height: 1.4; resize: vertical; @@ -312,7 +318,6 @@ body.theme-dark .dragdropzone { .writer-schema-progress-output { box-sizing: border-box; margin: 0; - padding: 0 0 0 1.25rem; display: flex; flex-direction: column; gap: 0.55rem; @@ -342,6 +347,7 @@ body.theme-dark .dragdropzone { } .writer-schema-card__header > button, +.writer-schema-inline-actions > button, .writer-schema-actions > button { border: 0; border-radius: 999px; @@ -354,11 +360,19 @@ body.theme-dark .dragdropzone { } .writer-schema-card__header > button:disabled, +.writer-schema-inline-actions > button:disabled, .writer-schema-actions > button:disabled { cursor: progress; opacity: 0.7; } +.writer-schema-action-icon { + width: 1rem; + height: 1rem; + margin-right: 0.35rem; + vertical-align: -0.15rem; +} + body.theme-dark .writer-schema-eyebrow { color: #89c2d9; } diff --git a/apps/web/writer-schema.html b/apps/web/writer-schema.html index 48e0ed57..56e7ae00 100644 --- a/apps/web/writer-schema.html +++ b/apps/web/writer-schema.html @@ -96,6 +96,10 @@

Progress Log

Full Request

+
+ + +

The exact prompt, shared context, task context, and language options sent to the Writer API. @@ -141,6 +145,26 @@

Full Error

+
+
+

Process AI Response

+
+

+ Paste a response from any AI system here, then create schema rows from the returned JSON without calling the + browser Writer API again. +

+ + +
+ +
+
+

Schema Editor

From 1255002a3bea9759e76ba9a17e97800a77f3fbc6 Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Sun, 14 Jun 2026 13:06:00 +0100 Subject: [PATCH 2/2] Add copy buttons for Chrome flag URLs --- .../src/tests/jest/writer-schema-page.test.js | 33 +++++++++++++ apps/web/src/writer-schema-page.mjs | 46 +++++++++++++++---- apps/web/styles.css | 21 +++++++++ apps/web/writer-schema.html | 33 +++++++++++++ 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/apps/web/src/tests/jest/writer-schema-page.test.js b/apps/web/src/tests/jest/writer-schema-page.test.js index 28a7b7e4..5e1de6af 100644 --- a/apps/web/src/tests/jest/writer-schema-page.test.js +++ b/apps/web/src/tests/jest/writer-schema-page.test.js @@ -20,6 +20,11 @@ describe('writer schema prototype page', () => {
AnyWayData

Checking Writer API availability...

+ @@ -256,6 +261,34 @@ describe('writer schema prototype page', () => { expect(dom.window.document.getElementById('writer-schema-prompt').value).toBe(DEFAULT_PROMPT); }); + test('bootstrap copies setup flag URLs to the clipboard', async () => { + const schemaComponent = { + destroy: jest.fn(), + replaceRows: jest.fn(), + setTextMode: jest.fn(), + render: jest.fn(), + syncTextFromRows: jest.fn(), + validateRows: jest.fn(() => ({ errors: [] })), + getSchemaText: jest.fn(() => ''), + }; + + await bootstrapWriterSchemaPage({ + documentObj: dom.window.document, + WriterCtor: null, + navigatorObj, + createThemeToggleComponentFn: () => ({ destroy: jest.fn() }), + createSharedSchemaDefinitionComponentFn: () => schemaComponent, + }); + + dom.window.document.getElementById('writer-schema-copy-flag').click(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(navigatorObj.clipboard.writeText).toHaveBeenCalledWith('chrome://flags/#writer-api-for-gemini-nano'); + expect(dom.window.document.getElementById('writer-schema-generation-status').textContent).toContain( + 'Copied the Chrome flags URL to the clipboard.' + ); + }); + test('bootstrap generates rows and populates the shared schema component', async () => { const schemaComponent = { destroy: jest.fn(), diff --git a/apps/web/src/writer-schema-page.mjs b/apps/web/src/writer-schema-page.mjs index 8e4412b8..24763e57 100644 --- a/apps/web/src/writer-schema-page.mjs +++ b/apps/web/src/writer-schema-page.mjs @@ -177,6 +177,17 @@ function createBlankRowFactory(prefix = 'writer-schema-row') { }); } +function decorateWriterCopyButton(buttonElement, labelText = 'Copy') { + if (!buttonElement || buttonElement.childElementCount > 0) { + return; + } + + buttonElement.insertAdjacentHTML( + 'afterbegin', + `${renderIconHtml('copy', { className: 'app-icon writer-schema-action-icon' })}${labelText}` + ); +} + function validateSchemaRows(schemaRows) { return validateSharedSchemaRows({ schemaRows, @@ -716,15 +727,18 @@ async function bootstrapWriterSchemaPage({ const copyPromptButton = documentObj.getElementById('writer-schema-copy-prompt'); const createSchemaFromResponseButton = documentObj.getElementById('writer-schema-create-schema'); const schemaRoot = documentObj.getElementById('writer-schema-editor-root'); + const setupFlagCopyButtons = Array.from(documentObj.querySelectorAll('[data-copy-text]')); - copyRequestButton?.insertAdjacentHTML( - 'afterbegin', - `${renderIconHtml('copy', { className: 'app-icon writer-schema-action-icon' })}Copy JSON` - ); - copyPromptButton?.insertAdjacentHTML( - 'afterbegin', - `${renderIconHtml('clipboard-paste', { className: 'app-icon writer-schema-action-icon' })}Copy Prompt` - ); + decorateWriterCopyButton(copyRequestButton, 'Copy JSON'); + if (copyPromptButton && copyPromptButton.childElementCount === 0) { + copyPromptButton.insertAdjacentHTML( + 'afterbegin', + `${renderIconHtml('clipboard-paste', { className: 'app-icon writer-schema-action-icon' })}Copy Prompt` + ); + } + setupFlagCopyButtons.forEach((buttonElement) => { + decorateWriterCopyButton(buttonElement, 'Copy'); + }); const schemaDefinitionProps = createSchemaDefinitionProps(); const schemaComponent = createSharedSchemaDefinitionComponentFn({ @@ -1012,6 +1026,22 @@ async function bootstrapWriterSchemaPage({ createSchemaFromResponseButton?.addEventListener('click', () => { void createSchemaFromResponse().catch(() => {}); }); + setupFlagCopyButtons.forEach((buttonElement) => { + buttonElement.addEventListener('click', () => { + void copyTextToClipboard(buttonElement.getAttribute('data-copy-text') || '', { navigatorObj }) + .then(() => { + setStatus(generationStatusElement, 'Copied the Chrome flags URL to the clipboard.', { + severity: 'info', + }); + }) + .catch((error) => { + const message = error instanceof Error ? error.message : String(error); + setStatus(generationStatusElement, `Unable to copy the Chrome flags URL: ${message}`, { + severity: 'error', + }); + }); + }); + }); return { destroy() { diff --git a/apps/web/styles.css b/apps/web/styles.css index 050ed60f..57e5c08b 100644 --- a/apps/web/styles.css +++ b/apps/web/styles.css @@ -257,6 +257,18 @@ body.theme-dark .dragdropzone { max-width: 75ch; } +.writer-schema-flag-links { + margin: 0.45rem 0 0; + padding-left: 1.25rem; +} + +.writer-schema-flag-links li { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + .writer-schema-layout { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); @@ -347,6 +359,7 @@ body.theme-dark .dragdropzone { } .writer-schema-card__header > button, +.writer-schema-flag-copy-button, .writer-schema-inline-actions > button, .writer-schema-actions > button { border: 0; @@ -360,12 +373,20 @@ body.theme-dark .dragdropzone { } .writer-schema-card__header > button:disabled, +.writer-schema-flag-copy-button:disabled, .writer-schema-inline-actions > button:disabled, .writer-schema-actions > button:disabled { cursor: progress; opacity: 0.7; } +.writer-schema-flag-copy-button { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.35rem 0.75rem; +} + .writer-schema-action-icon { width: 1rem; height: 1rem; diff --git a/apps/web/writer-schema.html b/apps/web/writer-schema.html index 56e7ae00..62407d2b 100644 --- a/apps/web/writer-schema.html +++ b/apps/web/writer-schema.html @@ -31,14 +31,47 @@

Writer API Schema Prototype

  • Enable #optimization-guide-on-device-model. +
  • Enable #prompt-api-for-gemini-nano-multimodal-input. +
  • Enable #writer-api-for-gemini-nano. +
  • Relaunch Chrome, then load this page again.