Put widgets and pages on Home, and update them on a schedule or when a file changes.
Subsections of Pages and Home
Pages
Note
Add-ons need a development build or a release newer than 4.12.1.
A page is a small HTML page in your workspace that LocalAI shows as part of its
UI. The assistant writes one when you ask (“make me a task board”), or you write
it yourself. A page keeps its data in files next to it, so the assistant can
read and change the same data in a chat.
A page runs in a sandboxed frame. It cannot reach the network, the rest of
LocalAI, your other files or your other chats. It talks to LocalAI only through
a small bridge, window.localai.
Note
Pages need the chats feature and the pages feature. An administrator always
has pages. For other users it is off until an administrator switches it on in
the user’s permissions (Users, then Pages (HTML pages in the
workspace)). When authentication is off, the single user has it. The files of
the chats must be on (see Chats).
pages/
tasks/
index.html the page (required, at most 1 MiB)
page.json title, icon, description and widget (optional)
tasks.json data files of the page, any name, any subfolder
The folder name is the page name: lowercase letters, digits and dashes, 1 to
64 characters, starting with a letter or a digit (^[a-z0-9][a-z0-9-]{0,63}$).
A folder with another name, or without index.html, is not a page.
page.json is optional:
{"title":"Tasks board","icon":"list-checks","description":"What I do this week"}
A missing or broken page.json does not stop the page; the folder name is
then the title.
A widget block in page.json lets the page be a tile on Home (see
A page on Home):
Pages in the sidebar opens the list of your pages. The entry shows only
when you have at least one page, and the first time it appears it carries a
small New label, until you open it or for a week (switch it off with
Notes about new things in Account). Pinned pages show under it.
The list groups your pages under Yours. Each row shows the title, the
description, the folder and when the page changed last and who wrote it (you
or the assistant), with Open, a pin toggle (Pin to sidebar) and a
menu: Open in dock (a new chat with the page beside it, see
The dock), Ask the assistant to change this (a new chat with the page’s path
in the message box), Show its files (the folder in Files), Rename
(moves the folder) and Delete page (moves the folder to the Trash, where
Files can restore it). Above eight pages, a filter field shows over the list.
Ask for a new page opens a chat with “Make me a page that " in the
message box.
With no pages, the screen offers Ask for a page, and Start from a task
board or Start from a reading list, which copy a starter page into
pages/task-board/ or pages/reading-list/. The copy is yours; ask the assistant to change it.
Two starter pages ship with LocalAI: a task board (task-board, data in
tasks.json) and a reading list (reading-list, data in reading.json).
Home offers both under Or start from a ready page.
Open a page to see it at /app/pages/<name>. The strip above it says that
the page runs as you, when and by whom it was last updated, and has
Reload and Ask the assistant to change this. When the assistant
changes index.html, the page reloads. When only a data file changes, the
page gets a files.changed event and stays as it is. Switching between the
light and dark theme reloads the page too: it gets the theme as data-theme
on its <html> element.
Pins are kept in LocalAI/ui.json of your home, under pinnedPages (a list of page
names). A pin or unpin keeps every other key of the file.
The first pinned page is your start page: the sidebar marks it with a
Start page label (see A start page).
How the assistant makes a page
Ask in a chat, for example “Make me a page with a reading list in
pages/reading/”. The assistant writes pages/reading/index.html and a data
file with its file tools. The page shows up under Pages as soon as the
change is saved. To change a page, open it and use Ask the assistant to
change this.
One index.html with inline script and style is the simplest page. A page
can also keep its script, style sheets, modules, small images and data in
files next to it (see Files next to the page). A
form cannot be submitted in the sandbox, so a page handles the click of a
button and the Enter key itself.
The bridge
Inside the frame, window.localai has these calls. Each returns a promise. A
path is relative to the page’s own folder.
Call
Result
localai.files.read(path)
{path, content, version, size}. Text files only (UTF-8), at most 1 MiB. A failed call rejects with an error that has status (404 for a missing file, 413, 415).
localai.files.write(path, content, {base})
{path, version, size, conflict_of}. Creates missing folders. Without base the file is replaced. With base (the version you read), a file that changed since is kept and your content goes to a conflict copy, named in conflict_of.
localai.files.list(dir)
[{path, kind, size, version}]. dir is optional; the default is the page’s folder.
localai.chat.send(text)
{sent: true, chat_id} or {sent: false}. See below.
localai.on('files.changed', fn)
Calls fn({paths}) when files of the page’s folder change. paths are relative to the folder. When LocalAI cannot tell what changed (for example after the window was in the background), fn gets {paths: [], resync: true}: read again what you show. Returns a function that removes the listener.
localai.on('visibility', fn)
Calls fn({visible}) when the page goes out of sight (the tab is hidden, or its tile on Home scrolled away) and when it comes back. Pause timers and animations while visible is false.
localai.on('mode', fn)
Calls fn({mode, size}) when a tile changes size without a reload: a tile in a slot that opens as a card, and back. localai.page and the data-mode and data-size attributes of <html> change first.
localai.page.resize(height)
Asks for a height in pixels. Only the page area of Home uses it (see the page area); elsewhere it resolves to {height: null}. Without height, the bridge measures the document (the box of <html>). Resolves to {height}, the height after the limits.
localai.page
{name, title, mode, size} of the page. mode is page in the full view and in the dock, widget in a tile, and area in the page area of Home; size is the tile’s size (compact, small, medium, large), empty in the full view, the dock and the page area. The page cannot set them.
Example: a counter that keeps its value in count.json.
<!doctype html><buttonid="b">Count</button><script>letcount=0constb=document.getElementById('b')constshow=()=>{b.textContent='Count: '+count}localai.files.read('count.json').then(f=>{count=JSON.parse(f.content).count}).catch(()=>{})// no file yet
.finally(show)b.onclick=()=>{count++show()localai.files.write('count.json',JSON.stringify({count}))}localai.on('files.changed',ev=>{if(ev.paths.includes('count.json')){localai.files.read('count.json').then(f=>{count=JSON.parse(f.content).count;show()})}})</script>
Your own writes also come back as files.changed. Compare the version you
got from write with the one read gives to skip them.
chat.send
chat.send(text) never starts the assistant by itself. LocalAI shows the
message above the page with a choice of chat: the page’s own chat (created on
first use, titled Page: <title>) or one of your recent chats. Nothing is sent
until you choose Send. The message is then added to the chat as your
message, starting with From the page "<title>" (pages/<name>):, so you can
see it in the chat before the assistant answers. Dismiss resolves the
promise with {sent: false}.
The page’s own chat is marked as started by a page. Like a chat that a trigger
starts, it gets no tool that acts at once and cannot create triggers, because
its first message was written by a page and not by you.
Tool calls through the bridge are not available.
When a page does not work
A page that throws an error in its script, or leaves a promise unhandled, shows
nothing about it: the assistant cannot run JavaScript, so it could not see the
error. LocalAI now keeps the error for it.
The frame of the page catches window.onerror and unhandled rejections and
hands them to LocalAI over the bridge. Nothing shows in the page, no badge or
button is added, and nothing goes into a chat.
When you say a page does not work, the assistant calls its read-only tool
page_errors with the page name. The tool gives the message, the line and
column when known (the line counts from the top of your index.html), how
often it happened and when it last happened. Without a name it lists the pages
that have recent errors, newest first. The built-in localai-apps skill tells
the assistant to read the error and fix it before it rewrites the page.
A page that throws no error but looks wrong has nothing to read. The
assistant then asks you what you see.
What is kept:
Rule
Value
Where
On the server, per user and per page name, outside your files. A tile of the page on Home uses the same page name. With a database the errors are in the table studio_page_errors, which every frontend shares; without one, in chats/pages/page-errors.json under the data path.
Who records
Only you, when you view your own page or its tile. A viewer of a shared page records nothing.
Same error
Counted once: same message at the same file, line and column.
How many
The 10 newest different errors of a page.
Length
A message is cut at 500 bytes and a stack at 1500.
Noise
The browser’s own errors, such as ResizeObserver loop and Script error., are dropped.
Rate
The frame sends an error once and counts repeats, so a throw in a 60 frames per second loop costs a message every few seconds. The server also limits a page to a burst of 20 reports, then one every 2 seconds.
Cleared
When any file of the page changes (a data file the page saves counts too), so the assistant never reads the error of code that is gone.
An error message can quote data of your page. The server never writes it to a
log. The assistant reads it only when it calls page_errors, and the tool
result says that the text is data and not instructions.
The sandbox is as before: the frame has no network and no new right. A page
cannot read the errors back, and it cannot stop the capture.
Files next to the page
A page can name the files of its own folder with relative URLs, as a web page
does. Before the page runs, LocalAI reads them with your rights and puts them
into the page; while it runs, fetch and XMLHttpRequest read the folder.
In the page
What happens
fetch('data.json'), fetch('./data/x.json?v=2')
Reads the file with files.read. The answer is a real Response: 200 with the text and a content-type from the extension, or 404 for a missing file (also 413 and 415). GET and HEAD only.
XMLHttpRequest
The same, asynchronous only, responseType'', text, json, arraybuffer or blob.
<script src="app.js">
The script goes into the page at the same place. With defer or async, it goes to the end of the body.
<link rel="stylesheet" href="style.css">
Goes in as a <style>, with its relative @import and url().
Images (png, jpg, gif, webp, avif, bmp, ico, svg) go in as data: URLs.
What does not work:
The network: an http:, https: or //host URL. fetch rejects with
TypeError: network is off in pages; put the data in a file next to the page;
a script, style sheet or image with such a URL stays blocked by the policy.
Files outside the page’s folder: ../, an absolute path, %2e%2e/, a
backslash. They are never read.
Packages (import 'react'), fonts from files, audio and video files, and
binary files through fetch (the bridge reads UTF-8 text only).
Writes through fetch: save with localai.files.write.
The files that go into the page are at most 4 MiB in all, and the images at
most 2 MiB. A script, style sheet or module that does not load (missing,
outside the folder, too large) shows a short notice at the bottom of the page
and a message in the browser console; an image that does not load gives only
the console message. When one of the files that went in changes, the page
reloads, as it does when index.html changes. Data read with fetch does
not reload the page: listen to files.changed.
Links
A page cannot open a window or leave its frame. A click on a link to an
http or https address (also with Ctrl, Cmd or the middle button, or the
Enter key) and window.open(url) ask LocalAI instead: a bar above the page
shows the site, the whole address and Open and Cancel. Open opens
the address in a new tab with noopener and noreferrer; Cancel does
nothing. When the host is in punycode (letters of other alphabets, such as
xn--pple-43d.com), the bar also says how it reads (аpple.com), since it
may imitate another site.
A link to #id scrolls the page to that element.
A link to a file of the page (other.html) opens nothing; the console says
why.
javascript:, data:, file:, blob: and //host links and addresses
with a user name or password are refused, with a message in the console.
The bar shows one link: a new one replaces it. A page asks at most 5
links at once, then one every 3 seconds.
window.open returns null in a page.
A page that starts a run
A page cannot call the assistant on its own, but it can write files, and a
file trigger can watch a folder of the
page. Each file the page writes there starts a run, which answers by writing a
file the page reads back.
The assistant makes the folder pages/ask/requests/ (a trigger needs a
folder that exists) and proposes a trigger:
{"when": {"file_arrives_in": "/pages/ask/requests"}, "prompt": "Read the request file named in this message, do what it asks, and write the answer to pages/ask/answers.json.", "writes": "pages/ask/answers.json"}.
You confirm it on its card.
The page writes a request:
localai.files.write('requests/' + Date.now() + '.json', JSON.stringify({text})).
The page listens for the answer:
localai.on('files.changed', (ev) => { if (ev.paths.includes('answers.json')) show() }).
Every request is one run, so it costs tokens. A page cannot start runs in a
loop: a file waits until it has not changed for 30 seconds, a file that changes
again after a run began waits for the trigger’s minimum gap (one minute by
default), the run’s own writes start nothing, and the trigger has a daily limit
(24 runs by default). The bridge also limits writes (below).
A page on Home
A page with a widget block in its page.json can also be a tile on Home.
The tile runs the same page, in the same sandbox and with the same bridge, but
in its compact mode: localai.page.mode is widget, and <html> has
data-mode="widget" and data-size="small|medium|large", so a style sheet can
switch layout without script. The page decides how its tile looks; LocalAI
does not shrink the full page.
Field
Meaning
widget.size
The size of the tile when home.json gives the widget no size: small (one column, one row unit: a line or a number), medium (two columns and rows: a short list) or large (the full row, three row units: a table or a chart). Any other value is medium.
widget.title
The title of the tile, when it should be shorter than the page’s.
widget.compact
true, or {"width": 160}: the page also has a layout for a slot (see A page in a slot). The width is in CSS pixels, 72 to 220 (120 by default); a value outside is clamped.
A page without a widget block cannot be a tile. To put a page on Home, add
the block and a widget {"id": "clock", "type": "page", "page": "clock"} to
LocalAI/home.json (or pick the page in Customise Home, or ask the
assistant to “put it on Home”). A page of a shared folder also has
"folder": <folder_id>. Home shows at most three page tiles.
A small clock that is a full page in its own view and one line on Home:
<!doctype html><style>body{font:15pxsystem-ui,sans-serif;margin:0;padding:24px;}#time{font-size:64px;}:root[data-mode="widget"]body{padding:12px16px;}:root[data-mode="widget"]#time{font-size:28px;}:root[data-mode="widget"].more{display:none;}</style><divid="time"></div><pclass="more">The time where you are. On Home, only the time shows.</p><script>constel=document.getElementById('time')lettimer=0consttick=()=>{el.textContent=newDate().toLocaleTimeString()}conststart=()=>{tick();timer=setInterval(tick,1000)}start()localai.on('visibility',({visible})=>{clearInterval(timer)if(visible)start()})</script>
The tile has a bar with the page’s icon and title, Open page and a refresh
button. Its frame is made only when the tile scrolls into view, and it is taken
down when the tile is far off screen; nearer, or in a hidden tab, the page gets
visibilityfalse and should pause. A tile counts toward the eight live
frames of the app (see Live frames and the budget of a tile).
The height of each tile is fixed by its size, so nothing moves when a page
loads. A tile reads its data as the full page does, with fetch or
localai.files.read, and files.changed keeps it live.
In a tile, a link of the page asks before it opens, as in the full view.
chat.send is refused: open the page to send a message. When a page is gone,
cannot be read, or has no widget block, the tile says so with a link to
Customise Home. A shared page needs your consent first: the tile shows
Allow this page, which opens the trust sheet with Allow on Home, in slots
and in the dock (see Shared pages in a slot and on Home).
A page in a slot
The slots of LocalAI/ui.json (the chat header, the sidebar footer and the
composer toolbar, see Make LocalAI yours)
show page tiles. A slot is a strip about 40 px high, so a page runs there only
when its widget block has compact: <html> then has data-size="compact"
and localai.page.size is compact. Draw one short line: a value and a word.
A click on the tile opens it as a card under the tile (above it in the sidebar
and the message box), at the size of widget.size. The card is the same frame
in a bigger box: the bridge sends mode with the new size and the page keeps
its state; it is never reloaded. Escape, a click outside or the close button
puts it back in the strip. The card has the page’s title, Open, and the line
“Sandboxed, no network”. In the strip the page is shown, not used: a click
opens the card. A page without compact is a button that opens the page: in
the dock beside the chat while a chat is open (see The dock), in
the page view anywhere else. Open of a card does the same.
A tile in a slot reads its data as the full page does, and files.changed keeps
it live. chat.send is refused in a tile. A link asks before it opens.
Live frames and the budget of a tile
The app keeps at most eight page frames running at once, for the page area and
the tiles on Home, the tiles in the slots and the page in front of the dock
together. A tile that gets none shows as a card with Run;
a click runs it and stops the frame that started first, which then shows as a
card. When a tile goes away, the next waiting tile runs.
A tile runs without a click, so it has a budget, and so does the page in
the dock. Its bridge sends a beat about
once a second with the time the page kept the browser busy since the last one
(its long tasks, and how late the beat’s timer fired). LocalAI stops the frame
and shows Reload when:
Reason
Limit
The page keeps the browser busy
more than 3 seconds of work within 10 seconds
The page stops answering while it is in sight
no beat for 5 seconds
The page does not start
no first beat within 10 seconds
The page area of Home is watched the same way, but it must show its first
beat within 5 seconds of its start, and a failure shows the tile of
the page area instead of Reload in a card.
A tile out of sight (a hidden tab, a tile scrolled away) is not watched,
because the browser slows its timers. Reload is manual: there is no retry loop.
The full page view has no budget. The browser decides how much a frame can do
before LocalAI can stop it; a frame that blocks the browser completely is
stopped only when it answers again.
Shared pages in a slot and on Home
A shared page on Home, in a slot or in the dock starts as soon as the screen
shows, so allowing it to open is not enough: its tile shows Allow and the
trust sheet offers Allow on Home, in slots and in the dock. That consent is kept against the hash of
the page like Allow, so a change of its code asks again, and Stop allowing
this page removes both. Allow once runs the tile for this visit only.
A page in the page area of Home
LocalAI/home.json can name one page that fills a full-width area on Home,
below the greeting, the composer and the shortcut chips and above the widgets:
{"pageArea":"today"}
pageArea is the name of one of your pages, {"page": "today", "folder": 3}
for a page of a shared folder (folder is the folder_id of the shared pages
list), or null (or no key) for no page area. Unlike a tile, the page needs no
widget block. Pick it in Edit layout on Home, under Page area, or ask
the assistant. The save is a normal change set, so it shows in the history and
can be reverted.
The page runs in the same sandbox, with the same bridge and the same budget as
a tile. It hears localai.page.modearea (and <html data-mode="area">) and
has no size: the area is the full width of Home. It is 240 px high until the
page says how high it is, and stays so for a page that never does; the page
scrolls inside it. To fit the area to its content, the page calls
localai.page.resize() when its content changes:
The area follows, never lower than 160 px or higher than about 60% of the
window: a longer page scrolls inside. LocalAI applies at most four heights a
second (the last one asked), ignores a change of a few pixels and never reloads
the frame for it. A page whose <html> or <body> fills the frame
(height: 100%) measures the frame, not its content: pass the height of its
content, localai.page.resize(el.offsetHeight). The frame is clipped to the area, so a
page cannot cover the strip, the composer or the widgets. chat.send is
refused, because the composer is on the same screen. The area counts toward the
eight live frames of the app. A shared page asks first, like a shared page on
Home: the area shows Allow this page until you allow it.
LocalAI draws a thin strip above the page: its name, who made it (Page by
the assistant, Page by you or Page shared by an author), Runs as
you, Reload and Edit, which opens a chat that asks the assistant to
change the page. The page cannot change the strip.
When the page does not load
Only the area is replaced: the composer and the widgets keep working. LocalAI
shows This page did not load. with the reason when:
Reason
What LocalAI saw
The page is missing
the page was moved, deleted, or you cannot open it any more
The value is not a page
pageArea in home.json is not a page name
A script error
the script threw before the page’s first beat
No first paint
no first beat from the page within 5 seconds
It was stopped
the budget of a tile (busy, no answer) or an attempt to leave its frame
LocalAI decides from what it sees (the read of the page, the load of the frame,
the beat of the bridge and a timer), never from what the page says about itself.
The tile offers Revert, Open the file and Remove from Home.
Revert undoes the last change of the page’s folder, or the last change of
home.json when the value is not a page name; it is not offered for a shared
page, which is not your file. Remove from Home writes "pageArea": null.
Both are change sets, so the assistant sees them and you can undo them. There
is no retry loop: Reload in the strip starts the page again.
An administrator can switch the page area off for everyone (the
home_page_area_enabledruntime setting).
The area then shows a note that says so, and the server leaves pageArea out
of the home.json it serves (GET /api/chats/wellknown/home names it in
withheld). The file keeps its value: a write of the whole file that does not
name pageArea keeps it too, so the page comes back when the area is on again.
A start page
The first entry of pinnedPages in LocalAI/ui.json is your start page. It has
its own screen, the page view at /app/pages/<name>, and a Start page label
in the sidebar. The Home route does not change. The strip of the page view has
a switch, Make this my first pinned page: turning it on pins the page if
needed and moves it to the top of the pinned list, and turning it off moves it
behind the other pins, so the next pin becomes the start page. The change keeps
the other pins, and every other key of the file.
The dock
The dock is a panel beside a chat that shows one or more pages in tabs, so you
can talk about a page while you see it. There is no dock until something opens
one. A page gets into the dock in four ways:
Dock in the chat header, or Ctrl+\ (Cmd+\ on a Mac), shows and hides
the dock. An empty dock, and the + of the tabs, list your pages and the
shared pages to open. The + always opens that list for a new tab and
puts the focus on its first page; a page of the list opens as a tab and
closes the list, and Escape or the tab in front goes back to the page.
Open in dock on the Pages list starts a new chat with the page beside it.
A page button in a slot, and Open of a compact tile’s card, open the page
in the dock of the open chat.
The assistant opens a page of your home with its open_page tool, for
example after it wrote the page you asked for. It does not ask first: the
page runs as you anyway when you open it. It refuses a page of a shared
folder, which needs your consent in the dock first.
The strip under the tabs says where the page comes from (Page by the
assistant, Page by you or the shared folder) and that it Runs as you.
Ask the assistant to change this puts a request that names the page in the
message box; nothing is sent until you send it. The bar has Reload, a link
to the full page and a button that hides the dock. The footer says Live:
updates as files change and Sandboxed, no network.
The dock and the Files pane of the chat share the right side, one at a time.
Files and Dock are two buttons next to each other in the chat header:
opening one hides the other, and a file the assistant writes never takes the
side from an open dock (the count on Files still grows). In a window under
1280 px, while the dock is open, both buttons show only their icon.
Each chat keeps its own dock: the open pages, the page in front, whether the
dock shows and its width are stored with the chat on the server, so a reload,
another browser and another LocalAI frontend of the same cluster show the
same dock. A new chat keeps its dock until its first message creates the chat.
Drag the left edge of the dock to change its width: at least 320 px and at most
60% of the window. A double click resets it to 452 px. With the edge focused,
the arrow keys move it (Shift for bigger steps), Home and End go to the bounds
and Enter resets it. The thread, the chat header and the message box narrow
for the dock; it covers none of them. On a phone the dock is a bottom sheet
that stops under the chat header: drag its handle up for full screen and down
to close, or tap it to switch between the two heights.
A docked page runs in the same sandbox as everywhere, with the same bridge
and the same budget as a tile (see Live frames and the budget of a
tile). The page in front counts toward
the eight live frames of the app. A page you open takes a frame at once and
stops the frame that started first; a page that comes back with a reload waits
for a free one and shows Run it here until then. localai.page.mode is
page, as in the full view, with no size. chat.send from a docked page shows the message in the dock; it goes to
this chat, as your message, only when you press Send. The message lies
over the top of the page, so the page does not move, and Send works half
a second after it shows. A page that asks more than 3 times at once waits, as
in Limits. A shared page runs
in the dock only after you allowed it on Home, in the slots and in the dock
(Allow on Home, in slots and in the dock), because the dock starts it again without a
click after a reload.
Limits
Limit
Value
index.html
1 MiB
A file that the bridge reads or writes
1 MiB, UTF-8 text
Files put into the page (scripts, styles, modules, images)
4 MiB in all, images 2 MiB
Path
256 bytes, 8 folders deep
Writes
20 at once, then one every 2 seconds, per page
chat.send
3 at once, then one every 20 seconds, per page; 4000 characters
Pages in a listing
200
A page writes data files only. A write of a code or document file is refused
with the error code forbidden and the message “a page can write data files
only (such as .json or .txt); ask the assistant to change the code of the
page”. That covers the extensions .js, .mjs, .cjs, .css, .html,
.htm, .svg, .xml, .xhtml and .wasm (in any case, in the page’s folder
and its subfolders), a file named index and page.json. Because a page loads
its own scripts and styles, a page that could rewrite them would run new code on
the next load. The server enforces this on the page’s bridge; you and the
assistant write the same files through the Files page and the file tools. A
shared page follows the same rule, and a consent is kept against the code you
allowed, which a page cannot change. Debounce writes in a page that saves while the
user types.
The rate limits are kept by each frontend. With several frontends, a page gets
one budget on each. They protect against a page that loops, not against a
person.
The sandbox
The frame is <iframe sandbox="allow-scripts"> without allow-same-origin.
The page has an opaque origin: it cannot read LocalAI’s cookies, storage or
DOM, cannot navigate the LocalAI window, open pop-ups or submit forms.
The page is loaded with this content security policy in front of its own
markup:
The network is blocked for fetch, XMLHttpRequest, WebSocket, external
scripts, styles, fonts and images, and nested frames. Images work as
data: URLs. Links open only after you confirm them (see Links).
A page with ES modules of its folder also has blob: in
script-src: its modules load from blob: URLs that the frame makes
itself, which reach no network. fetch and XMLHttpRequest of the page
are replaced by the bridge reads of Files next to the
page.
A page can still navigate its own frame to another address. LocalAI sees
the second load, closes the bridge and shows “The page tried to open another
address” with Reload. The request of that navigation has left the
browser by then, so do not run a page you do not trust with data you would
not share.
The bridge is a MessageChannel that LocalAI hands to the frame when it
loads. Every call is checked by the server with your rights: a path that
leaves the page’s folder (.., an absolute path, a backslash, a control
character) is refused. The workspace has no symbolic links, so a link cannot
lead out of the folder either.
A page runs as you: what it writes is saved as your change, and shows in the
file history as “Page : save ”. Undo works as for any other
change.
Shared pages
A shared folder is a folder outside your home that an administrator gave you
access to, read-only or read-write (a workspace grant). An administrator
creates the shared folders and chooses their members in the Shared tab of
Files or through the API (see Shared
folders). Its pages are the
folders pages/<name>/ with an index.html, as in your home. They show on the
Pages screen under Shared with you, with their author (the account that
last wrote index.html), the folder, and a badge: Not allowed yet,
Changed since you allowed it or Allowed. A user without a grant on the
folder sees nothing of it.
A shared page runs as you, so it runs only after you allow it. Opening one the
first time shows a sheet that says who wrote it, where it lives, what it can do
(read and write files in its own folder, write a message into a chat that you
see before the assistant answers), what it cannot do (read your other files or
chats, reach the network, use tools or change your settings), and one limit of
the sandbox in plain words: a page can load another address in its own frame,
which sends one request out before the page stops. Cancel has the focus;
Escape is Cancel. View source shows index.html as text.
Choice
Result
Allow
Kept for this version of this page. It runs now and each time you open it.
Allow once
It runs for this visit. You are asked again next time. A visit ends after 12 hours.
Allow on Home, in slots and in the dock
Shown by a tile on Home or in a slot, the page area and the dock. Kept for this version, and it also lets the page run there without a click.
Cancel
Nothing runs. The frame says “This page has not run”, with Review and allow and View source.
A consent is kept against the page (the node of its folder, so a rename does
not lose it and a page deleted and made again asks again) and the hash of its
code: index.html, page.json and the files of code that LocalAI puts into
the page: every <script src>, <link rel="stylesheet">, relative CSS
@import and ES module import of the folder, followed through the whole
module graph (the code_files of the page in the API). A file of code that
the page names and that is missing counts too, so adding it later also asks
again. When the author changes any of them, the next open asks again and says
what changed: the size of each file and the lines of index.html, or which
files of code there are when only one of them changed. A page that is open when its
author changes it keeps running the version you allowed, and its strip says
that it changed, with Review; a file of code it then asks for is refused
(409), so new code never runs under the old consent. The check ignores the
case of the name, as the files do: Index.html or JS/App.js is the same file
of code as index.html or js/app.js. The UI puts no text file
into a shared page that is not in its code_files.
The data files of a page are not in the hash: a page saves them all the time,
and images are only pictures. A page whose code reads its data and runs it as
code does what its data says; you trust its author for that when you allow it.
The files go into the page only after you allow it, with the same consent
check as the bridge. To find the files of code, the server reads the page’s
markup and modules; each frontend keeps what it found per file content in a
small cache, which only saves reads (the same content gives the same files on
every frontend).
A consent for one page never lets another page run, even one with the same
code, and it is yours only. Stop allowing this page in the page’s menu
revokes it. Every call of a shared page’s bridge checks the consent on the
server, so a page that you stop allowing, in this tab or in another one, stops
at its next call.
A shared page uses the same bridge and limits as your own, with your own grant
on the folder: in a folder you can only read, its writes are refused (“you can
only read this shared folder”). It reads, writes and lists only its own folder.
A message it sends goes to your chat and says which shared page it comes from.
The consents are kept on the server, not in your files: the assistant, a page
and a trigger run can all write your files, so a consent there could be written
by the code it is meant to hold back. With a database (authentication on), they
are in the tables studio_page_consents and studio_page_consent_events, which
every frontend shares; without one, in chats/pages/consents.json under the
data path. Each Allow, Allow once and revoke is also written to the
consent audit (studio_page_consent_events: who, which page and folder, which
hash, when) and to the log.
An administrator can turn shared pages off at run time with
shared_pages_enabled in the runtime settings
(or start with LOCALAI_DISABLE_SHARED_PAGES=true). Then the list still shows
the pages, dimmed, with “Shared pages are off on this server. An administrator
turned them off.”; a page does not open; and the bridge refuses every call, so
an open shared page stops at its next call. Revoking a consent still works. The
files are kept.
API
The UI uses these routes. They need the chats and pages features (the dock routes need only chats).
Route
Use
GET /api/chats/pages
List your pages, each with its widget block when it has one.
GET /api/chats/pages/{name}
A page with its index.html as html.
GET /api/chats/pages/{name}/files?dir=
files.list
GET /api/chats/pages/{name}/file?path=
files.read. With encoding=base64 the file must be an image (by extension) and content is its bytes in base64, with media_type. The UI uses it to put images into the page; the bridge reads text only.
PUT /api/chats/pages/{name}/file
files.write, body {path, content, base}
POST /api/chats/pages/{name}/chat
chat.send after you chose Send, body {text, chat_id, model}
POST /api/chats/pages/{name}/errors
An error that the script of your own page threw, body {message, stack, file, line, column, count}. The UI sends it; the assistant reads the errors with page_errors. 200 with recorded (false for browser noise), 429 when reported too fast. There is no route to read them back and none for shared pages.
GET /api/chats/{id}/dock
The dock of a chat: {open, tabs: [{page, folder}], active, width}, or null. The chat’s detail (GET /api/chats/{id}) carries it as dock.
PUT /api/chats/{id}/dock
Replace the dock of a chat: at most 6 tabs, each page once (folder is the folder_id of a shared page, absent for your own), active the index of the tab in front, width 0 (the default) or 320 to 4096. A dock with no tab clears it. 400 out of bounds, 409 for a sub-agent run. The chat’s event stream sends dock when it changes.
GET /api/chats/page-starters
The starter pages.
POST /api/chats/page-starters/{id}/copy
Copy a starter into pages/.
GET /api/chats/shared-folders
Your shared folders, with your mode and how many pages each has. folder_id of a shared page is the id of its folder.
GET /api/chats/shared-pages
The pages of your shared folders: folder, folder_id, author_id, author_name, writable, hash, code_files, widget, consent (none, allowed, changed), and enabled.
GET /api/chats/shared-pages/{folder_id}/{name}
A shared page with html, lines and consent (state, and for the version you allowed allowed_at, size, meta_size, lines, and embed when you allowed it on Home, in the slots and in the dock).
PUT /api/chats/shared-pages/{folder_id}/{name}/consent
Allow a version, body {hash, once, embed}. 409 when the page changed since you read it. A visit (once) answers with a token. embed also allows it on Home, in the slots and in the dock (a lasting consent, so not with once: 400); a lasting consent to the same version keeps embed.
GET .../files, GET and PUT .../file, POST .../chat
The bridge of a shared page, as above. Each call sends X-LocalAI-Page-Hash (the version that runs) and, for a visit, X-LocalAI-Page-Visit. Without a consent: 403 page_not_allowed. A read of a file of code by a frame of an older version: 409.
With shared pages off, every shared-pages route but the list and the revoke
answers 403 shared_pages_off.
The answers use the error types of the chats API: 400 for a bad name or path,
403 when the feature is off or a page tries to write code (anything but a data file), 404 for
a page or file that does not exist (also another user’s), 413 for a file over
1 MiB, 415 for a file that is not text and 429 for too many writes or
messages.
Home widgets and live pages
Note
Add-ons need a development build or a release newer than 4.12.1.
Home can show widgets. A widget is a small tile with a list, a number or one of your pages. You can change what Home shows, and a trigger can keep a tile current.
Note
This needs the chats and their files. The Pages feature, and the Triggers feature, are off for new users on a server with accounts. An administrator turns them on in the permissions of each user. Administrators always have them. The server option LOCALAI_DISABLE_TRIGGERS turns triggers off for everyone.
Kinds of tile
Kind
Shows
Data
list
A list of items, each with a label and an optional note
Data comes from files, not from live services. A page has no network access. A line chart is the only chart type of the data widgets.
Add, remove and reorder
Open Home and choose Edit layout.
Add a widget, remove one, or change the order.
Save.
You can also ask the assistant: “Show my open tasks on Home”. It writes the data file and adds the widget to LocalAI/home.json. Home is per user. There is no shared Home for a group. Home has at most 12 widgets, and at most 3 of them can be page tiles. You cannot change the size of a tile from Home. A page sets its own size in its page.json.
Make a page a tile
A page can be a tile when its page.json has a widget block with a size of small, medium or large. Pick the page in Edit layout, or ask the assistant to put it on Home. See A page on Home.
Keep a tile current
A trigger starts a run of the assistant. The run rewrites a file, and the tile reads it.
A schedule trigger runs daily, on weekdays, weekly, monthly, once, or from a cron expression. A schedule trigger runs at most once every 5 minutes, however it is made.
A file trigger runs when a file arrives in a folder. A page can write that file. See A page that starts a run.
There is no trigger that a web request can start from outside.
Example:
Ask: “Every morning at 8, list my open tasks from my notes on Home.”
The assistant writes data/tasks.json, adds a list widget for it, and proposes a trigger: daily at 08:00, writes data/tasks.json.
Review the trigger card and choose Create.
Each morning the trigger runs one chat turn for you and rewrites the file. The tile shows the new list.