Draw a picture in a chat: force_tool image_generate on POST /chats and /messages
ci / ui (push) Waiting to run
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s

A switch among a chat's chips (not an agent chat's), offered where /me says
the server draws for the account, asks for the next message's reply to be a
picture; it goes off once the message is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 14:31:02 +00:00
1 parent df27ca82fa
commit 2cdd4e5355
5 files changed
+110 -2

No files matched your search

+3
View File
@@ -57,6 +57,9 @@ All notable changes to LLeMbas Desktop. The format follows
is away or does not accept scheduled work. A run waiting for its computer says so. The form is away or does not accept scheduled work. A run waiting for its computer says so. The form
offers "Where it runs" where the server can: this computer or another linked one that accepts offers "Where it runs" where the server can: this computer or another linked one that accepts
scheduled work, its folder (this computer's agent folder to begin with), mode and wait. scheduled work, its folder (this computer's agent folder to begin with), mode and wait.
- **A picture as the reply, in a chat.** Where the server draws for the account, a Draw a picture
switch beside the model asks for the next message's reply to be a picture drawn from it — in a
new chat (its first message) as in an open one. It goes off once the message is sent.
### Fixed ### Fixed
+10
View File
@@ -541,6 +541,8 @@ KEYS = (
"This computer (%(name)s)", "This computer (%(name)s)",
"Waiting for the computer", "Waiting for the computer",
"approvals wait %(n)s min", "approvals wait %(n)s min",
# chat.js: a picture as the next reply
"The reply to the next message is a picture drawn from it.",
) )
# The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the # The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the
@@ -820,6 +822,7 @@ OWN: dict[str, dict[str, str]] = {
"This computer (%(name)s)": "Tento počítač (%(name)s)", "This computer (%(name)s)": "Tento počítač (%(name)s)",
"Waiting for the computer": "Čeká na počítač", "Waiting for the computer": "Čeká na počítač",
"approvals wait %(n)s min": "schválení čeká %(n)s min", "approvals wait %(n)s min": "schválení čeká %(n)s min",
"The reply to the next message is a picture drawn from it.": "Odpovědí na další zprávu bude obrázek nakreslený podle ní.",
}, },
"de": { "de": {
"Sign in again: the server no longer accepts this app's sign-in.": "Melden Sie sich erneut an: Der Server nimmt die Anmeldung dieser App nicht mehr an.", "Sign in again: the server no longer accepts this app's sign-in.": "Melden Sie sich erneut an: Der Server nimmt die Anmeldung dieser App nicht mehr an.",
@@ -1094,6 +1097,7 @@ OWN: dict[str, dict[str, str]] = {
"This computer (%(name)s)": "Dieser Computer (%(name)s)", "This computer (%(name)s)": "Dieser Computer (%(name)s)",
"Waiting for the computer": "Wartet auf den Computer", "Waiting for the computer": "Wartet auf den Computer",
"approvals wait %(n)s min": "Freigaben warten %(n)s Min.", "approvals wait %(n)s min": "Freigaben warten %(n)s Min.",
"The reply to the next message is a picture drawn from it.": "Die Antwort auf die nächste Nachricht ist ein danach gezeichnetes Bild.",
}, },
"es": { "es": {
"Sign in again: the server no longer accepts this app's sign-in.": "Vuelva a iniciar sesión: el servidor ya no acepta el inicio de sesión de esta aplicación.", "Sign in again: the server no longer accepts this app's sign-in.": "Vuelva a iniciar sesión: el servidor ya no acepta el inicio de sesión de esta aplicación.",
@@ -1368,6 +1372,7 @@ OWN: dict[str, dict[str, str]] = {
"This computer (%(name)s)": "Este ordenador (%(name)s)", "This computer (%(name)s)": "Este ordenador (%(name)s)",
"Waiting for the computer": "Esperando al ordenador", "Waiting for the computer": "Esperando al ordenador",
"approvals wait %(n)s min": "las aprobaciones esperan %(n)s min", "approvals wait %(n)s min": "las aprobaciones esperan %(n)s min",
"The reply to the next message is a picture drawn from it.": "La respuesta al siguiente mensaje es una imagen dibujada a partir de él.",
}, },
"fr": { "fr": {
"Sign in again: the server no longer accepts this app's sign-in.": "Reconnectez-vous : le serveur n’accepte plus la connexion de cette application.", "Sign in again: the server no longer accepts this app's sign-in.": "Reconnectez-vous : le serveur n’accepte plus la connexion de cette application.",
@@ -1642,6 +1647,7 @@ OWN: dict[str, dict[str, str]] = {
"This computer (%(name)s)": "Cet ordinateur (%(name)s)", "This computer (%(name)s)": "Cet ordinateur (%(name)s)",
"Waiting for the computer": "En attente de l’ordinateur", "Waiting for the computer": "En attente de l’ordinateur",
"approvals wait %(n)s min": "les approbations attendent %(n)s min", "approvals wait %(n)s min": "les approbations attendent %(n)s min",
"The reply to the next message is a picture drawn from it.": "La réponse au prochain message est une image dessinée d’après lui.",
}, },
"hu": { "hu": {
"Sign in again: the server no longer accepts this app's sign-in.": "Jelentkezzen be újra: a szerver már nem fogadja el ennek az alkalmazásnak a bejelentkezését.", "Sign in again: the server no longer accepts this app's sign-in.": "Jelentkezzen be újra: a szerver már nem fogadja el ennek az alkalmazásnak a bejelentkezését.",
@@ -1916,6 +1922,7 @@ OWN: dict[str, dict[str, str]] = {
"This computer (%(name)s)": "Ez a számítógép (%(name)s)", "This computer (%(name)s)": "Ez a számítógép (%(name)s)",
"Waiting for the computer": "A számítógépre vár", "Waiting for the computer": "A számítógépre vár",
"approvals wait %(n)s min": "a jóváhagyások %(n)s percig várnak", "approvals wait %(n)s min": "a jóváhagyások %(n)s percig várnak",
"The reply to the next message is a picture drawn from it.": "A következő üzenetre adott válasz egy abból rajzolt kép lesz.",
}, },
"it": { "it": {
"Sign in again: the server no longer accepts this app's sign-in.": "Accedi di nuovo: il server non accetta più l’accesso di questa app.", "Sign in again: the server no longer accepts this app's sign-in.": "Accedi di nuovo: il server non accetta più l’accesso di questa app.",
@@ -2190,6 +2197,7 @@ OWN: dict[str, dict[str, str]] = {
"This computer (%(name)s)": "Questo computer (%(name)s)", "This computer (%(name)s)": "Questo computer (%(name)s)",
"Waiting for the computer": "In attesa del computer", "Waiting for the computer": "In attesa del computer",
"approvals wait %(n)s min": "le approvazioni attendono %(n)s min", "approvals wait %(n)s min": "le approvazioni attendono %(n)s min",
"The reply to the next message is a picture drawn from it.": "La risposta al prossimo messaggio è un’immagine disegnata a partire da esso.",
}, },
"pl": { "pl": {
"Sign in again: the server no longer accepts this app's sign-in.": "Zaloguj się ponownie: serwer nie przyjmuje już logowania tej aplikacji.", "Sign in again: the server no longer accepts this app's sign-in.": "Zaloguj się ponownie: serwer nie przyjmuje już logowania tej aplikacji.",
@@ -2464,6 +2472,7 @@ OWN: dict[str, dict[str, str]] = {
"This computer (%(name)s)": "Ten komputer (%(name)s)", "This computer (%(name)s)": "Ten komputer (%(name)s)",
"Waiting for the computer": "Czeka na komputer", "Waiting for the computer": "Czeka na komputer",
"approvals wait %(n)s min": "zatwierdzenia czekają %(n)s min", "approvals wait %(n)s min": "zatwierdzenia czekają %(n)s min",
"The reply to the next message is a picture drawn from it.": "Odpowiedzią na następną wiadomość będzie obraz narysowany na jej podstawie.",
}, },
"sk": { "sk": {
"Sign in again: the server no longer accepts this app's sign-in.": "Prihláste sa znova: server už neprijíma prihlásenie tejto aplikácie.", "Sign in again: the server no longer accepts this app's sign-in.": "Prihláste sa znova: server už neprijíma prihlásenie tejto aplikácie.",
@@ -2738,6 +2747,7 @@ OWN: dict[str, dict[str, str]] = {
"This computer (%(name)s)": "Tento počítač (%(name)s)", "This computer (%(name)s)": "Tento počítač (%(name)s)",
"Waiting for the computer": "Čaká na počítač", "Waiting for the computer": "Čaká na počítač",
"approvals wait %(n)s min": "schválenia čakajú %(n)s min", "approvals wait %(n)s min": "schválenia čakajú %(n)s min",
"The reply to the next message is a picture drawn from it.": "Odpoveďou na ďalšiu správu bude obrázok nakreslený podľa nej.",
}, },
} }
+39
View File
@@ -1682,6 +1682,45 @@ test("L8: at 390 px the form with a computer chosen stays inside the window", as
assert.deepEqual(p.errors, []) assert.deepEqual(p.errors, [])
}) })
// M8: a picture as the reply (force_tool "image_generate" on POST /chats and /messages).
test("M8: Draw a picture asks for a picture with the next message only, in a new chat and an open one", async () => {
const cfg = i8()
Object.assign(cfg.routes, {
"POST /chats": { chat: chatRow({ id: "chat7" }), message: userMessage("m1", "a lembas loaf"), reply: null },
"POST /chats/chat7/messages": { message: userMessage("m2", "and another"), reply: null },
})
const p = await page(cfg)
await p.waitForFunction(() => window.__mock.api("GET", "/me").length === 1)
await p.click(".kind__tab >> text=Chat")
await p.click(".cchip--picture")
assert.equal(await p.getAttribute(".cchip--picture", "aria-pressed"), "true")
await shot(p, "m8-picture-1280")
await p.fill("#composer-text", "a lembas loaf")
await p.keyboard.press("Enter")
await p.waitForFunction(() => window.__mock.api("POST", "/chats").length === 1)
assert.equal((await p.evaluate(() => window.__mock.api("POST", "/chats")))[0].args.body.force_tool, "image_generate")
await p.waitForSelector('.cchip--picture[aria-pressed="false"]')
await p.fill("#composer-text", "and another")
await p.keyboard.press("Enter")
await p.waitForFunction(() => window.__mock.api("POST", "/chats/chat7/messages").length === 1)
assert.equal((await p.evaluate(() => window.__mock.api("POST", "/chats/chat7/messages")))[0].args.body.force_tool, undefined)
assert.deepEqual(p.errors, [])
})
test("M8: no picture switch where the server cannot draw, or in an agent chat", async () => {
const p = await page(i8({ images: false }))
await p.waitForFunction(() => window.__mock.api("GET", "/me").length === 1)
await p.click(".kind__tab >> text=Chat")
assert.equal(await p.locator(".cchip--picture").count(), 0)
await p.context().close()
const q = await page(i8())
await q.waitForFunction(() => window.__mock.api("GET", "/me").length === 1)
await q.click(".kind__tab >> text=Agent")
assert.equal(await q.locator(".cchip--picture").count(), 0)
assert.deepEqual(q.errors, [])
})
// --- Run ---------------------------------------------------------------------------------------- // --- Run ----------------------------------------------------------------------------------------
const only = process.argv[2] || "" const only = process.argv[2] || ""
+8
View File
@@ -377,6 +377,7 @@ const LEMBAS_I18N = {
"The previous command (Ctrl+Shift+↑)": "Předchozí příkaz (Ctrl+Shift+↑)", "The previous command (Ctrl+Shift+↑)": "Předchozí příkaz (Ctrl+Shift+↑)",
"The release that goes with the server's version. This is %(current)s.": "Vydání, které patří k verzi serveru. Toto je %(current)s.", "The release that goes with the server's version. This is %(current)s.": "Vydání, které patří k verzi serveru. Toto je %(current)s.",
"The reply is ready.": "Odpověď je hotová.", "The reply is ready.": "Odpověď je hotová.",
"The reply to the next message is a picture drawn from it.": "Odpovědí na další zprávu bude obrázek nakreslený podle ní.",
"The result is no longer there.": "Výsledek už tu není.", "The result is no longer there.": "Výsledek už tu není.",
"The server refused this computer's link: %(why)s": "Server odmítl propojení tohoto počítače: %(why)s", "The server refused this computer's link: %(why)s": "Server odmítl propojení tohoto počítače: %(why)s",
"The server's answer had no picture in it.": "V odpovědi serveru nebyl žádný obrázek.", "The server's answer had no picture in it.": "V odpovědi serveru nebyl žádný obrázek.",
@@ -861,6 +862,7 @@ const LEMBAS_I18N = {
"The previous command (Ctrl+Shift+↑)": "Vorheriger Befehl (Ctrl+Shift+↑)", "The previous command (Ctrl+Shift+↑)": "Vorheriger Befehl (Ctrl+Shift+↑)",
"The release that goes with the server's version. This is %(current)s.": "Die Version, die zur Version des Servers passt. Dies ist %(current)s.", "The release that goes with the server's version. This is %(current)s.": "Die Version, die zur Version des Servers passt. Dies ist %(current)s.",
"The reply is ready.": "Die Antwort ist fertig.", "The reply is ready.": "Die Antwort ist fertig.",
"The reply to the next message is a picture drawn from it.": "Die Antwort auf die nächste Nachricht ist ein danach gezeichnetes Bild.",
"The result is no longer there.": "Das Ergebnis ist nicht mehr da.", "The result is no longer there.": "Das Ergebnis ist nicht mehr da.",
"The server refused this computer's link: %(why)s": "Der Server hat die Verknüpfung dieses Computers abgelehnt: %(why)s", "The server refused this computer's link: %(why)s": "Der Server hat die Verknüpfung dieses Computers abgelehnt: %(why)s",
"The server's answer had no picture in it.": "Die Antwort des Servers enthielt kein Bild.", "The server's answer had no picture in it.": "Die Antwort des Servers enthielt kein Bild.",
@@ -1345,6 +1347,7 @@ const LEMBAS_I18N = {
"The previous command (Ctrl+Shift+↑)": "El comando anterior (Ctrl+Shift+↑)", "The previous command (Ctrl+Shift+↑)": "El comando anterior (Ctrl+Shift+↑)",
"The release that goes with the server's version. This is %(current)s.": "La versión que corresponde a la del servidor. Esta es %(current)s.", "The release that goes with the server's version. This is %(current)s.": "La versión que corresponde a la del servidor. Esta es %(current)s.",
"The reply is ready.": "La respuesta está lista.", "The reply is ready.": "La respuesta está lista.",
"The reply to the next message is a picture drawn from it.": "La respuesta al siguiente mensaje es una imagen dibujada a partir de él.",
"The result is no longer there.": "El resultado ya no está.", "The result is no longer there.": "El resultado ya no está.",
"The server refused this computer's link: %(why)s": "El servidor rechazó la vinculación de este equipo: %(why)s", "The server refused this computer's link: %(why)s": "El servidor rechazó la vinculación de este equipo: %(why)s",
"The server's answer had no picture in it.": "La respuesta del servidor no traía ninguna imagen.", "The server's answer had no picture in it.": "La respuesta del servidor no traía ninguna imagen.",
@@ -1829,6 +1832,7 @@ const LEMBAS_I18N = {
"The previous command (Ctrl+Shift+↑)": "La commande précédente (Ctrl+Shift+↑)", "The previous command (Ctrl+Shift+↑)": "La commande précédente (Ctrl+Shift+↑)",
"The release that goes with the server's version. This is %(current)s.": "La version qui va avec celle du serveur. Celle-ci est la %(current)s.", "The release that goes with the server's version. This is %(current)s.": "La version qui va avec celle du serveur. Celle-ci est la %(current)s.",
"The reply is ready.": "La réponse est prête.", "The reply is ready.": "La réponse est prête.",
"The reply to the next message is a picture drawn from it.": "La réponse au prochain message est une image dessinée d’après lui.",
"The result is no longer there.": "Le résultat n'est plus là.", "The result is no longer there.": "Le résultat n'est plus là.",
"The server refused this computer's link: %(why)s": "Le serveur a refusé la liaison de cet ordinateur : %(why)s", "The server refused this computer's link: %(why)s": "Le serveur a refusé la liaison de cet ordinateur : %(why)s",
"The server's answer had no picture in it.": "La réponse du serveur ne contenait aucune image.", "The server's answer had no picture in it.": "La réponse du serveur ne contenait aucune image.",
@@ -2313,6 +2317,7 @@ const LEMBAS_I18N = {
"The previous command (Ctrl+Shift+↑)": "Előző parancs (Ctrl+Shift+↑)", "The previous command (Ctrl+Shift+↑)": "Előző parancs (Ctrl+Shift+↑)",
"The release that goes with the server's version. This is %(current)s.": "A szerver verziójához tartozó kiadás. Ez a %(current)s.", "The release that goes with the server's version. This is %(current)s.": "A szerver verziójához tartozó kiadás. Ez a %(current)s.",
"The reply is ready.": "A válasz kész.", "The reply is ready.": "A válasz kész.",
"The reply to the next message is a picture drawn from it.": "A következő üzenetre adott válasz egy abból rajzolt kép lesz.",
"The result is no longer there.": "Az eredmény már nincs meg.", "The result is no longer there.": "Az eredmény már nincs meg.",
"The server refused this computer's link: %(why)s": "A szerver elutasította ennek a számítógépnek a kapcsolatát: %(why)s", "The server refused this computer's link: %(why)s": "A szerver elutasította ennek a számítógépnek a kapcsolatát: %(why)s",
"The server's answer had no picture in it.": "A szerver válaszában nem volt kép.", "The server's answer had no picture in it.": "A szerver válaszában nem volt kép.",
@@ -2797,6 +2802,7 @@ const LEMBAS_I18N = {
"The previous command (Ctrl+Shift+↑)": "Il comando precedente (Ctrl+Shift+↑)", "The previous command (Ctrl+Shift+↑)": "Il comando precedente (Ctrl+Shift+↑)",
"The release that goes with the server's version. This is %(current)s.": "La versione che va con quella del server. Questa è la %(current)s.", "The release that goes with the server's version. This is %(current)s.": "La versione che va con quella del server. Questa è la %(current)s.",
"The reply is ready.": "La risposta è pronta.", "The reply is ready.": "La risposta è pronta.",
"The reply to the next message is a picture drawn from it.": "La risposta al prossimo messaggio è un’immagine disegnata a partire da esso.",
"The result is no longer there.": "Il risultato non c’è più.", "The result is no longer there.": "Il risultato non c’è più.",
"The server refused this computer's link: %(why)s": "Il server ha rifiutato il collegamento di questo computer: %(why)s", "The server refused this computer's link: %(why)s": "Il server ha rifiutato il collegamento di questo computer: %(why)s",
"The server's answer had no picture in it.": "La risposta del server non conteneva immagini.", "The server's answer had no picture in it.": "La risposta del server non conteneva immagini.",
@@ -3281,6 +3287,7 @@ const LEMBAS_I18N = {
"The previous command (Ctrl+Shift+↑)": "Poprzednie polecenie (Ctrl+Shift+↑)", "The previous command (Ctrl+Shift+↑)": "Poprzednie polecenie (Ctrl+Shift+↑)",
"The release that goes with the server's version. This is %(current)s.": "Wydanie pasujące do wersji serwera. To jest %(current)s.", "The release that goes with the server's version. This is %(current)s.": "Wydanie pasujące do wersji serwera. To jest %(current)s.",
"The reply is ready.": "Odpowiedź jest gotowa.", "The reply is ready.": "Odpowiedź jest gotowa.",
"The reply to the next message is a picture drawn from it.": "Odpowiedzią na następną wiadomość będzie obraz narysowany na jej podstawie.",
"The result is no longer there.": "Wyniku już nie ma.", "The result is no longer there.": "Wyniku już nie ma.",
"The server refused this computer's link: %(why)s": "Serwer odrzucił połączenie tego komputera: %(why)s", "The server refused this computer's link: %(why)s": "Serwer odrzucił połączenie tego komputera: %(why)s",
"The server's answer had no picture in it.": "W odpowiedzi serwera nie było obrazu.", "The server's answer had no picture in it.": "W odpowiedzi serwera nie było obrazu.",
@@ -3765,6 +3772,7 @@ const LEMBAS_I18N = {
"The previous command (Ctrl+Shift+↑)": "Predchádzajúci príkaz (Ctrl+Shift+↑)", "The previous command (Ctrl+Shift+↑)": "Predchádzajúci príkaz (Ctrl+Shift+↑)",
"The release that goes with the server's version. This is %(current)s.": "Vydanie, ktoré patrí k verzii servera. Toto je %(current)s.", "The release that goes with the server's version. This is %(current)s.": "Vydanie, ktoré patrí k verzii servera. Toto je %(current)s.",
"The reply is ready.": "Odpoveď je hotová.", "The reply is ready.": "Odpoveď je hotová.",
"The reply to the next message is a picture drawn from it.": "Odpoveďou na ďalšiu správu bude obrázok nakreslený podľa nej.",
"The result is no longer there.": "Výsledok už tu nie je.", "The result is no longer there.": "Výsledok už tu nie je.",
"The server refused this computer's link: %(why)s": "Server odmietol prepojenie tohto počítača: %(why)s", "The server refused this computer's link: %(why)s": "Server odmietol prepojenie tohto počítača: %(why)s",
"The server's answer had no picture in it.": "V odpovedi servera nebol žiadny obrázok.", "The server's answer had no picture in it.": "V odpovedi servera nebol žiadny obrázok.",
+50 -2
View File
@@ -789,6 +789,47 @@ const MODE_WORDS = {
plan: () => t("Plan — changes nothing"), plan: () => t("Plan — changes nothing"),
} }
// --- Draw a picture: the next message's reply is a picture (`force_tool: "image_generate"`) -------
let wantPicture = false // the next message, in a chat (not an agent chat), asks for a picture
/** Whether a chat here may be asked for a picture: the server draws for this account. */
function picturesOffered() {
const me = ctx.me()
return !!me?.services?.images && me?.permissions?.["tools.image"] !== false
}
function pictureChip() {
const b = chip({
ico: ICONS.picture,
label: t("Draw a picture"),
title: t("The reply to the next message is a picture drawn from it."),
menuish: false,
cls: "cchip--picture",
onclick: () => {
wantPicture = !wantPicture
b.setAttribute("aria-pressed", String(wantPicture))
$("composer-text").placeholder = wantPicture ? t("What to draw…") : t("Message")
$("composer-text").focus()
},
})
b.setAttribute("aria-pressed", String(wantPicture))
return b
}
/** The message's body, asking for a picture while the switch is on. */
function takePicture(body) {
return wantPicture ? { ...body, force_tool: "image_generate" } : body
}
/** The message went: the switch goes off with it. */
function pictureSent() {
if (!wantPicture) return
wantPicture = false
$("composer-text").placeholder = t("Message")
document.querySelector(".cchip--picture")?.setAttribute("aria-pressed", "false")
}
function modeChip(value, onChange, disabled) { function modeChip(value, onChange, disabled) {
return chip({ return chip({
ico: ICONS.shield, ico: ICONS.shield,
@@ -953,6 +994,8 @@ async function renderChips() {
renderChips() renderChips()
}, mayPick)) }, mayPick))
if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips()))) if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips())))
if (draft.kind === "chat" && picturesOffered()) parts.push(pictureChip())
else wantPicture = false
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), { cap: subagentsCap(), own: namedOnly(model?.subagents) })) if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), { cap: subagentsCap(), own: namedOnly(model?.subagents) }))
// An agent chat's subagents run on its device: offered as the server says for that device. // An agent chat's subagents run on its device: offered as the server says for that device.
const device = draft.kind === "agent" ? draftDevice() : "" const device = draft.kind === "agent" ? draftDevice() : ""
@@ -998,6 +1041,8 @@ async function renderChips() {
if (chat.kind === "agent") parts.push(modeChip(chat.mode || "edit", (m) => save({ mode: m }))) if (chat.kind === "agent") parts.push(modeChip(chat.mode || "edit", (m) => save({ mode: m })))
if (chat.model) parts.push(modelChip(models, chat.model, (id) => save({ model: id }), mayPick)) if (chat.model) parts.push(modelChip(models, chat.model, (id) => save({ model: id }), mayPick))
parts.push(effortChip(current, chat.effort || levelsOf(current).start, (l) => save({ effort: l }))) parts.push(effortChip(current, chat.effort || levelsOf(current).start, (l) => save({ effort: l })))
if (chat.kind === "chat" && picturesOffered()) parts.push(pictureChip())
else wantPicture = false
box.replaceChildren(...parts.filter(Boolean)) box.replaceChildren(...parts.filter(Boolean))
const offer = await teamOffered(chat) const offer = await teamOffered(chat)
if (!offer || drawing !== chipsDrawn || open?.chat?.id !== chat.id) return if (!offer || drawing !== chipsDrawn || open?.chat?.id !== chat.id) return
@@ -1368,7 +1413,8 @@ async function createChat(text, attachments) {
} }
Object.assign(body, { kind: "agent", device, directory: draft.dir, mode: draft.mode || "edit" }) Object.assign(body, { kind: "agent", device, directory: draft.dir, mode: draft.mode || "edit" })
} }
const r = await post("/chats", body) const r = await post("/chats", draft.kind === "chat" ? takePicture(body) : body)
pictureSent()
draft = null draft = null
$("new-chat").hidden = true $("new-chat").hidden = true
$("main").dataset.new = "false" $("main").dataset.new = "false"
@@ -1405,9 +1451,11 @@ async function submit() {
try { try {
if (draft) await createChat(text, files) if (draft) await createChat(text, files)
else if (open?.chat?.id) { else if (open?.chat?.id) {
const body = { text } let body = { text }
if (files.length) body.attachments = files if (files.length) body.attachments = files
if (open.chat.kind === "chat") body = takePicture(body)
const r = await post(`/chats/${encodeURIComponent(open.chat.id)}/messages`, body) const r = await post(`/chats/${encodeURIComponent(open.chat.id)}/messages`, body)
pictureSent()
if (r.message) $("messages").append(userMessage(r.message)) if (r.message) $("messages").append(userMessage(r.message))
if (r.reply) follower(open.chat, r.reply.id) if (r.reply) follower(open.chat, r.reply.id)
} else return } else return