Plain HTML, CSS and ES modules in ui/, no build step. The sidebar opens on agent chats (this computer first), with chats on the server as the second tab; the transcript draws the /api/v2 steps, follows a reply as it is written, answers cards, and starts agent chats in one of the agent's folders. Two explorers kept apart (this computer's, the server's) and a viewer: Markdown drawn by a safe renderer that never parses HTML, code in read-only CodeMirror with the server's token colours, HTML in the sealed frame, show calls opened in it. Settings for the sign-in, the agent and its folders, notifications and updates; no admin pages. Layout on fixed and shared tracks; the sidebar is a drawer below 48rem, the viewer goes over the chat below 64rem. All 124 sentences in the server's nine languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.2 KiB
JavaScript
113 lines
4.2 KiB
JavaScript
// Before the app: which server, and the app's sign-in to it. A server older than LLeMbas 2.0 is
|
|
// refused here, with the reason Rust gave (src-tauri/src/server.rs, `check`). Signing in is the
|
|
// server's device login: the approval page opens in the system's browser; this page shows the
|
|
// code to compare and waits.
|
|
|
|
import { $, h, clear } from "./dom.js"
|
|
import { invoke, listen, why } from "./bridge.js"
|
|
|
|
let code = null // the code being approved, while signing in
|
|
let failed = ""
|
|
|
|
export function renderSetup(s) {
|
|
const box = clear($("setup-body"))
|
|
$("setup-version").textContent = s.version
|
|
const server = h("section", { "aria-labelledby": "setup-server-h" }, h("h2", { id: "setup-server-h", text: t("Server") }))
|
|
const address = h("input", {
|
|
id: "address",
|
|
name: "address",
|
|
type: "text",
|
|
inputmode: "url",
|
|
autocomplete: "url",
|
|
placeholder: "https://ai.example",
|
|
spellcheck: "false",
|
|
value: s.server || "",
|
|
})
|
|
const hint = h("p", { class: "hint", id: "server-hint", "aria-live": "polite" })
|
|
if (!s.checked) hint.textContent = t("Looking for a LLeMbas server there…")
|
|
else if (s.refused) {
|
|
hint.textContent = s.refused
|
|
hint.classList.add("is-error")
|
|
} else if (s.discovery) hint.textContent = t("%(name)s, LLeMbas %(version)s.", { name: s.discovery.name, version: s.discovery.version })
|
|
else hint.textContent = t("LLeMbas Desktop needs a LLeMbas 2.0 server or later.")
|
|
const connect = h("button", { type: "submit", class: "btn btn--primary", text: t("Connect") })
|
|
const form = h(
|
|
"form",
|
|
{ class: "field", novalidate: true },
|
|
h("label", { for: "address", text: t("Address of your LLeMbas server") }),
|
|
h("div", { class: "row" }, address, connect),
|
|
hint,
|
|
)
|
|
form.addEventListener("submit", async (e) => {
|
|
e.preventDefault()
|
|
connect.disabled = true
|
|
hint.className = "hint"
|
|
hint.textContent = t("Looking for a LLeMbas server there…")
|
|
try {
|
|
await invoke("set_server", { address: address.value })
|
|
} catch (err) {
|
|
hint.className = "hint is-error"
|
|
hint.textContent = why(err)
|
|
} finally {
|
|
connect.disabled = false
|
|
}
|
|
})
|
|
server.append(form)
|
|
if (s.refused && s.server) {
|
|
server.append(h("div", { class: "row--buttons" }, h("button", { type: "button", class: "btn", text: t("Try again"), onclick: () => invoke("recheck_server") })))
|
|
}
|
|
box.append(server)
|
|
|
|
if (s.discovery && !s.account) {
|
|
const sign = h("section", { "aria-labelledby": "setup-sign-h" }, h("h2", { id: "setup-sign-h", text: t("Sign in") }))
|
|
sign.append(h("p", { class: "muted", text: t("The app signs in to your server once, in your browser, as any device does. It may then read and write your chats, as the web interface does.") }))
|
|
if (s.signing_in && code) {
|
|
sign.append(
|
|
h("p", null, t("Approve this code in the page that opened in your browser:")),
|
|
h("p", { class: "code", text: code.user_code }),
|
|
h(
|
|
"div",
|
|
{ class: "row--buttons" },
|
|
h("button", { type: "button", class: "btn", text: t("Open the page again"), onclick: () => invoke("open_external", { url: code.verification_uri_complete }) }),
|
|
h("button", { type: "button", class: "btn", text: t("Cancel"), onclick: () => invoke("sign_in_cancel") }),
|
|
),
|
|
)
|
|
} else {
|
|
sign.append(
|
|
h(
|
|
"div",
|
|
{ class: "row--buttons" },
|
|
h("button", {
|
|
type: "button",
|
|
class: "btn btn--primary",
|
|
text: s.signing_in ? t("Waiting for the browser…") : t("Sign in"),
|
|
disabled: s.signing_in,
|
|
onclick: () => {
|
|
failed = ""
|
|
invoke("sign_in").catch((err) => {
|
|
failed = why(err)
|
|
renderSetup(s)
|
|
})
|
|
},
|
|
}),
|
|
),
|
|
)
|
|
}
|
|
if (failed) sign.append(h("p", { class: "status is-error", text: failed }))
|
|
box.append(sign)
|
|
}
|
|
}
|
|
|
|
export function initSetup(current) {
|
|
listen("signin", (e) => {
|
|
const p = e.payload
|
|
if (p.state === "code") code = p.code
|
|
if (p.state === "done") code = null
|
|
if (p.state === "failed") {
|
|
code = null
|
|
failed = p.message
|
|
}
|
|
renderSetup(current())
|
|
})
|
|
}
|