This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ PUBLIC_NAME=AI Usage Disclosure
|
||||
CONTACT_URL=https://ai.trustednet.eu
|
||||
SALES_URL=https://ai.trustednet.eu/product
|
||||
DEFAULT_LANGUAGE=de
|
||||
BULK_URL=http://localhost:8081
|
||||
BULK_URL=http://localhost:8088
|
||||
|
||||
# Optional second container for batch rendering.
|
||||
BULK_MAX_URLS=500
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## 1.8.1 - 2026-07-23
|
||||
|
||||
- Added an accessible first-exposure embed with visible link text next to the SVG badge instead of relying on SVG/`alt` information alone.
|
||||
- Added `accessibleText` and `embedHtml` to `GET /v1/render`; `embedHtml` combines the visible disclosure and inline JSON-LD in one copy/paste artifact.
|
||||
- The normal generator now renders HTML/Markdown/JSON-LD through the core render endpoint and copies the complete HTML + JSON-LD embed by default.
|
||||
- Bulk HTML exports now use `embedHtml`, so each subject receives visible disclosure markup and JSON-LD in one step.
|
||||
- Flat SVG badges automatically choose black or white text per segment for at least 4.5:1 contrast; the old overlay gradient was removed so the computed contrast remains deterministic.
|
||||
- Emoji/icon badges enforce at least 3:1 foreground/background contrast by falling back to black or white when a custom colour pair is insufficient; the generator shows a corresponding warning.
|
||||
- Increased the recommended linked disclosure target to at least 36 CSS pixels high and added a visible text label in the generator preview.
|
||||
- Adopted the supplied registry deployment: `compose.yaml` now uses `git.send.nrw/sendnrw/ai-disclosure-standard:latest` and `:bulk-latest`, and `.gitea/workflows/registry-bulk.yml` builds the separate bulk tags.
|
||||
- Removed the superseded GHCR bulk workflow to avoid competing release pipelines.
|
||||
|
||||
## 1.8.0 - 2026-07-22
|
||||
|
||||
- Added the optional `ai-disclosure-bulk` container without accounts, database or separate legal decision logic.
|
||||
|
||||
@@ -28,7 +28,7 @@ build-bulk:
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bin/ai-disclosure-bulk ./cmd/bulk
|
||||
|
||||
docker-build:
|
||||
docker build -t ai-disclosure-standard:1.8.0-local .
|
||||
docker build -t ai-disclosure-standard:1.8.1-local .
|
||||
|
||||
docker-build-bulk:
|
||||
docker build -f Dockerfile.bulk -t ai-disclosure-bulk:1.0.0-local .
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# AI Disclosure Standard 1.8.0
|
||||
# AI Disclosure Standard 1.8.1
|
||||
|
||||
Ein zustandsloser Go-Dienst für sichtbare und maschinenlesbare Erklärungen zur KI-Nutzung in Artikeln, Webseiten und einzelnen Inhaltsbestandteilen.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- einzelne Deklarationen und zusammengefasste Artikel-/Webseiten-Deklarationen;
|
||||
- SVG-Badges, HTML-Erklärung und JSON-LD unter einem gemeinsamen Link;
|
||||
- SVG-Badges, sichtbarer barrierearmer HTML-Hinweis und JSON-LD unter einem gemeinsamen Link;
|
||||
- empfohlener Ein-Schritt-Embed: sichtbarer Hinweis + dekoratives SVG + inline JSON-LD in einem HTML-Snippet;
|
||||
- Text, Titelbild, weitere Bilder, Recherche, Übersetzung, Audio, Video und Code getrennt erfassbar;
|
||||
- automatisch erzeugte, professionell formulierte Zusammenfassung als Fließtext und strukturierte Tabelle;
|
||||
- frei wählbare Nachweisgrundlage: Selbsterklärung, technisch protokolliert, signiert oder verifiziert;
|
||||
@@ -37,7 +38,7 @@ Datenschutz: http://localhost:8080/datenschutz
|
||||
Barrierefrei: http://localhost:8080/barrierefreiheit
|
||||
Healthcheck: http://localhost:8080/healthz
|
||||
Funktionen: http://localhost:8080/v1/capabilities
|
||||
Bulk: http://localhost:8081/
|
||||
Bulk (Compose-Port): http://localhost:8088/
|
||||
```
|
||||
|
||||
Go lädt `.env` nicht selbst. Unter PowerShell kann die Datei vor dem Start in die Prozessumgebung übernommen werden oder Docker Compose mit `--env-file .env` verwendet werden.
|
||||
@@ -46,9 +47,23 @@ Go lädt `.env` nicht selbst. Unter PowerShell kann die Datei vor dem Start in d
|
||||
|
||||
```powershell
|
||||
Copy-Item .env.example .env
|
||||
docker compose --env-file .env up -d --build
|
||||
docker compose --env-file .env pull
|
||||
docker compose --env-file .env up -d
|
||||
```
|
||||
|
||||
Die bereitgestellte `compose.yaml` verwendet die Registry-Images `git.send.nrw/sendnrw/ai-disclosure-standard:latest` und `:bulk-latest`, bindet beide Dienste an das externe Netzwerk `proxy` und veröffentlicht den Bulk-Dienst zusätzlich auf Host-Port `8088`. Das Netzwerk `proxy` muss vor dem Start bereits existieren.
|
||||
|
||||
|
||||
## Gitea Actions: Bulk-Image
|
||||
|
||||
Der Workflow `.gitea/workflows/registry-bulk.yml` baut `Dockerfile.bulk` unabhängig vom Hauptimage und veröffentlicht in der konfigurierten Registry unter anderem:
|
||||
|
||||
```text
|
||||
git.send.nrw/sendnrw/<repository>:bulk-latest
|
||||
git.send.nrw/sendnrw/<repository>:bulk-<version>
|
||||
```
|
||||
|
||||
Er verwendet dieselbe Registry- und Secret-Konfiguration wie der vorhandene Hauptimage-Workflow (`DOCKER_USERNAME` / `DOCKER_PASSWORD`).
|
||||
|
||||
## Bulk-Generator
|
||||
|
||||
@@ -60,7 +75,9 @@ Typischer Ablauf:
|
||||
2. **Als Bulk-Vorlage öffnen** auswählen.
|
||||
3. Optional ein Website-Profil mit wiederkehrenden Angaben wie Autor, Impressums-/Verantwortlichkeits-URL und Beschwerdestelle im Browser speichern.
|
||||
4. Absolute URLs oder relative Pfade einfügen. Für relative Pfade kann einmalig eine Basis-URL gesetzt werden.
|
||||
5. HTML, Markdown, JSON-LD, JSONL oder CSV erzeugen und kopieren bzw. herunterladen.
|
||||
5. Das komplette HTML (sichtbarer Hinweis + JSON-LD), Markdown, separates JSON-LD, JSONL oder CSV erzeugen und kopieren bzw. herunterladen.
|
||||
|
||||
Der HTML-Export verwendet standardmäßig `embedHtml` aus dem Core: Das SVG ist dekorativ (`alt=""`), der Link enthält unmittelbar sichtbaren Text und hat ein größeres Klickziel; direkt danach folgt das JSON-LD als `<script type="application/ld+json">`.
|
||||
|
||||
Der Bulk-Container ruft die eingegebenen Inhalts-URLs **nicht** ab. Website-Profile und gespeicherte Kennzeichnungsvorlagen werden ausschließlich im `localStorage` des Browsers gespeichert. URL-Listen und erzeugte Ergebnisse werden weder im Browser dauerhaft gespeichert noch serverseitig persistiert. Profile und Vorlagen können als JSON exportiert, importiert oder vollständig gelöscht werden.
|
||||
|
||||
@@ -68,7 +85,7 @@ Relevante Variablen:
|
||||
|
||||
| Variable | Standard | Bedeutung |
|
||||
|---|---|---|
|
||||
| `BULK_URL` | leer / in der Beispielkonfiguration `http://localhost:8081` | öffentliche URL des Bulk-Generators; aktiviert den Übergabe-Button im normalen Generator |
|
||||
| `BULK_URL` | leer / in `.env.example` `http://localhost:8088` | öffentliche URL des Bulk-Generators; aktiviert den Übergabe-Button im normalen Generator |
|
||||
| `CORE_INTERNAL_URL` | `http://app:8080` | feste interne Origin des Disclosure-Core; der Bulk-Dienst folgt keinen frei eingegebenen Ziel-URLs |
|
||||
| `DISCLOSURE_BASE_URL` | `http://localhost:8080` | öffentliche Core-Origin für Links in der Bulk-Oberfläche |
|
||||
| `GENERATOR_URL` | wie `DISCLOSURE_BASE_URL` | öffentlicher Link zurück zum normalen Generator |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -300,7 +300,7 @@
|
||||
|
||||
function buildExports(results) {
|
||||
const successful = results.filter(item => item.ok && item.data);
|
||||
const html = successful.map(item => `<!-- ${item.subject} -->\n${item.data.html}`).join('\n\n');
|
||||
const html = successful.map(item => `<!-- ${item.subject} -->\n${item.data.embedHtml || item.data.html}`).join('\n\n');
|
||||
const markdown = successful.map(item => `### ${item.subject}\n\n${item.data.markdown}`).join('\n\n');
|
||||
const jsonArray = successful.map(item => item.data.jsonLd);
|
||||
const json = JSON.stringify(jsonArray, null, 2);
|
||||
|
||||
@@ -201,8 +201,8 @@
|
||||
</div>
|
||||
|
||||
<div class="toolbar wrap export-buttons">
|
||||
<button type="button" data-copy="html">HTML kopieren</button>
|
||||
<button type="button" class="button secondary" data-download="html">HTML herunterladen</button>
|
||||
<button type="button" data-copy="html">Komplettes HTML kopieren</button>
|
||||
<button type="button" class="button secondary" data-download="html">HTML + JSON-LD herunterladen</button>
|
||||
<button type="button" data-copy="markdown">Markdown kopieren</button>
|
||||
<button type="button" class="button secondary" data-download="markdown">Markdown herunterladen</button>
|
||||
<button type="button" data-copy="json">JSON-LD kopieren</button>
|
||||
@@ -211,6 +211,8 @@
|
||||
<button type="button" class="button secondary" data-download="csv">CSV herunterladen</button>
|
||||
</div>
|
||||
|
||||
<p class="embed-note">Der HTML-Export ist die empfohlene Ein-Schritt-Integration: sichtbarer, tastaturerreichbarer Hinweis mit Text neben dem Badge plus eingebettetes JSON-LD. Das SVG ist dabei dekorativ; die zugängliche Bezeichnung stammt aus dem sichtbaren Linktext.</p>
|
||||
|
||||
<div class="result-table-wrap">
|
||||
<table class="result-table">
|
||||
<thead><tr><th>Inhalt</th><th>Status</th><th>Links</th></tr></thead>
|
||||
@@ -219,7 +221,7 @@
|
||||
</div>
|
||||
|
||||
<div class="output-grid">
|
||||
<label>HTML<textarea id="output-html" rows="12" readonly></textarea></label>
|
||||
<label>Komplettes HTML inkl. JSON-LD<textarea id="output-html" rows="12" readonly></textarea></label>
|
||||
<label>Markdown<textarea id="output-markdown" rows="12" readonly></textarea></label>
|
||||
<label>JSON-LD<textarea id="output-json" rows="12" readonly></textarea></label>
|
||||
<label>CSV<textarea id="output-csv" rows="8" readonly></textarea></label>
|
||||
|
||||
+24
-19
@@ -1,15 +1,9 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: ai-disclosure-standard:1.8.0-local
|
||||
image: git.send.nrw/sendnrw/ai-disclosure-standard:latest
|
||||
container_name: ai
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
BULK_URL: ${BULK_URL:-http://localhost:8081}
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- license-cache:/data
|
||||
read_only: true
|
||||
@@ -21,26 +15,30 @@ services:
|
||||
- ALL
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "/ai-disclosure", "--healthcheck"]
|
||||
test:
|
||||
- CMD
|
||||
- /ai-disclosure
|
||||
- --healthcheck
|
||||
interval: 15s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
|
||||
networks:
|
||||
- proxy
|
||||
bulk:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.bulk
|
||||
image: ai-disclosure-bulk:1.0.0-local
|
||||
image: git.send.nrw/sendnrw/ai-disclosure-standard:bulk-latest
|
||||
container_name: ai-bulk
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
CORE_INTERNAL_URL: http://app:8080
|
||||
DISCLOSURE_BASE_URL: ${BASE_URL:-http://localhost:8080}
|
||||
GENERATOR_URL: ${BASE_URL:-http://localhost:8080}
|
||||
BULK_PUBLIC_NAME: AI Disclosure Bulk
|
||||
BULK_PUBLIC_NAME: ${PUBLIC_NAME:-AI Disclosure Bulk}
|
||||
BULK_MAX_URLS: ${BULK_MAX_URLS:-500}
|
||||
BULK_WORKERS: ${BULK_WORKERS:-4}
|
||||
ports:
|
||||
- "8081:8081"
|
||||
- 8088:8081
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:size=8m,mode=1777
|
||||
@@ -53,11 +51,18 @@ services:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "/ai-disclosure-bulk", "--healthcheck"]
|
||||
test:
|
||||
- CMD
|
||||
- /ai-disclosure-bulk
|
||||
- --healthcheck
|
||||
interval: 15s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
|
||||
networks:
|
||||
- proxy
|
||||
volumes:
|
||||
license-cache:
|
||||
license-cache: null
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
|
||||
@@ -25,7 +25,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: app
|
||||
image: ghcr.io/REPLACE_ME/ai-disclosure-standard:1.8.0
|
||||
image: ghcr.io/REPLACE_ME/ai-disclosure-standard:1.8.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/REPLACE_ME/ai-disclosure-standard:1.8.0
|
||||
image: ghcr.io/REPLACE_ME/ai-disclosure-standard:1.8.1
|
||||
environment:
|
||||
BASE_URL: https://ai.example.org
|
||||
PUBLIC_NAME: AI Usage Disclosure
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Mehrsprachige Hintergrundseite
|
||||
|
||||
Version 1.8.0 stellt unter `/background` eine eigenständige Informationsseite zur KI-Kennzeichnung und zu Artikel 50 des EU AI Act bereit.
|
||||
Version 1.8.1 stellt unter `/background` eine eigenständige Informationsseite zur KI-Kennzeichnung und zu Artikel 50 des EU AI Act bereit.
|
||||
|
||||
## Routen
|
||||
|
||||
|
||||
@@ -42,10 +42,12 @@ Für solche sichtbaren Offenlegungen gilt nach Artikel 50 Absatz 5 und den Leitl
|
||||
|
||||
Daher gilt für die Einbettung dieses Projekts:
|
||||
|
||||
- Badge oder Klartext unmittelbar am betroffenen Inhalt platzieren;
|
||||
- den empfohlenen `embedHtml`-Baustein oder eine gleichwertige Umsetzung unmittelbar am betroffenen Inhalt platzieren;
|
||||
- nicht ausschließlich im Footer, Impressum oder auf einer erst später erreichbaren Unterseite;
|
||||
- aussagekräftigen Alternativtext verwenden;
|
||||
- bei der quadratischen Emoji-Variante zusätzlich einen unmittelbar verständlichen Text oder eine gleichwertig eindeutige sichtbare Kennzeichnung verwenden.
|
||||
- sichtbaren, verständlichen Text als Bestandteil des Links bereitstellen, damit die Offenlegung nicht nur über eine Grafik wahrnehmbar ist;
|
||||
- das Badge im empfohlenen Embed mit `alt=""`/`aria-hidden="true"` dekorativ behandeln, weil derselbe Bedeutungsgehalt bereits als sichtbarer Linktext vorhanden ist und sonst doppelt angesagt würde;
|
||||
- bei einer alleinstehenden Badge-Grafik weiterhin einen aussagekräftigen Alternativtext bereitstellen;
|
||||
- ausreichende Kontraste und ein ausreichend großes, tastaturerreichbares Linkziel sicherstellen. Der mitgelieferte Renderer wählt bei flachen Badges automatisch Schwarz/Weiß für mindestens 4,5:1 Textkontrast und korrigiert Emoji-/Icon-Farben unter 3:1.
|
||||
|
||||
Offizielle Quellen:
|
||||
|
||||
@@ -107,7 +109,7 @@ Anbieter generativer KI-Systeme müssen in den erfassten Fällen dafür sorgen,
|
||||
|
||||
Artikel 50 Absatz 2 nimmt Systeme aus, die lediglich Standard-Bearbeitungsfunktionen ausführen oder die vom Betreiber bereitgestellten Eingabedaten bzw. deren Semantik nicht wesentlich verändern. Ob diese Ausnahme tatsächlich greift, hängt von der konkreten Funktion und Veränderungswirkung ab; der sichtbare Generator kann diese technische Anbieterfrage nicht automatisch entscheiden.
|
||||
|
||||
Das von diesem Projekt erzeugte **JSON-LD ist ergänzende Dokumentation**. Es ist nicht als Ersatz für die technische Markierung gedacht, die der Anbieter des generativen KI-Systems in oder an der Ausgabe implementieren muss.
|
||||
Das von diesem Projekt erzeugte **JSON-LD ist ergänzende Dokumentation**. Es ist nicht als Ersatz für die technische Markierung gedacht, die der Anbieter des generativen KI-Systems in oder an der Ausgabe implementieren muss. Der Endpunkt `/v1/render` kann es über `embedHtml` direkt zusammen mit der sichtbaren Offenlegung als `<script type="application/ld+json">` ausgeben; dadurch lässt sich die menschlich sichtbare und die ergänzende strukturierte Information in einem Integrationsschritt einbauen, ohne beide Pflichten begrifflich gleichzusetzen.
|
||||
|
||||
Wer selbst Anbieter eines generativen KI-Systems im Sinne des AI Act ist, muss deshalb zusätzlich eine geeignete Provider-Marking-Lösung umsetzen und dokumentieren. Der von der Kommission positiv bewertete Code of Practice kann hierfür als freiwilliger Compliance-Rahmen genutzt werden.
|
||||
|
||||
|
||||
@@ -71,7 +71,8 @@ Für ein erfasstes Verbraucherangebot sind unter anderem zu prüfen:
|
||||
- Kontraste, Zoom und Reflow;
|
||||
- verständliche Fehlermeldungen;
|
||||
- Screenreader-Ausgabe;
|
||||
- Alternativtexte eingebetteter Badges;
|
||||
- sichtbarer Text neben eingebetteten Badges; bei dekorativen Badges im empfohlenen Embed `alt=""`, bei alleinstehenden Badge-Grafiken ein aussagekräftiger Alternativtext;
|
||||
- Link-/Klickzielgröße und sichtbare Tastaturfokussierung;
|
||||
- Barrierefreiheit des vollständigen Bestell- oder Vertragspfads;
|
||||
- gesetzlich verlangte Informationen zur Barrierefreiheit.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Initialization is performed in `internal/app/server.go`. The product ID and embe
|
||||
```go
|
||||
licenses := licenseclient.New(ctx, licenseclient.Config{
|
||||
Product: "ai-disclosure-standard",
|
||||
ClientVersion: "1.8.0",
|
||||
ClientVersion: "1.8.1",
|
||||
Token: cfg.LicenseToken,
|
||||
TrustStore: trustStore,
|
||||
BaseURL: cfg.BaseURL,
|
||||
|
||||
@@ -119,7 +119,7 @@ Anfrage:
|
||||
"baseUrl": "https://ai.example.org",
|
||||
"host": "ai.example.org",
|
||||
"instanceId": "production-eu-1",
|
||||
"clientVersion": "1.8.0"
|
||||
"clientVersion": "1.8.1"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
const (
|
||||
ProductID = "ai-disclosure-standard"
|
||||
ProductVersion = "1.8.0"
|
||||
ProductVersion = "1.8.1"
|
||||
FeatureCustomText = "custom_text"
|
||||
FeatureCustomBadge = "custom_badge"
|
||||
FeatureWhiteLabel = "white_label"
|
||||
|
||||
+23
-9
@@ -467,7 +467,9 @@ type renderedArtifacts struct {
|
||||
DeclarationURL string `json:"declarationUrl"`
|
||||
BadgeURL string `json:"badgeUrl"`
|
||||
ManifestURL string `json:"manifestUrl"`
|
||||
AccessibleText string `json:"accessibleText"`
|
||||
HTML string `json:"html"`
|
||||
EmbedHTML string `json:"embedHtml"`
|
||||
Markdown string `json:"markdown"`
|
||||
JSONLD declaration.Declaration `json:"jsonLd"`
|
||||
}
|
||||
@@ -499,20 +501,32 @@ func (s *Server) handleRender(w http.ResponseWriter, r *http.Request) {
|
||||
badgeURL := s.cfg.BaseURL + "/v1/badge.svg?" + badgeQ.Encode()
|
||||
|
||||
assessment := assessLegalContext(d, locale)
|
||||
alt := renderBadgeAlt(q, d, locale, assessment)
|
||||
imageMarkup := fmt.Sprintf(`<a href="%s"><img src="%s" alt="%s"></a>`, html.EscapeString(declarationURL), html.EscapeString(badgeURL), html.EscapeString(alt))
|
||||
htmlMarkup := imageMarkup
|
||||
markdown := fmt.Sprintf(`[](%s)`, markdownAlt(alt), badgeURL, declarationURL)
|
||||
if assessment.RequiresDisclosure && theme == "emoji" {
|
||||
htmlMarkup += " <span>" + html.EscapeString(assessment.BadgeMessage) + "</span>"
|
||||
markdown += " **" + markdownText(assessment.BadgeMessage) + "**"
|
||||
badgeText := renderBadgeAlt(q, d, locale, assessment)
|
||||
detailsText := strings.TrimSpace(locale.Text["embed_details"])
|
||||
if detailsText == "" {
|
||||
detailsText = "AI usage details"
|
||||
}
|
||||
accessibleText := badgeText + " – " + detailsText
|
||||
badgeHeight := 20
|
||||
if theme == "emoji" {
|
||||
badgeHeight = 48
|
||||
}
|
||||
htmlMarkup := fmt.Sprintf(`<p class="ai-disclosure" data-ai-disclosure><a class="ai-disclosure__link" href="%s" style="display:inline-flex;align-items:center;gap:.5rem;min-height:36px;padding:.25rem .375rem;max-width:100%%;color:inherit;text-decoration:underline;text-underline-offset:.15em"><img class="ai-disclosure__badge" src="%s" alt="" aria-hidden="true" style="height:%dpx;width:auto;max-width:100%%;flex:0 0 auto"><span class="ai-disclosure__text" style="line-height:1.35">%s</span></a></p>`,
|
||||
html.EscapeString(declarationURL), html.EscapeString(badgeURL), badgeHeight, html.EscapeString(accessibleText))
|
||||
markdown := fmt.Sprintf(`[ %s](%s)`, badgeURL, markdownText(accessibleText), declarationURL)
|
||||
jsonLDBytes, err := json.MarshalIndent(d, "", " ")
|
||||
if err != nil {
|
||||
s.logger.Error("could not marshal render JSON-LD", "error", err)
|
||||
s.problem(w, http.StatusInternalServerError, "render_failed", "Could not render JSON-LD.")
|
||||
return
|
||||
}
|
||||
embedMarkup := htmlMarkup + "\n" + `<script type="application/ld+json">` + "\n" + string(jsonLDBytes) + "\n</script>"
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_ = json.NewEncoder(w).Encode(renderedArtifacts{
|
||||
Subject: d.Subject, DeclarationURL: declarationURL, BadgeURL: badgeURL, ManifestURL: manifestURL,
|
||||
HTML: htmlMarkup, Markdown: markdown, JSONLD: d,
|
||||
Subject: d.Subject, DeclarationURL: declarationURL, BadgeURL: badgeURL, ManifestURL: manifestURL, AccessibleText: accessibleText,
|
||||
HTML: htmlMarkup, EmbedHTML: embedMarkup, Markdown: markdown, JSONLD: d,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -699,7 +699,9 @@ func TestRenderEndpointReturnsReusableArtifacts(t *testing.T) {
|
||||
DeclarationURL string `json:"declarationUrl"`
|
||||
BadgeURL string `json:"badgeUrl"`
|
||||
ManifestURL string `json:"manifestUrl"`
|
||||
AccessibleText string `json:"accessibleText"`
|
||||
HTML string `json:"html"`
|
||||
EmbedHTML string `json:"embedHtml"`
|
||||
Markdown string `json:"markdown"`
|
||||
JSONLD map[string]any `json:"jsonLd"`
|
||||
}
|
||||
@@ -709,7 +711,7 @@ func TestRenderEndpointReturnsReusableArtifacts(t *testing.T) {
|
||||
if payload.Subject != "https://content.example/article" {
|
||||
t.Fatalf("subject %q", payload.Subject)
|
||||
}
|
||||
for name, value := range map[string]string{"declaration": payload.DeclarationURL, "badge": payload.BadgeURL, "manifest": payload.ManifestURL, "html": payload.HTML, "markdown": payload.Markdown} {
|
||||
for name, value := range map[string]string{"declaration": payload.DeclarationURL, "badge": payload.BadgeURL, "manifest": payload.ManifestURL, "accessibleText": payload.AccessibleText, "html": payload.HTML, "embedHtml": payload.EmbedHTML, "markdown": payload.Markdown} {
|
||||
if value == "" {
|
||||
t.Fatalf("%s is empty", name)
|
||||
}
|
||||
@@ -723,6 +725,12 @@ func TestRenderEndpointReturnsReusableArtifacts(t *testing.T) {
|
||||
if !strings.Contains(payload.BadgeURL, "theme=mono") {
|
||||
t.Fatalf("badge URL missing theme: %s", payload.BadgeURL)
|
||||
}
|
||||
if !strings.Contains(payload.HTML, `alt="" aria-hidden="true"`) || !strings.Contains(payload.HTML, "Details zur KI-Nutzung") {
|
||||
t.Fatalf("accessible visible disclosure missing: %s", payload.HTML)
|
||||
}
|
||||
if !strings.Contains(payload.EmbedHTML, `<script type="application/ld+json">`) || !strings.Contains(payload.EmbedHTML, `https://content.example/article`) {
|
||||
t.Fatalf("JSON-LD embed missing: %s", payload.EmbedHTML)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratorExposesBulkHandoffWhenConfigured(t *testing.T) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
@@ -69,15 +71,17 @@ func Render(o Options) ([]byte, string) {
|
||||
if isHexColor(o.RightColor) {
|
||||
rightColor = o.RightColor
|
||||
}
|
||||
leftText := bestTextColor(leftColor)
|
||||
rightText := bestTextColor(rightColor)
|
||||
|
||||
title := html.EscapeString(label + ": " + message)
|
||||
labelEsc := html.EscapeString(label)
|
||||
messageEsc := html.EscapeString(message)
|
||||
openLink, closeLink := svgLink(o.Link)
|
||||
|
||||
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="%s" width="%d" height="20" viewBox="0 0 %d 20"><title>%s</title>%s<linearGradient id="s" x2="0" y2="100%%"><stop offset="0" stop-color="#fff" stop-opacity=".12"/><stop offset="1" stop-opacity=".12"/></linearGradient><clipPath id="r"><rect width="%d" height="20" rx="%d" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="%d" height="20" fill="%s"/><rect x="%d" width="%d" height="20" fill="%s"/><rect width="%d" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"><text x="%d" y="15" fill="#010101" fill-opacity=".3">%s</text><text x="%d" y="14">%s</text><text x="%d" y="15" fill="#010101" fill-opacity=".3">%s</text><text x="%d" y="14">%s</text></g>%s</svg>`,
|
||||
title, total, total, title, openLink, total, radius, leftWidth, leftColor, leftWidth, rightWidth, rightColor, total,
|
||||
leftWidth/2, labelEsc, leftWidth/2, labelEsc, leftWidth+rightWidth/2, messageEsc, leftWidth+rightWidth/2, messageEsc, closeLink)
|
||||
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="%s" focusable="false" width="%d" height="20" viewBox="0 0 %d 20"><title>%s</title>%s<clipPath id="r"><rect width="%d" height="20" rx="%d" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="%d" height="20" fill="%s"/><rect x="%d" width="%d" height="20" fill="%s"/></g><g text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" font-weight="700"><text x="%d" y="14" fill="%s">%s</text><text x="%d" y="14" fill="%s">%s</text></g>%s</svg>`,
|
||||
title, total, total, title, openLink, total, radius, leftWidth, leftColor, leftWidth, rightWidth, rightColor,
|
||||
leftWidth/2, leftText, labelEsc, leftWidth+rightWidth/2, rightText, messageEsc, closeLink)
|
||||
return withETag([]byte(svg))
|
||||
}
|
||||
|
||||
@@ -94,11 +98,17 @@ func renderEmoji(o Options, label, message string) ([]byte, string) {
|
||||
background = "#4a5568"
|
||||
}
|
||||
foreground := "#ffffff"
|
||||
if isHexColor(o.RightColor) {
|
||||
background = o.RightColor
|
||||
}
|
||||
if isHexColor(o.LeftColor) {
|
||||
foreground = o.LeftColor
|
||||
}
|
||||
if isHexColor(o.RightColor) {
|
||||
background = o.RightColor
|
||||
// Emoji badges use the left colour as icon foreground and the right colour
|
||||
// as background. Keep at least WCAG 2.2's 3:1 non-text contrast; if a
|
||||
// custom pair falls below it, prefer the higher-contrast black/white icon.
|
||||
if contrastRatio(foreground, background) < 3.0 {
|
||||
foreground = bestTextColor(background)
|
||||
}
|
||||
|
||||
title := html.EscapeString(label + ": " + message)
|
||||
@@ -188,6 +198,45 @@ func truncate(s string, max int) string {
|
||||
return string(r[:max-1]) + "…"
|
||||
}
|
||||
|
||||
func bestTextColor(background string) string {
|
||||
if contrastRatio("#000000", background) >= contrastRatio("#ffffff", background) {
|
||||
return "#000000"
|
||||
}
|
||||
return "#ffffff"
|
||||
}
|
||||
|
||||
func contrastRatio(foreground, background string) float64 {
|
||||
fg, okFG := relativeLuminance(foreground)
|
||||
bg, okBG := relativeLuminance(background)
|
||||
if !okFG || !okBG {
|
||||
return 1
|
||||
}
|
||||
if fg < bg {
|
||||
fg, bg = bg, fg
|
||||
}
|
||||
return (fg + 0.05) / (bg + 0.05)
|
||||
}
|
||||
|
||||
func relativeLuminance(hex string) (float64, bool) {
|
||||
if !isHexColor(hex) {
|
||||
return 0, false
|
||||
}
|
||||
channels := make([]float64, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
v, err := strconv.ParseUint(hex[1+i*2:3+i*2], 16, 8)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
c := float64(v) / 255.0
|
||||
if c <= 0.04045 {
|
||||
channels[i] = c / 12.92
|
||||
} else {
|
||||
channels[i] = math.Pow((c+0.055)/1.055, 2.4)
|
||||
}
|
||||
}
|
||||
return 0.2126*channels[0] + 0.7152*channels[1] + 0.0722*channels[2], true
|
||||
}
|
||||
|
||||
func isHexColor(s string) bool {
|
||||
if len(s) != 7 || s[0] != '#' {
|
||||
return false
|
||||
|
||||
@@ -92,3 +92,34 @@ func TestEmojiIconMapping(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlatBadgeAutomaticallyChoosesContrastingText(t *testing.T) {
|
||||
data, _ := Render(Options{Label: "KI-Nutzung", Message: "Teilweise", Extent: "partial", Theme: "color"})
|
||||
body := string(data)
|
||||
if !strings.Contains(body, `fill="#b7791f"`) {
|
||||
t.Fatalf("expected partial background: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `fill="#000000">Teilweise</text>`) {
|
||||
t.Fatalf("expected automatic black text on gold background: %s", body)
|
||||
}
|
||||
if ratio := contrastRatio("#000000", "#b7791f"); ratio < 4.5 {
|
||||
t.Fatalf("contrast ratio %.2f is below 4.5", ratio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmojiBadgeAdjustsInsufficientCustomForegroundContrast(t *testing.T) {
|
||||
data, _ := Render(Options{
|
||||
Label: "KI-Nutzung", Message: "Hinweis", Extent: "assisted", Theme: "emoji",
|
||||
LeftColor: "#ffffff", RightColor: "#abcdef",
|
||||
})
|
||||
body := string(data)
|
||||
if strings.Contains(body, `stroke="#ffffff"`) {
|
||||
t.Fatalf("low-contrast custom foreground was not adjusted: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `stroke="#000000"`) {
|
||||
t.Fatalf("expected automatic black icon foreground: %s", body)
|
||||
}
|
||||
if ratio := contrastRatio("#000000", "#abcdef"); ratio < 3 {
|
||||
t.Fatalf("adjusted contrast ratio %.2f is below 3", ratio)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
bulkweb "github.com/b1tsblog/ai-disclosure-standard/bulkweb"
|
||||
)
|
||||
|
||||
const Version = "1.0.0"
|
||||
const Version = "1.0.1"
|
||||
|
||||
type Server struct {
|
||||
cfg Config
|
||||
|
||||
@@ -144,6 +144,54 @@ func init() {
|
||||
applyAssuranceTranslations()
|
||||
applyThemeTranslations()
|
||||
applyLegalGeneratorTranslations()
|
||||
applyEmbedTranslations()
|
||||
}
|
||||
|
||||
func applyEmbedTranslations() {
|
||||
translations := map[string]map[string]string{
|
||||
"de": {
|
||||
"embed_details": "Details zur KI-Nutzung",
|
||||
"embed_html_label": "Komplettes HTML inkl. JSON-LD",
|
||||
"copy_html": "Komplettes HTML kopieren",
|
||||
"embed_markdown_label": "Barrierearmes Markdown",
|
||||
"embed_json_label": "JSON-LD",
|
||||
"embed_rendering": "Einbettung wird erzeugt …",
|
||||
"embed_render_error": "Einbettung konnte nicht erzeugt werden.",
|
||||
"contrast_adjusted": "Der Kontrast der gewählten Emoji-Farben ist zu gering; das Symbol wird automatisch mit Schwarz oder Weiß dargestellt.",
|
||||
},
|
||||
"en": {
|
||||
"embed_details": "AI usage details",
|
||||
"embed_html_label": "Complete HTML incl. JSON-LD",
|
||||
"copy_html": "Copy complete HTML",
|
||||
"embed_markdown_label": "Accessible Markdown",
|
||||
"embed_json_label": "JSON-LD",
|
||||
"embed_rendering": "Rendering embed …",
|
||||
"embed_render_error": "The embed could not be rendered.",
|
||||
"contrast_adjusted": "The selected emoji colours have insufficient contrast; the icon will automatically use black or white.",
|
||||
},
|
||||
"fr": {"embed_details": "Détails sur l’utilisation de l’IA", "embed_html_label": "HTML complet avec JSON-LD", "embed_markdown_label": "Markdown accessible", "embed_json_label": "JSON-LD"},
|
||||
"es": {"embed_details": "Detalles sobre el uso de IA", "embed_html_label": "HTML completo con JSON-LD", "embed_markdown_label": "Markdown accesible", "embed_json_label": "JSON-LD"},
|
||||
"it": {"embed_details": "Dettagli sull’uso dell’IA", "embed_html_label": "HTML completo con JSON-LD", "embed_markdown_label": "Markdown accessibile", "embed_json_label": "JSON-LD"},
|
||||
"nl": {"embed_details": "Details over AI-gebruik", "embed_html_label": "Volledige HTML met JSON-LD", "embed_markdown_label": "Toegankelijke Markdown", "embed_json_label": "JSON-LD"},
|
||||
"pt": {"embed_details": "Detalhes sobre a utilização de IA", "embed_html_label": "HTML completo com JSON-LD", "embed_markdown_label": "Markdown acessível", "embed_json_label": "JSON-LD"},
|
||||
"pl": {"embed_details": "Szczegóły użycia AI", "embed_html_label": "Pełny HTML z JSON-LD", "embed_markdown_label": "Dostępny Markdown", "embed_json_label": "JSON-LD"},
|
||||
}
|
||||
for code, values := range translations {
|
||||
l := catalogs[code]
|
||||
for key, value := range values {
|
||||
l.Text[key] = value
|
||||
}
|
||||
if l.Text["embed_rendering"] == "" {
|
||||
l.Text["embed_rendering"] = translations["en"]["embed_rendering"]
|
||||
}
|
||||
if l.Text["embed_render_error"] == "" {
|
||||
l.Text["embed_render_error"] = translations["en"]["embed_render_error"]
|
||||
}
|
||||
if l.Text["contrast_adjusted"] == "" {
|
||||
l.Text["contrast_adjusted"] = translations["en"]["contrast_adjusted"]
|
||||
}
|
||||
catalogs[code] = l
|
||||
}
|
||||
}
|
||||
|
||||
func applyLegalGeneratorTranslations() {
|
||||
|
||||
@@ -183,13 +183,13 @@ func Build(lang, productName, baseURL, salesURL, contactURL string, prices Price
|
||||
# Adjust BASE_URL and PUBLIC_NAME in .env
|
||||
docker compose up -d --build
|
||||
curl -fsS http://localhost:8080/readyz`},
|
||||
{ID: "docker", Title: copy.InstallTitles[1], Summary: copy.InstallSummaries[1], Code: `docker build -t ai-disclosure-standard:1.8.0 .
|
||||
{ID: "docker", Title: copy.InstallTitles[1], Summary: copy.InstallSummaries[1], Code: `docker build -t ai-disclosure-standard:1.8.1 .
|
||||
docker run -d --name ai-disclosure \
|
||||
-p 8080:8080 \
|
||||
-e BASE_URL=https://ai.example.org \
|
||||
-e PUBLIC_NAME="AI Usage Disclosure" \
|
||||
--read-only --tmpfs /tmp \
|
||||
ai-disclosure-standard:1.8.0`},
|
||||
ai-disclosure-standard:1.8.1`},
|
||||
{ID: "kubernetes", Title: copy.InstallTitles[2], Summary: copy.InstallSummaries[2], Code: `# Image, Domain und TLS-Secret in deploy/kubernetes.yaml ersetzen
|
||||
kubectl create secret generic ai-disclosure-license \
|
||||
--from-literal=token='...'
|
||||
|
||||
+6
-4
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: AI Usage Disclosure API
|
||||
version: 1.8.0
|
||||
version: 1.8.1
|
||||
description: Stateless multilingual badge, declaration and validation API with optional licensed presentation capabilities.
|
||||
servers:
|
||||
- url: https://ai.example.org
|
||||
@@ -132,7 +132,7 @@ paths:
|
||||
/v1/render:
|
||||
get:
|
||||
summary: Render reusable embedding artifacts from declaration query parameters
|
||||
description: Returns HTML, Markdown and the parsed JSON-LD declaration from the same core logic used by the normal generator. The optional bulk container calls this endpoint for each subject URL.
|
||||
description: Returns accessible visible HTML, a one-step HTML embed with inline JSON-LD, Markdown and the parsed JSON-LD declaration from the same core logic used by the normal generator. The optional bulk container calls this endpoint for each subject URL.
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/preset'}
|
||||
- {$ref: '#/components/parameters/extent'}
|
||||
@@ -172,13 +172,15 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [declarationUrl, badgeUrl, manifestUrl, html, markdown, jsonLd]
|
||||
required: [declarationUrl, badgeUrl, manifestUrl, accessibleText, html, embedHtml, markdown, jsonLd]
|
||||
properties:
|
||||
subject: {type: string, format: uri}
|
||||
declarationUrl: {type: string, format: uri}
|
||||
badgeUrl: {type: string, format: uri}
|
||||
manifestUrl: {type: string, format: uri}
|
||||
html: {type: string}
|
||||
accessibleText: {type: string}
|
||||
html: {type: string, description: Visible accessible disclosure markup without JSON-LD}
|
||||
embedHtml: {type: string, description: Recommended one-step embed containing the visible disclosure and inline JSON-LD}
|
||||
markdown: {type: string}
|
||||
jsonLd: {$ref: './schema/declaration.schema.json'}
|
||||
"403": {$ref: '#/components/responses/ProRequired'}
|
||||
|
||||
+82
-11
@@ -6,6 +6,8 @@
|
||||
if (!form) return;
|
||||
const $ = id => document.getElementById(id);
|
||||
const ARTICLE_50_START = '2026-08-02';
|
||||
let renderSequence = 0;
|
||||
let renderTimer = 0;
|
||||
const presets = {
|
||||
// Human/editorial review is a factual workflow statement and must never be inferred.
|
||||
'no-ai': {extent:'none', activities:'', review:'none'},
|
||||
@@ -33,6 +35,80 @@
|
||||
el.querySelectorAll?.('input,select,textarea').forEach(input => { input.disabled = hidden; });
|
||||
}
|
||||
|
||||
function hexRGB(value) {
|
||||
if (!/^#[0-9a-f]{6}$/i.test(value || '')) return null;
|
||||
return [1, 3, 5].map(i => parseInt(value.slice(i, i + 2), 16) / 255);
|
||||
}
|
||||
|
||||
function relativeLuminance(value) {
|
||||
const rgb = hexRGB(value);
|
||||
if (!rgb) return null;
|
||||
const channels = rgb.map(c => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
||||
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
|
||||
}
|
||||
|
||||
function contrastRatio(foreground, background) {
|
||||
const a = relativeLuminance(foreground);
|
||||
const b = relativeLuminance(background);
|
||||
if (a == null || b == null) return null;
|
||||
const lighter = Math.max(a, b);
|
||||
const darker = Math.min(a, b);
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
function emojiBackground() {
|
||||
if (articleMode()) return '#7c3aed';
|
||||
const presetColors = {'no-ai':'#2f855a', research:'#2b6cb0', summary:'#b7791f', full:'#c53030'};
|
||||
const extentColors = {none:'#2f855a', assisted:'#2b6cb0', partial:'#b7791f', mostly:'#c05621', full:'#c53030'};
|
||||
return presetColors[$('preset').value] || extentColors[$('extent').value] || '#4a5568';
|
||||
}
|
||||
|
||||
function updateContrastStatus(locale) {
|
||||
const target = $('badge-contrast-status');
|
||||
if (!target) return;
|
||||
target.hidden = true;
|
||||
target.textContent = '';
|
||||
if ($('theme').value !== 'emoji') return;
|
||||
const foreground = validValue('left-color') || '#ffffff';
|
||||
const background = validValue('right-color') || emojiBackground();
|
||||
const ratio = contrastRatio(foreground, background);
|
||||
if (ratio != null && ratio < 3) {
|
||||
target.textContent = locale.text.contrast_adjusted || 'The selected emoji colours have insufficient contrast; the icon will automatically use black or white.';
|
||||
target.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRenderedArtifacts(params, theme, locale) {
|
||||
const sequence = ++renderSequence;
|
||||
clearTimeout(renderTimer);
|
||||
const rendering = locale.text.embed_rendering || 'Rendering embed …';
|
||||
$('html-code').value = rendering;
|
||||
$('markdown-code').value = rendering;
|
||||
$('json-code').value = rendering;
|
||||
renderTimer = setTimeout(async () => {
|
||||
const renderParams = new URLSearchParams(params);
|
||||
renderParams.set('theme', theme);
|
||||
try {
|
||||
const response = await fetch(`${base}/v1/render?${renderParams}`, {headers: {'Accept': 'application/json'}});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.detail || `HTTP ${response.status}`);
|
||||
if (sequence !== renderSequence) return;
|
||||
$('html-code').value = payload.embedHtml || payload.html || '';
|
||||
$('markdown-code').value = payload.markdown || '';
|
||||
$('json-code').value = payload.jsonLd ? JSON.stringify(payload.jsonLd, null, 2) : '';
|
||||
if (payload.accessibleText) $('preview-text').textContent = payload.accessibleText;
|
||||
if (payload.declarationUrl) $('preview-link').href = payload.declarationUrl;
|
||||
if (payload.badgeUrl) $('preview-badge').src = payload.badgeUrl;
|
||||
} catch (error) {
|
||||
if (sequence !== renderSequence) return;
|
||||
const message = locale.text.embed_render_error || 'The embed could not be rendered.';
|
||||
$('html-code').value = `${message} ${error.message}`;
|
||||
$('markdown-code').value = message;
|
||||
$('json-code').value = message;
|
||||
}
|
||||
}, 140);
|
||||
}
|
||||
|
||||
function setArticleParams(params) {
|
||||
params.set('mode', 'article');
|
||||
const fields = {
|
||||
@@ -410,27 +486,22 @@
|
||||
badgeParams.set('theme', $('theme').value);
|
||||
badgeParams.set('link', declarationURL);
|
||||
const badgeURL = `${base}/v1/badge.svg?${badgeParams}`;
|
||||
const manifestURL = `${base}/v1/declaration.json?${params}`;
|
||||
const assessment = legalAssessment(locale);
|
||||
renderAssessment(locale, assessment);
|
||||
|
||||
const alt = assessment.legal
|
||||
const badgeText = assessment.legal
|
||||
? assessment.badge
|
||||
: (isArticle
|
||||
? (locale.text.badge_article || 'Article transparency')
|
||||
: ((locale.presets[preset] && locale.presets[preset].title) || locale.extents[$('extent').value] || 'AI usage disclosure'));
|
||||
const accessibleText = `${badgeText} – ${locale.text.embed_details || 'AI usage details'}`;
|
||||
|
||||
$('preview-badge').src = badgeURL;
|
||||
$('preview-badge').alt = alt;
|
||||
$('preview-badge').alt = '';
|
||||
$('preview-link').href = declarationURL;
|
||||
const imageMarkup = `<a href="${declarationURL}"><img src="${badgeURL}" alt="${alt}"></a>`;
|
||||
$('html-code').value = assessment.legal && $('theme').value === 'emoji'
|
||||
? `${imageMarkup} <span>${assessment.badge}</span>`
|
||||
: imageMarkup;
|
||||
$('markdown-code').value = assessment.legal && $('theme').value === 'emoji'
|
||||
? `[](${declarationURL}) **${assessment.badge}**`
|
||||
: `[](${declarationURL})`;
|
||||
$('json-code').value = manifestURL;
|
||||
$('preview-text').textContent = accessibleText;
|
||||
updateContrastStatus(locale);
|
||||
scheduleRenderedArtifacts(params, $('theme').value, locale);
|
||||
const bulkLink = $('bulk-template-link');
|
||||
if (bulkLink) {
|
||||
bulkLink.hidden = !cfg.bulkURL;
|
||||
|
||||
@@ -73,3 +73,6 @@
|
||||
input:required,select:required{border-left-width:3px}.article-component label[hidden]{display:none!important}
|
||||
@media(max-width:760px){.form-step{padding:17px}.field-badge{margin-left:0;margin-top:3px}.conditional-panel{padding:15px}}
|
||||
.legal-consequences{margin-top:16px;border:1px solid var(--line);border-radius:12px;background:#f7f4ee;padding:14px 16px}.legal-consequences summary{cursor:pointer;font-weight:850}.legal-consequences p{margin:.8rem 0 0;color:var(--muted);font-size:.9rem;line-height:1.55}.legal-consequences .form-warning{color:#5b3029}
|
||||
|
||||
/* Accessible first-exposure disclosure preview */
|
||||
.disclosure-preview-link{display:inline-flex;align-items:center;gap:.55rem;min-height:36px;padding:4px 7px;max-width:100%;border-radius:7px;text-decoration:underline;text-underline-offset:.16em;line-height:1.35}.disclosure-preview-link img{flex:0 0 auto}.disclosure-preview-link span{font-weight:700}.contrast-status{margin:0;padding:12px 14px;border-left:4px solid #8a5a11;border-radius:10px;background:#fff4e5;color:#5b3b0b;font-size:.9rem;line-height:1.5}.code-tabs textarea#html-code,.code-tabs textarea#json-code{height:180px}
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer><span>{{.Background.Footer}} · App 1.8.0 · Schema 1.3 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
|
||||
<footer><span>{{.Background.Footer}} · App 1.8.1 · Schema 1.3 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
|
||||
<script src="/static/marketing.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -264,17 +264,18 @@
|
||||
<label>{{index .Text "custom_right_color"}}
|
||||
<input id="right-color" maxlength="7" pattern="#[0-9A-Fa-f]{6}" placeholder="#2b6cb0" {{if not .CustomBadge}}disabled{{end}}>
|
||||
</label>
|
||||
{{if .CustomBadge}}<p id="badge-contrast-status" class="contrast-status wide" role="status" aria-live="polite" hidden></p>{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<div class="preview-card">
|
||||
<p class="eyebrow">{{index .Text "preview"}}</p>
|
||||
<a id="preview-link" href="#"><img id="preview-badge" alt="{{index .Text "preview"}}"></a>
|
||||
<a id="preview-link" class="disclosure-preview-link" href="#"><img id="preview-badge" alt="" aria-hidden="true"><span id="preview-text"></span></a>
|
||||
<div class="code-tabs">
|
||||
<label>HTML<textarea id="html-code" readonly></textarea></label>
|
||||
<label>Markdown<textarea id="markdown-code" readonly></textarea></label>
|
||||
<label>JSON-LD URL<textarea id="json-code" readonly></textarea></label>
|
||||
<label>{{index .Text "embed_html_label"}}<textarea id="html-code" readonly></textarea></label>
|
||||
<label>{{index .Text "embed_markdown_label"}}<textarea id="markdown-code" readonly></textarea></label>
|
||||
<label>{{index .Text "embed_json_label"}}<textarea id="json-code" readonly></textarea></label>
|
||||
</div>
|
||||
<div class="toolbar wrap"><button id="copy-html" type="button">{{index .Text "copy_html"}}</button><a id="bulk-template-link" class="button secondary" href="#" target="_blank" rel="noopener" hidden>{{or (index .Text "open_bulk") "Bulk"}}</a></div>
|
||||
<div class="notice compliance-notice"><strong>{{index .Text "ai_act_embed_label"}}</strong> {{index .Text "ai_act_embed_text"}}</div>
|
||||
@@ -289,7 +290,7 @@
|
||||
<article><p class="eyebrow">HA</p><h2>{{index .Text "api_stateless_title"}}</h2><p>{{index .Text "api_stateless_desc"}}</p></article>
|
||||
</section>
|
||||
</main>
|
||||
<footer><span>AI Usage Disclosure · App 1.8.0 · Schema 1.3 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
|
||||
<footer><span>AI Usage Disclosure · App 1.8.1 · Schema 1.3 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
|
||||
<script nonce="{{.CSPNonce}}">window.APP_CONFIG={{.AppConfig}};</script>
|
||||
<script src="/static/app.js" defer></script>
|
||||
</body>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer><span>{{.Marketing.Footer}} · App 1.8.0 · Schema 1.3 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
|
||||
<footer><span>{{.Marketing.Footer}} · App 1.8.1 · Schema 1.3 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
|
||||
<script src="/static/marketing.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user