From 0379d14b83f9fbfa52bc3b732b01ca3ac5c6dc9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 5 Oct 2026 00:03:20 +0200 Subject: [PATCH 1/7] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 7719ca451..2b335f489 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph linkeddatahub - 6.1.0 + 6.1.1-SNAPSHOT ${packaging.type} AtomGraph LinkedDataHub @@ -46,7 +46,7 @@ https://github.com/AtomGraph/LinkedDataHub scm:git:git://github.com/AtomGraph/LinkedDataHub.git scm:git:git@github.com:AtomGraph/LinkedDataHub.git - linkeddatahub-6.1.0 + linkeddatahub-5.5.4 From cd6e022d0f24f4758a33278789aeddc19da267e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 5 Oct 2026 00:03:23 +0200 Subject: [PATCH 2/7] Set the RDF library and CLI versions to 6.1.1-SNAPSHOT --- cli/pom.xml | 2 +- rdf/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index d78a8ae3d..ec5faef9e 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -4,7 +4,7 @@ com.atomgraph linkeddatahub-cli - 6.1.0 + 6.1.1-SNAPSHOT jar LinkedDataHub CLI diff --git a/rdf/pom.xml b/rdf/pom.xml index 1a66ffd6f..2244a400f 100644 --- a/rdf/pom.xml +++ b/rdf/pom.xml @@ -4,7 +4,7 @@ com.atomgraph linkeddatahub-rdf - 6.1.0 + 6.1.1-SNAPSHOT jar LinkedDataHub RDF From 1ab906794e4d5c92856105c1a0f5d8ea4019be5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 5 Oct 2026 00:10:01 +0200 Subject: [PATCH 3/7] The release workflow builds linkeddatahub-rdf before the CLI it attaches The cli job built cli/ alone, so Maven resolved linkeddatahub-rdf at the release version from Central. release.sh deploys the library only after pushing the tag, and Central publishes it minutes later, while the job starts seconds after the push: it failed for 6.0.1 and 6.1.0, and since it is the job that creates the GitHub release, neither got one. 85fde19fb taught the three test workflows to install rdf/ from the checkout first and missed this one. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 385670d70..7480507ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,6 +161,13 @@ jobs: # strip the literal prefix echo "FULL_VERSION=${RAW_REF#linkeddatahub-}" >> $GITHUB_ENV + # The CLI resolves linkeddatahub-rdf at its own version, which the tagged checkout holds. Maven + # Central does not: release.sh deploys the library after pushing the tag, and Central publishes + # it minutes later, while this job runs seconds after the push. + - name: Build the linkeddatahub-rdf library + run: mvn -B install + working-directory: rdf + # release.sh keeps cli/pom.xml at the platform version, so the tagged commit already carries it # and the jar manifest ldh --version reads gets it from there - name: Build the ldh CLI From 2ad169de013540bbd224fdc2cdba06b4f3088488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 5 Oct 2026 23:04:13 +0200 Subject: [PATCH 4/7] An assistant: a request in words becomes a Web-Algebra plan, in a chat block of its own (#407) * An assistant drawer: a question becomes a Web-Algebra plan, shown before it runs The drawer on the right edge of every page for a signed-in reader sends a question in natural language to the web-algebra sidecar's plan service, with the document the reader is on, its dataspace's SPARQL endpoint and the earlier turns. The plan comes back as its summary, the operations it invokes and the XML itself, and runs only on Execute (or at once with "Execute by default"); the steps report as the executor does, the result set or the documents written are shown, and the page catches up. Each card keeps its plan and, once run, what it returned, so a follow-up that refers to "them" is planned on that result as a VALUES block rather than on a new query. A Clear button empties the log and both stores. LDH's operation family is addressed as `waldh:` elements. The stack gains the web-algebra service and nginx's reserved /webalgebra path with the caller's certificate; OPENAI_MODEL picks the planner's model. The chat section of ldh.css parks the core Drawer under the sticky header. Co-Authored-By: Claude Fable 5.1 * Serve the agent guide at /AGENTS.md, and say in it that the default graph is empty A client writing a query against a dataspace's endpoint has to know that every document is a named graph and a pattern outside GRAPH matches nothing; the assistant's SPARQLString reads an endpoint's AGENTS.md as service documentation and was answering "no results" for want of it. The repository's guide is shipped in the WAR from where it lives, mapped to the default servlet beside robots.txt and sitemap.xml so it is public and never enters the dispatcher, served as text/markdown, and gains the named-graph paragraph in its Querying section. Co-Authored-By: Claude Fable 5.1 * The assistant is part of the dataspace's panel, and its rows name what they return The drawer moves inside #tab-body, beside the panes and under the dataspace tab bar: the header and the tab bar keep the full width, the drawer starts where the panel does, and its head takes the action bar's height so the two seams are one line. An open drawer insets the panes and the footer rather than the frame. The create dock's full-bleed margin and padding, computed on the viewport, slid its buttons under the drawer; they now share --ldh-pane-w, the panes' width, which the open drawer narrows. A result set's rows get their labels: the distinct URIs are looked up with the two metadata queries a view block runs, against the endpoint the plan queried, and the rows are drawn again with the metadata tunnelled to the link leaf when the labels land. The UI spec measures the panes and the seam, and expects the written container by its title. Co-Authored-By: Claude Fable 5.1 * The model key is an optional secret, declared in the override like the OAuth ones The base compose file no longer binds secrets/openai_api_key.txt, so a stack without a key - the test workflows, a deployment that only runs plans - comes up; a deployment that writes plans declares the openai_api_key secret and OPENAI_API_KEY_FILE in its override, which is where the dev stack now has them. Co-Authored-By: Claude Fable 5.1 * The assistant hands the planner the dataspace's ontology, and the agent guide says a join crosses documents The dataspace's ontology, which every document advertises with Link rel=lds:ontology, is the vocabulary its domain data is described in: labels, domains and ranges the planner's induced schema lacks, and none of the documents' plumbing the induced one picks up. The client now keeps the Link target the way it keeps the endpoint, lds:ontology() reads it, and the question goes to the plan service with it, so a SPARQLString against this dataspace is written from GET of the ontology rather than from ExtractOntology (REST-VKG #26). The guide the query writer reads from /AGENTS.md gains the two rules the Northwind evaluation showed the model breaking: a resource is described in the document about it, so a pattern that follows a link to another resource needs a GRAPH of its own for each; and a property is used on instances of its domain, so what the ontology declares on Person is reached through the corporation's employee, not written on the corporation. Co-Authored-By: Claude Fable 5.1 * An ASK is answered with a boolean, which the assistant's query check depends on The SPARQL endpoint answered an ASK with an empty result set, so the Web-Algebra SPARQLString check - an ASK with the query's own pattern, to send a query that matches nothing back to the model - never bit against this platform. The boolean result is Core 5.0.6: SPARQLResult, its provider, and the ASK path of SPARQLEndpointImpl. Core is pinned at 5.0.6-SNAPSHOT ahead of Web-Client's transitive 5.0.5, and SPARQLResultProvider is registered beside ResultSetProvider on the server and on both clients. The pin resolves from a local install only until Core is released, so an image build needs that release, or the snapshot deployed, first. Co-Authored-By: Claude Fable 5.1 * A plan the service refused reports as a failure in the drawer, with the service's reason An envelope holding a message instead of a plan was shown as a reply whatever the status, which read a validator's refusal - a parser's line and column - as the assistant declining in words. A message on a 200 is still a reply; on any other status it is the service's reason for failing, and the card now reports it as the failure it is, through the chain's own failure path, with that reason as the detail rather than the status line. Co-Authored-By: Claude Fable 5.1 * A plan that stopped folds out the XML of the steps it did enter A step row borrowed its operation's XML from the plan only when the executor's steps and the plan's operations matched one to one, so a plan that failed at its tenth operation of nineteen rendered every row without a fold-out, and so did one still running - only a plan that ran to the end lined up. The steps are now paired with the plan's leading operations, as many as were entered, in the order the executor enters them; a ForEach's iterations are more steps than operations and still get none. Co-Authored-By: Claude Fable 5.1 * The assistant is a block in the content body, with its composer docked onto the create bar The assistant produces blocks, and the drawer kept it where blocks are not: a plan wrote into the document, then the whole page reloaded to catch up, in a panel beside the content it changed. The placement rule already said where it belongs - a form for what is added to the current document renders inline - so it moves there. The conversation is an ephemeral block at the end of the content body, before the create bar, so what a step writes lands above it in order; the composer is a panel docked onto the bar's upper edge, opened by the bar's own button beside Create and closed by it or by Escape. A page with no bar - a proxied resource, a search result - gets a bar holding the composer alone, so reads work everywhere. None of it is server-rendered but the button: a conversation exists only once the client runs, and the body it sits in is replaced on every navigation and every write's catch-up. So the conversation is not the DOM: each document's log is an element kept under the document's URI, the one composer form is kept for the session, and ldh:ChatMount moves them into whatever body was just rendered. A card is painted into its log whether the log is in the page or not, so a plan still running when the body goes finishes into its log and is there when the document is next shown; a card that reported a write comes through the catch-up with its steps, its documents and its fold-outs. Conversations are per document, and the history the planner hears is that document's. The drawer, its edge sensor and its CSS go; the mount is guarded on the WebID itself rather than the agent's document, which document() only has once something loaded it. The assistant spec covers the new surface: opened from the bar and docked above it, a card in the body, Clear from the block's head, and the card surviving the catch-up. Co-Authored-By: Claude Fable 5.1 * The composer is as wide as the content column, and starts where it starts Docked onto the create bar, the composer took the bar's full width: the bar is full-bleed on the left and padded on the right to the column's edge, so the composer ran from the viewport's edge to the Create button while the content it serves sits in a centred column. Its left margin now makes up the column's inset over the bar's own padding, and its width is the column's inside its padding; margin plus width fill the bar's line exactly, which is what wraps the buttons below it. The spec measures it against the column. Co-Authored-By: Claude Fable 5.1 * The assistant is a component of the app kit: ldh-chat- classes, the kit's block header, its skin in app.css The assistant had no place in the design system - no component, no card, no rule - so its markup could only be read off the XSL. It is one now (ui_kits/app/Assistant.jsx, preview/component-assistant.html), and the product uses the kit's vocabulary: the chat-* classes become ldh-chat-*, the block's head is THE block header at compact density with Clear in its actions cluster, and the rules that are the component's look - the create bar's, the block's, the turns', the plan card's, the step rows', the composer's - move from ldh.css into the vendored app.css, pushed upstream in the same change. What stays in ldh.css is the page's geometry: where the bar parks and how wide the docked composer is, which only this page's column and pane widths can say. Co-Authored-By: Claude Fable 5.1 * The card answers in words, with the trace folded under it A plan's card showed its rows and its step tree, which is the machinery; the answer the reader asked for was theirs to read off it. Once a plan ends, and once its rows have their labels, the card asks the answer service beside the plan service for a reading - the question, the plan's summary, the steps, the documents written and the labelled rows go, a sentence or two comes back - and that sentence becomes the card's first line. The steps now live under a trace disclosure whose summary is their count and the plan's time: open while the plan is being read and run, since the rows are what there is to see, folded once there is an answer to read instead. The rows stay on the card and stay the authority; a service that cannot answer leaves the card as it was. Co-Authored-By: Claude Fable 5.1 * A result is a block in the card, drawn by the plan's hint, and Keep makes it the document's A plan's result rendered as a bare table whatever it was. It now sits in a well of its own under the card, headed like a block with the mode's label, and drawn through the document's own block rendering by the plan's presentation hint: a result set as the results table or, when the plan said chart, as a chart drawn into the card with the hint's axes; a graph as the view block's list, grid or table. What the reader sees in the card is what a block in the document would show. Keep, in the well's actions, writes it there: a plan of the card's own making - the query stored on the document, the endpoint registered as a service first when it is not the dataspace's own, a view in the mode drawn or a chart with the axes drawn, and an object block placing it after the document's last block - run at once as its own card. A document the assistant wrote to comes back in ContentMode, which is the mode that shows its blocks, so the kept block appears above the conversation. The fragments carry their '#', since the waldh: operations resolve against as a relative URI. Co-Authored-By: Claude Opus 5.5 (1M context) * A write's report is not a result, and the folded trace shows it opens and how it went Every write answers a one-row ?status ?url result set, and the card drew each one as a result well with Keep, sent them to the answer pass as the rows - which then said no value was returned - and kept them as the next question's "them". They are the documents written, which the card already lists, so they are left out of all three, and the list counts each document once. The trace folded under a summary that read as a caption: no chevron, no hover, and the green rows seemed to vanish. The summary now has a chevron that turns when open and the outcome's glyph, a green check or a red error, so the folded trace still says how the steps went. Co-Authored-By: Claude Opus 5.5 (1M context) * Keep goes: a result reaches the document by asking for it Keep pinned a result into the document as a view or chart built from the card's query. It chose the block by the card's rendering, and turned "what's the current year" into a view over SELECT (YEAR(NOW()) AS ?currentYear), which a view - a list of resources - cannot show, and whose block then failed to render at all. With chats about to be stored in the document, every result persists anyway, and putting one into the document's flow is a request in words that the planner writes as blocks. The button, its write plan and its translations are removed; the result wells stay. Co-Authored-By: Claude Opus 5.5 (1M context) * A view lists what an aliased first variable binds, and says when it binds no resources SPARQL.js gives an aliased projection, (?s AS ?resource), as a map rather than the string "?s", and reading only the string left the view's focus variable empty and its whole render failing. ldh:first-var-name reads both shapes. An alias is bound only in the projection, so the rewrites that project something else or append patterns to the WHERE (the result count, the container lookup, the parallax step) first wrap the query as a subquery (ldh:wrap-subquery). Without it the container lookup joined on an unbound variable, matched every triple in the store and exhausted Fuseki's memory. The count's projection and the parallax step's WHERE now match the outer query only, not a subquery's. A first variable that binds values, (YEAR(NOW()) AS ?year), gives rows the DESCRIBE finds nothing for. The count tells the two apart, and the block says the query lists no resources rather than that nothing matched. Co-Authored-By: Claude Opus 5.5 (1M context) * A conversation is a block: each chat has its own composer, is stored turn by turn, and shows its plans resolved The assistant moves from one ephemeral, client-kept log docked on the create bar to an ldh:Chat resource placed by an ldh:Object block, like a view or a chart. A page holds as many chats as it has chat blocks, each ending in its own composer; the bar's Assistant button starts a new one before the bar, which is written - chat, object block and the document's next rdf:_N, in one conditional POST - on its first question, so a click that asks nothing leaves nothing behind. Every turn is an ldh:ChatTurn, the chat's next rdf:_N, written to the chat's own document once its answer lands: the question, the answer, the plan and what the execution reported (result capped at 100), as XMLLiterals, and the outcome. The write catch-up waits for it, and the chat block's RowHook draws every turn from the store, so a chat survives leaving the page, can be embedded, and continues with its stored history. A reader without acl:Append sees the transcript and no composer. The literals travel escaped, typed rdf:XMLLiteral: Jena's RDF/XML parser refuses an rdf:RDF inside rdf:parseType="Literal", and a turn whose plan read a graph answered 400. The trace shows the plan as it ran: a step that reported a value at run time (the query a SPARQLString wrote, as wa:value) folds it out, and the operation around it shows the call replaced by that value. ldh.ttl and the rdf/ vocabulary gain ldh:Chat, ldh:ChatTurn and the turn's properties; the global mount, the session composer and Clear are gone. tests/ui/specs/shell/assistant.spec.mjs is rewritten around chat blocks (8 specs, 5 without a model). Co-Authored-By: Claude Opus 5.5 (1M context) * The assistant reads how to show a result as the client's mode URIs, and draws a grid and a timeline A plan's now names the layout mode as ac:mode - ac:TableMode, ac:ListMode, ac:GridMode, ac:ChartMode, the values a view block's ac:mode takes - and a chart's details as the properties a chart block has: ldh:chartType, ldh:categoryVarName, ldh:seriesVarName. ldh:present-mode and ldh:present-chart-type read them, and still read the table/list/grid/chart tokens and the type/category/series attributes of turns stored before, so those draw as they did. A result well carries its mode's URI in data-mode. Specs: a stubbed grid plan whose graph depicts its resources draws one card per resource with its image, live and again from the store; a stubbed timeline plan draws one labelled span per row, the one whose end came from COALESCE included. Both run without a model. Co-Authored-By: Claude Opus 5.5 (1M context) * An operation's XML fills its code field The trace folds out each operation's XML in the kit's code field, which is a flex row built for the kit's own text area, the one child that grows. The trace puts a plain pre there, which nothing told to grow, so a plan of short lines showed its XML in a box as wide as its longest line inside a field as wide as the card. The pre now takes flex: 1 and min-width: 0, as the text area does; long lines still scroll inside it. Co-Authored-By: Claude Opus 5.5 (1M context) * The assistant spec passes the admin base positionally, as ldh takes it since #404 Co-Authored-By: Claude Opus 5.5 (1M context) * Core 5.0.6 arrives through Web-Client 6.0.4, so the snapshot pin and the overlay exclude go The boolean result of an ASK, which the assistant checks its queries with, shipped in Core 5.0.6 and Web-Client 6.0.4 depends on it. The direct core:5.0.6-SNAPSHOT dependency, which only resolved from a local repository, and the overlay's exclude of its own Core jar are no longer needed; the SPARQLResultProvider registrations compile against the released Core. Co-Authored-By: Claude Opus 5.5 (1M context) * A bar chart's value axis starts at zero, a chart's Save hides with its controls, and a maximised graph fills the viewport - chart.xsl: a bar's length is its value, so the bar chart's value axis takes minValue 0 (ignored when the data goes below it) - app.css: the chart's Save stores what its controls picked, so its footer is hidden while the controls are collapsed - ldh.css: a maximised 3D graph canvas is pinned with inset: 0 and auto size, rather than 100% of a containing block that is not the viewport Co-Authored-By: Claude Opus 5.5 (1M context) --------- Co-authored-by: Claude Fable 5.1 --- .env_sample | 5 + AGENTS.md | 6 + CHANGELOG.md | 7 + CLAUDE.md | 1 + Dockerfile | 3 + README.md | 3 + docker-compose.yml | 65 + pom.xml | 15 +- .../linkeddatahub/rdf/vocabulary/LDH.java | 14 + .../atomgraph/linkeddatahub/Application.java | 4 + .../com/atomgraph/linkeddatahub/ldh.ttl | 47 + src/main/webapp/WEB-INF/web.xml | 7 + .../com/atomgraph/linkeddatahub/css/app.css | 88 + .../com/atomgraph/linkeddatahub/css/ldh.css | 24 +- .../atomgraph/linkeddatahub/xsl/client.xsl | 9 + .../linkeddatahub/xsl/client/block/chart.xsl | 3 +- .../linkeddatahub/xsl/client/chat.xsl | 1847 +++++++++++++++++ .../linkeddatahub/xsl/client/functions.xsl | 5 + .../linkeddatahub/xsl/client/navigation.xsl | 5 +- .../atomgraph/linkeddatahub/xsl/document.xsl | 23 +- .../atomgraph/linkeddatahub/xsl/layout.xsl | 3 +- .../atomgraph/linkeddatahub/xsl/resource.xsl | 2 +- .../linkeddatahub/xsl/translations.rdf | 72 + tests/ui/coverage/components.mjs | 3 + tests/ui/specs/shell/assistant.spec.mjs | 413 ++++ 25 files changed, 2654 insertions(+), 20 deletions(-) create mode 100644 src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/chat.xsl create mode 100644 tests/ui/specs/shell/assistant.spec.mjs diff --git a/.env_sample b/.env_sample index 6738d3a56..591bd0679 100644 --- a/.env_sample +++ b/.env_sample @@ -17,3 +17,8 @@ OWNER_STATE_OR_PROVINCE=Denmark OWNER_COUNTRY_NAME=DK MAX_CONTENT_LENGTH=2097152 + +# the assistant's plan service image tag (ghcr.io/atomgraph/webalgebra-server-ldh); `local` for an image built from a REST-VKG checkout +# WEBALGEBRA_VERSION=latest +# the OpenAI model the assistant plans and writes queries with; gpt-4o when unset +# OPENAI_MODEL=gpt-4o diff --git a/AGENTS.md b/AGENTS.md index d0d322beb..0ae9e0ee1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,12 @@ The dataspace exposes a **read-only SPARQL 1.1 Query** endpoint (advertised via Write portable, standard SPARQL: use explicit `GRAPH` patterns, no engine-specific extensions. +**The default graph is empty.** Every document is a named graph whose name is the document's URL, so a triple pattern outside `GRAPH` matches nothing. Put the patterns inside `GRAPH ?g { ... }` to query across every document, or inside `GRAPH { ... }` for one document; a subquery needs its own `GRAPH` as well. A query that returns no rows without a `GRAPH` clause is not evidence that the data is absent. + +**One document per resource, so a join crosses graphs.** A resource is described in the document about it (`foaf:primaryTopic`), and the resources it links to - an order's customer, a product's category, an employee's manager - are each described in their own document, in another graph. A pattern that follows such a link therefore needs a `GRAPH` of its own for each resource: `GRAPH ?o { ?order schema:customer ?customer } GRAPH ?c { ?customer schema:legalName ?name }`, never both triples in one `GRAPH ?g`, which asks for them in the same document and matches nothing. Only a resource's own companions - its address, its contact point, an order's line items - share its document. When in doubt, give every subject variable its own graph variable: it costs nothing when two resources do share a document, and it is the only pattern that works when they do not. + +**A property is used on instances of its domain.** The ontology (`Link rel=lds:ontology`) declares each property's `rdfs:domain` and `rdfs:range`, and the data follows it: a property declared on `schema:Person` is not on the `schema:Corporation` that employs the person. To reach it, follow the ontology's path - the corporation's `schema:employee`, then the person's `schema:address`. + Outbound `SERVICE` and `LOAD` — from the triplestore and from the platform alike — are routed through the `egress` forward proxy, which refuses loopback, private and link-local destinations. A federated query reaches public endpoints and cannot reach the deployment's own services. With no proxy configured and `ALLOW_INTERNAL_URLS` unset, in-JVM `SERVICE` (in a `PATCH` update or an import mapping) is disabled outright. ## Content & document model diff --git a/CHANGELOG.md b/CHANGELOG.md index dd28de1de..783a08637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [Unreleased] +### Added +- An assistant for a signed-in reader: a request in natural language becomes a Web-Algebra plan, shown with its operations and its XML, and runs only on Execute; the steps, the documents the plan wrote and its result are reported as they happen, and the result is answered in words and drawn as a table, a list, a grid or a chart +- A conversation is an `ldh:Chat` block, placed in the document by an object block: the create bar's Assistant button starts one, each ends in its own composer, and every turn is stored as an `ldh:ChatTurn` with its plan and what the execution reported, so a chat is there when the reader comes back +- A `web-algebra` service in the stack (`ghcr.io/atomgraph/webalgebra-server-ldh`), reached through nginx at the reserved `/webalgebra` path with a WebID certificate on the connection; it acts for the reader through the secretary agent's delegation, so a plan may write exactly what its reader may. Needs the `openai_api_key` secret +- The agent guide (`AGENTS.md`) is served at `/AGENTS.md` on every dataspace origin, public and outside the dispatcher, so a client writing a query against the endpoint - the assistant's `SPARQLString` reads it as service documentation - learns that every document is a named graph and the default graph is empty + ## [6.1.0] - 2026-10-05 ### Migration - **BREAKING**: `ldh` drops `-b`/`--base`; dataspace-level commands take the base URI as their positional, defaulting to `LDH_BASE` diff --git a/CLAUDE.md b/CLAUDE.md index c2e17250f..94867e5ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,6 +108,7 @@ The application runs as a multi-container setup: - **linkeddatahub**: Main Java application (Tomcat) - **fuseki**: One SPARQL server holding a TDB2 dataset per dataspace role (`config/fuseki/config.ttl`), named after the dataspace origin (deployment host dropped, role appended: `end-user`, `admin`, `northwind-traders.demo.end-user`, …), each under `fuseki//`; bound to apps in `config/system.trig` - **egress**: Squid forward proxy for the store's and platform's outbound requests (SPARQL `SERVICE`, `LOAD`): public destinations only, so a query cannot reach another dataset, Varnish or the platform +- **web-algebra**: REST-VKG's `webalgebra-server-ldh` image — executes Web-Algebra plans and writes them from natural language for the assistant drawer (`xsl/client/chat.xsl`). nginx forwards `/webalgebra` (a reserved path, like `/static/` and `/uploads/`) to it with the caller's certificate; it presents the secretary's certificate to this instance with `On-Behalf-Of` naming the caller, which `WebIDFilter` honours because every agent's WebID document carries ` acl:delegates `. It addresses the instance by its public origin, rewritten to `nginx:9443` (`WEBALGEBRA_PROXY_HOST`), the same trick the platform's own `ClientUriRewriteFilter` plays - **varnish-frontend/varnish-admin/varnish-end-user**: Caching layers (admin and end-user caches both front the single `fuseki`) ### Data Flow diff --git a/Dockerfile b/Dockerfile index a95246e1f..35072ea33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,9 @@ RUN mvn -B -Pstandalone dependency:go-offline COPY src /usr/src/platform/src +# the agent guide the WAR serves at /AGENTS.md (pom.xml, maven-war-plugin webResources) +COPY AGENTS.md /usr/src/platform/AGENTS.md + RUN mvn -Pstandalone clean install # ============================== diff --git a/README.md b/README.md index c6a90abbd..85b374178 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ The [`ldh` command line interface](#command-line-interface) is attached to every - `secrets/client_truststore_password.txt` - `secrets/owner_cert_password.txt` - `secrets/secretary_cert_password.txt` + - `secrets/openai_api_key.txt` — optional: an OpenAI API key for the assistant drawer, declared in `docker-compose.override.yml` like the OAuth secrets; without it the assistant runs plans but cannot write them The one you will need to remember in order to authenticate with LinkedDataHub using WebID client certificate is `owner_cert_password`. 5. Launch the application services by running this from command line: ```shell @@ -217,6 +218,8 @@ _:warning: Do not use blank nodes to identify applications or services. We recom
Password of the secretary's WebID certificate
client_truststore_password
Password of the client truststore
+
openai_api_key
+
OpenAI API key the web-algebra service writes the assistant's plans with. Optional, declared in the override together with OPENAI_API_KEY_FILE; the service executes plans without it, and the drawer then reports that a plan could not be written
google_client_id
Google's OAuth client ID
Login with Google authentication is enabled when this value is provided
diff --git a/docker-compose.yml b/docker-compose.yml index ff9d76b2e..6fadb1c27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ secrets: file: ./secrets/secretary_cert_password.txt client_truststore_password: file: ./secrets/client_truststore_password.txt + # openai_api_key: + # file: ./secrets/openai_api_key.txt # google_client_id: # file: ./secrets/google/client_id.txt # google_client_secret: @@ -28,6 +30,8 @@ services: depends_on: linkeddatahub: condition: service_healthy + web-algebra: + condition: service_started # its upstream has to resolve when nginx starts ports: - ${HTTP_PORT}:8080 # allow Tomcat to do HTTP to HTTPS redirect - ${HTTPS_PORT}:8443 # HTTPS @@ -149,6 +153,38 @@ services: target: /etc/squid/squid.conf expose: - 3128 + web-algebra: # runs Web-Algebra plans against this instance, and writes them from natural language for the assistant drawer + image: ghcr.io/atomgraph/webalgebra-server-ldh:${WEBALGEBRA_VERSION:-latest} + mem_limit: 1024m + restart: on-failure + depends_on: + - egress + expose: + - 8080 # internal only: nginx is the sole client, and the server trusts the Client-Cert header only because nginx sets it + environment: + # the origin that receives an identity - this instance, with every dataspace subdomain under it. Its public name is + # not a route to it from inside the network, so requests for it go to nginx's client-cert listener under that name + - WEBALGEBRA_TRUSTED_ORIGIN=${PROTOCOL}://${HOST}:${HTTPS_PORT} + - WEBALGEBRA_PROXY_HOST=nginx + - WEBALGEBRA_PROXY_PORT=9443 + # the secretary's WebID: the agent every user's WebID document already delegates (root-owner.trig.template, SignUp), + # so a plan submitted by a user runs under that user's own access control, not the secretary's + - WEBALGEBRA_KEYSTORE=/var/webalgebra/ssl/secretary/keystore.p12 + - WEBALGEBRA_KEYSTORE_PASSWORD_FILE=/run/secrets/secretary_cert_password + - WEBALGEBRA_TRUSTSTORE=/var/webalgebra/ssl/server/server.crt # the self-signed server certificate, as PEM + # the model key is optional, like the OAuth secrets: a deployment that writes plans declares the openai_api_key + # secret and this variable in its override; without them the service runs plans and declines to write any + # - OPENAI_API_KEY_FILE=/run/secrets/openai_api_key + - OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o} + # public destinations (SPARQL endpoints a plan names, the model's API) go through egress like the platform's own; nginx + # is reached directly, since egress refuses internal addresses + - JAVA_TOOL_OPTIONS=-Dhttp.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttp.proxyPort=3128 -Dhttps.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttps.proxyPort=3128 -Dhttp.nonProxyHosts=nginx + secrets: + - secretary_cert_password + # - openai_api_key + volumes: + - ./ssl/secretary:/var/webalgebra/ssl/secretary:ro + - ./ssl/server:/var/webalgebra/ssl/server:ro varnish-frontend: image: varnish:7.7.3 user: root # otherwise varnish user does not have permissions to the mounted folder which is owner by root @@ -200,6 +236,10 @@ configs: } http { + upstream web-algebra { + server web-algebra:8080; + } + upstream linkeddatahub { server ${NGINX_UPSTREAM_SERVER:-varnish-frontend:6060}; } @@ -217,6 +257,7 @@ configs: limit_req_zone $$limit_key zone=linked_data:10m rate=15r/s; limit_req_zone $$limit_key zone=static_files:10m rate=20r/s; + limit_req_zone $$limit_key zone=web_algebra:10m rate=5r/s; # a plan is a model call; polling is what bursts limit_req_status 429; client_max_body_size ${MAX_CONTENT_LENGTH:-2097152}; @@ -231,6 +272,30 @@ configs: ssl_prefer_server_ciphers on; ssl_verify_client ${NGINX_SSL_VERIFY_CLIENT:-optional_no_ca}; + # the assistant's plan service. A reserved path, like /static/ and /uploads/: prefix-matched ahead of the + # generic location and never rewritten, so /webalgebra, /webalgebra/plans and /webalgebra/{id}/result reach the + # sidecar as they are. Reachable only with a WebID certificate on the connection: the sidecar acts for the + # WebID in the certificate nginx forwards, and a caller without one has nobody to act for. On-Behalf-Of is + # cleared for the same reason Client-Cert is - identity comes from the connection, never from a header + location ^~ /webalgebra { + if ($$ssl_client_verify = NONE) { + return 401; + } + + proxy_pass http://web-algebra; + limit_req zone=web_algebra burst=10 nodelay; + proxy_read_timeout 120s; # writing a plan is a 10-30 s model call + + proxy_set_header Host $$host; + proxy_set_header X-Forwarded-Host $$host; + proxy_set_header X-Forwarded-Proto $$scheme; + proxy_set_header X-Forwarded-Port ${HTTPS_PORT}; + + proxy_set_header Client-Cert ''; + proxy_set_header Client-Cert $$ssl_client_escaped_cert; + proxy_set_header On-Behalf-Of ''; + } + location / { add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS" always; diff --git a/pom.xml b/pom.xml index 2b335f489..f204d29eb 100644 --- a/pom.xml +++ b/pom.xml @@ -169,13 +169,13 @@ ${project.groupId} client - 6.0.3 + 6.0.4 classes ${project.groupId} client - 6.0.3 + 6.0.4 war @@ -399,6 +399,17 @@ ${project.build.directory}/${build.warName} true true + + + + ${basedir} + + AGENTS.md + + + ${project.groupId} diff --git a/rdf/src/main/java/com/atomgraph/linkeddatahub/rdf/vocabulary/LDH.java b/rdf/src/main/java/com/atomgraph/linkeddatahub/rdf/vocabulary/LDH.java index c4f2e0637..00953ffa9 100644 --- a/rdf/src/main/java/com/atomgraph/linkeddatahub/rdf/vocabulary/LDH.java +++ b/rdf/src/main/java/com/atomgraph/linkeddatahub/rdf/vocabulary/LDH.java @@ -45,6 +45,10 @@ private LDH() { } public static final Resource CSVImport = ResourceFactory.createResource(NS + "CSVImport"); /** ldh:RDFImport class */ public static final Resource RDFImport = ResourceFactory.createResource(NS + "RDFImport"); + /** ldh:Chat class: a conversation with the assistant, its turns the rdf:_N members */ + public static final Resource Chat = ResourceFactory.createResource(NS + "Chat"); + /** ldh:ChatTurn class: one question and what came of it */ + public static final Resource ChatTurn = ResourceFactory.createResource(NS + "ChatTurn"); /** ldh:MissingPropertyValue constraint class */ public static final Resource MissingPropertyValue = ResourceFactory.createResource(NS + "MissingPropertyValue"); /** ldh:ChildrenView resource */ @@ -62,6 +66,16 @@ private LDH() { } public static final Property delimiter = ResourceFactory.createProperty(NS + "delimiter"); /** ldh:chartType property */ public static final Property chartType = ResourceFactory.createProperty(NS + "chartType"); + /** ldh:question property: what a chat turn asked */ + public static final Property question = ResourceFactory.createProperty(NS + "question"); + /** ldh:answer property: the assistant's answer in words */ + public static final Property answer = ResourceFactory.createProperty(NS + "answer"); + /** ldh:plan property: the turn's Web-Algebra plan, an rdf:XMLLiteral */ + public static final Property plan = ResourceFactory.createProperty(NS + "plan"); + /** ldh:execution property: what running the plan reported, an rdf:XMLLiteral */ + public static final Property execution = ResourceFactory.createProperty(NS + "execution"); + /** ldh:outcome property: how the turn went, in one line */ + public static final Property outcome = ResourceFactory.createProperty(NS + "outcome"); /** ldh:categoryVarName property */ public static final Property categoryVarName = ResourceFactory.createProperty(NS + "categoryVarName"); /** ldh:seriesVarName property */ diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index 222eb7a99..bb2d7591c 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -47,6 +47,7 @@ import com.atomgraph.core.io.ModelProvider; import com.atomgraph.core.io.QueryProvider; import com.atomgraph.core.io.ResultSetProvider; +import com.atomgraph.core.io.SPARQLResultProvider; import com.atomgraph.core.io.UpdateRequestProvider; import com.atomgraph.core.mapper.BadGatewayExceptionMapper; import com.atomgraph.core.provider.QueryParamProvider; @@ -1072,6 +1073,7 @@ public void init() register(new ValidatingModelProvider(getMessageDigest())); register(new ResultSetProvider()); + register(new SPARQLResultProvider()); // the boolean of an ASK, which ResultSetProvider cannot write register(new QueryProvider()); register(new QueryParamProvider()); register(new UpdateRequestProvider()); @@ -1723,6 +1725,7 @@ public void releaseConnection(final HttpClientConnection managedConn, final Obje config.register(new ModelProvider()); config.register(new DatasetProvider()); config.register(new ResultSetProvider()); + config.register(new SPARQLResultProvider()); config.register(new QueryProvider()); config.register(new UpdateRequestProvider()); config.property(ClientProperties.FOLLOW_REDIRECTS, true); @@ -1831,6 +1834,7 @@ public void releaseConnection(final HttpClientConnection managedConn, final Obje config.register(new ModelProvider()); config.register(new DatasetProvider()); config.register(new ResultSetProvider()); + config.register(new SPARQLResultProvider()); config.register(new QueryProvider()); config.register(new UpdateRequestProvider()); // TO-DO: UpdateRequestProvider config.property(ClientProperties.FOLLOW_REDIRECTS, true); diff --git a/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl b/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl index 68766a73d..b3a3132b3 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl @@ -111,6 +111,41 @@ rdfs:comment "Whether the view block is shown when its query returns no results. Defaults to true" ; rdfs:isDefinedBy : . +:question a owl:DatatypeProperty ; + rdfs:domain :ChatTurn ; + rdfs:range xsd:string ; + rdfs:label "Question" ; + rdfs:comment "What the reader asked the assistant in this turn" ; + rdfs:isDefinedBy : . + +:answer a owl:DatatypeProperty ; + rdfs:domain :ChatTurn ; + rdfs:range xsd:string ; + rdfs:label "Answer" ; + rdfs:comment "The assistant's reading, in words, of what the turn's plan did" ; + rdfs:isDefinedBy : . + +:plan a owl:DatatypeProperty ; + rdfs:domain :ChatTurn ; + rdfs:range rdf:XMLLiteral ; + rdfs:label "Plan" ; + rdfs:comment "The Web-Algebra plan the question became: the wa:plan envelope with its summary and operation" ; + rdfs:isDefinedBy : . + +:execution a owl:DatatypeProperty ; + rdfs:domain :ChatTurn ; + rdfs:range rdf:XMLLiteral ; + rdfs:label "Execution" ; + rdfs:comment "What running the plan reported: the wa:execution with its steps, the documents written and a capped snapshot of the result" ; + rdfs:isDefinedBy : . + +:outcome a owl:DatatypeProperty ; + rdfs:domain :ChatTurn ; + rdfs:range xsd:string ; + rdfs:label "Outcome" ; + rdfs:comment "How the turn went, in one line: rows or resources returned, documents written, no results, or the failure's message" ; + rdfs:isDefinedBy : . + # CLASSES # constructor @@ -343,6 +378,18 @@ rdfs:label "View constructor" ; rdfs:isDefinedBy : . +# chat + +:Chat a rdfs:Class, owl:Class ; + rdfs:label "Assistant" ; + rdfs:comment "A conversation with the assistant, placed in a document by an object block. Its turns are its rdf:_N members, in the order they were asked" ; + rdfs:isDefinedBy : . + +:ChatTurn a rdfs:Class, owl:Class ; + rdfs:label "Assistant turn" ; + rdfs:comment "One question to the assistant and what came of it" ; + rdfs:isDefinedBy : . + # exception :ResourceExistsException a rdfs:Class, owl:Class ; diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 252bc4e74..e12941edd 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -361,12 +361,19 @@ support@atomgraph.com]]> com.atomgraph.linkeddatahub.listener.ClientStylesheetListener + + md + text/markdown + default /static/* /robots.txt /favicon.ico /sitemap.xml + + /AGENTS.md com.atomgraph.linkeddatahub.Application diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/app.css b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/app.css index bd2a40125..ae6a7e9cb 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/app.css +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/app.css @@ -2154,6 +2154,8 @@ html[data-tweak-block-edges="flow"] .ldh-block:has(.ldh-edit-form) { .ldh-view-toolbar.is-collapsed, .ldh-pivot-bar.is-collapsed, .chart-controls.is-collapsed { display: none; } +/* the chart's Save stores what its controls picked, so it goes with them */ +.chart-controls.is-collapsed ~ .ldh-block-foot { display: none; } .ldh-grid-block .card { position: relative; display: flex; flex-direction: column; align-items: stretch; @@ -4599,3 +4601,89 @@ html[data-tweak-block-edges="flow"] .ldh-block:has(.ldh-edit-form) { box-shadow: inset 2px 0 0 var(--fg-selected); } .sb-other > li.is-active > * { color: var(--fg-selected); font-weight: var(--fw-medium); } + +/* ========================================================================= + The create bar and the assistant (Assistant.jsx). + + The create bar is the strip at the foot of the content body that holds the + document's Create button and, for a reader who can be acted for, the + Assistant button. The host decides where the bar parks (LinkedDataHub pins + it sticky at the column's foot); this is what it looks like. + + A conversation is a BLOCK - a chat, placed in the document like a view or a + chart - headed by THE block header, so a page holds as many as it has chat + blocks, and the Assistant button adds another before the bar. Its body is the + log, then its own composer as the block's footer: two checkboxes over the + question and the send button. A turn is the reader's question; a plan card is + a nested block holding the plan's summary, its operations as step rows + (planned / running / done / failed, each a disclosure folding out its XML + and, when it failed, the message), and what came of it: the documents + written, the rows returned. + ========================================================================= */ +.ldh-create-dock { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--sp-2); padding: var(--sp-2) var(--sp-6); background: var(--bg-card); border-top: 1px solid var(--border-default); } +.ldh-chat > form.ldh-chat-composer { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-3); border-top: 1px solid var(--border-default); } +.ldh-chat-composer-row { display: flex; align-items: flex-end; gap: var(--sp-2); } +.ldh-chat-composer .ac-field { flex: 1; } +.ldh-chat-options { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); } +.ldh-chat-composer .ac-choice { font-size: var(--fs-sm); color: var(--fg-muted); } +.ldh-chat-composer textarea { resize: none; } + +/* an empty log takes no room: a new chat block is its header and its composer */ +.ldh-chat-log:empty { display: none; } +.ldh-chat-log { display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-3); } +/* turns: the reader's message hugs the sender's edge, the assistant's cards sit flush left */ +.ldh-chat-turn { align-self: flex-end; max-width: 85%; margin: 0; padding: var(--sp-2) var(--sp-3); background: var(--bg-accent-quiet); color: var(--fg-1); border-radius: var(--r-md) var(--r-md) var(--r-xs) var(--r-md); font-size: var(--fs-sm); line-height: var(--lh-base); } +/* the plan card rides the nested-block well; chat scopes its rhythm */ +.ldh-chat-plan { padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-3); font-size: var(--fs-sm); } +.ldh-chat-plan > p { margin: 0; line-height: var(--lh-base); color: var(--fg-2); } +.ldh-chat-plan-meta { font-family: var(--font-mono); font-size: 10px; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--fg-muted); } +.ldh-chat-plan .ac-codefield { margin: var(--sp-2) 0 var(--sp-1); max-height: 210px; overflow: auto; } +/* the field is the kit's flex row; the pre stands where its .ac-cf-area would and fills the row like it, else it is only as wide as its longest line */ +.ldh-chat-plan .ac-codefield pre { flex: 1; min-width: 0; margin: 0; padding: var(--sp-2) var(--sp-3); font-family: var(--font-mono); font-size: 11px; line-height: var(--lh-snug); color: var(--fg-2); white-space: pre; } +.ldh-chat-plan-actions { display: flex; gap: var(--sp-2); } +/* the answer: the card's first line once there is one, read off the rows below it */ +.ldh-chat-answer { margin: 0; font-size: var(--fs-md); line-height: var(--lh-base); color: var(--fg-1); white-space: pre-line; } +/* the trace: the steps under a disclosure whose summary is their count and the plan's time - open while the plan is + being read or run, folded once the answer has been written. No marker; the meta line is the control */ +details.ldh-chat-trace > summary { font-family: var(--font-mono); font-size: 10px; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--fg-muted); cursor: pointer; list-style: none; } +/* the summary is a control and looks like one: a chevron that turns when open, and the outcome's glyph */ +details.ldh-chat-trace > summary { display: inline-flex; align-items: center; gap: var(--sp-1); padding: 2px var(--sp-2) 2px 2px; border-radius: var(--r-sm); } +details.ldh-chat-trace > summary:hover { background: var(--bg-hover); } +details.ldh-chat-trace > summary > .chev { transition: transform var(--dur-fast, 120ms) ease; } +details.ldh-chat-trace[open] > summary > .chev { transform: rotate(90deg); } +details.ldh-chat-trace > summary > .st.is-done { color: var(--success-500); } +details.ldh-chat-trace > summary > .st.is-failed { color: var(--danger-500); } +details.ldh-chat-trace > summary::-webkit-details-marker { display: none; } +details.ldh-chat-trace > summary:hover { color: var(--fg-2); } +details.ldh-chat-trace > ul { margin-top: var(--sp-2); } +/* The steps: one row per operation, there from the moment the plan is shown and changing state as the + executor reports - planned, running, done, failed. Each row is a disclosure whose summary is the row + and whose body is the operation's own XML (the first row's is the whole plan) and, when it failed, + the message; no marker, the row itself is the control. The tint is the status colour set the + alerts use (success-50 / danger-50), so a row that went green or red reads like the alert it replaces */ +.ldh-chat-steps, .ldh-chat-steps ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; } +/* the rows nest as the operations do: a step inside another's argument sits one level in, under a hairline */ +.ldh-chat-steps ul { margin: 2px 0 0 calc(12px + var(--sp-1)); padding-left: var(--sp-2); border-left: 1px solid var(--border-default); } +details.ldh-chat-step { display: block; } +details.ldh-chat-step > summary { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: center; gap: var(--sp-2); padding: var(--sp-1) var(--sp-2); border-radius: var(--r-sm); font-size: var(--fs-sm); color: var(--fg-1); cursor: pointer; list-style: none; } +details.ldh-chat-step > summary::-webkit-details-marker { display: none; } +details.ldh-chat-step > summary:hover { background: var(--bg-hover); } +.ldh-chat-step .st { display: inline-flex; color: var(--fg-hint); } +.ldh-chat-step .op { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ldh-chat-step.is-running > summary { background: var(--bg-accent-quiet); } +.ldh-chat-step.is-running .st { justify-self: center; } +.ldh-chat-step.is-done > summary { background: var(--success-50); } +.ldh-chat-step.is-done .st { color: var(--success-500); } +.ldh-chat-step.is-failed > summary { background: var(--danger-50); } +.ldh-chat-step.is-failed .st { color: var(--danger-500); } +.ldh-chat-step.kd-write .st { color: var(--warning-500); } +.ldh-chat-step.kd-destructive .st { color: var(--danger-500); } +.ldh-chat-step-message { margin: var(--sp-2) var(--sp-2) 0; font-size: var(--fs-sm); line-height: var(--lh-base); color: var(--fg-2); } +/* changed-document links: one row per affected graph, mono URI leaf */ +.ldh-chat-docs .op { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 0; } +/* a plan that only read shows its result set in the block's width */ +.ldh-chat-plan .ldh-chat-result { display: block; max-width: 100%; overflow-x: auto; } +/* a result sits in a well of its own under the card - depth 2 - headed like a block; a chart's canvas gets the height a chart block gets */ +.ldh-chat-result-block { margin-top: var(--sp-2); } +.ldh-chat-result-block > .ldh-nblock-body { padding: var(--sp-2) var(--sp-3) var(--sp-3); } +.ldh-chat-result-block .ldh-chat-chart { min-height: 320px; } diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/ldh.css b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/ldh.css index 5070c76d3..d5da76f56 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/ldh.css +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/ldh.css @@ -135,7 +135,7 @@ object { display: block; margin: auto; max-height: 400px; overflow: hidden; widt .graph-3d-show-panel label.sub-option { margin-left: 16px; } .graph-3d-zoom { position: absolute; top: 10px; left: 10px; z-index: 10; } .graph-3d-fullscreen { position: absolute; bottom: 10px; right: 10px; z-index: 10; } -.graph-3d-canvas.graph-3d-maximized { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; } +.graph-3d-canvas.graph-3d-maximized { position: fixed; inset: 0; width: auto; height: auto; z-index: 1000; } /* OpenLayers specific */ /* the marker info window borrows the modal's head/body anatomy but its host is ol.Overlay's own wrapper, not a dialog - so the panel treatment lands here: the overlay IS the card, which is why @@ -268,7 +268,7 @@ circle { cursor: move; } /* ---------- sticky chrome: header on top, action bar (legacy --action-bar-top) right under it. Bar heights are tokens that size the bars, not mirrors of their content, so stacked sticky offsets (.editor-bar) can compose them without drifting ---------- */ -:root { --ldh-header-height: 56px; --ldh-tabbar-height: 40px; --ldh-actionbar-height: 61px; --action-bar-top: var(--ldh-header-height); --ldh-content-w: 1280px; } +:root { --ldh-header-height: 56px; --ldh-tabbar-height: 40px; --ldh-actionbar-height: 61px; --action-bar-top: var(--ldh-header-height); --ldh-content-w: 1280px; --ldh-pane-w: 100vw; /* the width the dataspace's panes span: the viewport, less whatever stands beside them */ } /* image caps for RDF-pointed depictions: the pixel size is whatever the dataset points at, so the SURFACE picks the height. These once lived in the pre-refresh vendored app.css; a re-vendor lost the definitions while the var() uses stayed, silently voiding every declaration that read them @@ -583,21 +583,21 @@ button.tree-link { border: 0; background: transparent; width: 100%; font: inheri modes' Create picker) sits in a full-bleed header-like row. Sticky, not fixed: it rides the viewport bottom while the content scrolls, but parks at its flow position at the end of the content - stacked on the footer, never over it. The negative side margins bleed - the row out of the centered content column to the viewport edges; the right padding brings + the row out of the centered content column to the panes' edges; the right padding brings the buttons back to the content column's right edge, flush with the blocks. The margin bleed - and the padding must share the 100vw base: each is half a classic scrollbar off the layout - width, in the same direction, so the errors cancel and the buttons land exactly on the column - edge - rebasing one without the other breaks the alignment ---------- */ + and the padding must share the --ldh-pane-w base: each is half a classic scrollbar off the + layout width, in the same direction, so the errors cancel and the buttons land exactly on the + column edge - rebasing one without the other breaks the alignment. The base is the panes' + width rather than the viewport's because the assistant drawer stands beside the panes and + narrows it (the chat section below) ---------- */ html { overflow-x: clip; } /* the 50vw bleed overshoots by half a classic scrollbar; clip never creates a scroll container */ +/* the bar's look is the app kit's (app.css, .ldh-create-dock); this is where the page parks it */ .ldh-create-dock { position: sticky; bottom: 0; - display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2); z-index: 24; /* above content, below popovers (40) and modal chrome */ - margin: auto calc(50% - 50vw) 0; /* margin-top: auto parks it at the bottom of the stretched column */ - padding: var(--sp-2) calc((100vw - min(var(--ldh-content-w), 100vw)) / 2 + var(--sp-6)) var(--sp-2) var(--sp-6); - background: var(--bg-card); - border-top: 1px solid var(--border-default); + margin: auto calc(50% - var(--ldh-pane-w) / 2) 0; /* margin-top: auto parks it at the bottom of the stretched column */ + padding-right: calc((var(--ldh-pane-w) - min(var(--ldh-content-w), var(--ldh-pane-w))) / 2 + var(--sp-6)); } /* the modal dim overlay is pointer-inert, so hide the dock while a modal is open */ body:has(.ac-backdrop.modal) .ldh-create-dock { display: none; } @@ -973,3 +973,5 @@ div.ldh-failure > .ac-alert { margin: 0; } .rdfa-editor-content th { background: var(--bg-recess); } .rdfa-editor-content caption { color: var(--fg-muted); } .rdfa-editor-content .rdfa-editor-island { border-color: var(--border-default); background: var(--bg-recess); } + + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl index a9fd3f2a1..50e0871d0 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl @@ -87,6 +87,7 @@ extension-element-prefixes="ixsl" + @@ -191,6 +192,9 @@ WHERE + + + + + @@ -781,6 +789,7 @@ WHERE + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/block/chart.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/block/chart.xsl index 3c40190cc..4be0c8d2b 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/block/chart.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/block/chart.xsl @@ -225,7 +225,8 @@ exclude-result-prefixes="#all" - + + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/chat.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/chat.xsl new file mode 100644 index 000000000..5aa0975a1 --- /dev/null +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client/chat.xsl @@ -0,0 +1,1847 @@ + + + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+ + + + + +
+
+ + + + + +
+
+ + +
+
+
+
+