diff --git a/apps/api/src/media-library.ts b/apps/api/src/media-library.ts new file mode 100644 index 0000000..3d3c9b6 --- /dev/null +++ b/apps/api/src/media-library.ts @@ -0,0 +1,82 @@ +import path from "node:path"; +import { ORPCError } from "@orpc/server"; +import type { JobPublisher } from "@rakazo/adapter-kit"; +import { mediaLibrarySyncJob } from "@rakazo/adapter-kit"; +import { checkMediaLibraryFolder, mediaLibrarySourceView } from "@rakazo/adapters"; +import type { Actor, MediaLibrarySourceAddInput } from "@rakazo/contracts"; +import type { PrismaClient } from "@rakazo/db"; + +export interface MediaLibraryRouteDeps { + prisma: PrismaClient; + jobs: JobPublisher; +} + +/** Library folders are paths on the server, so only its owner adds them, like host folders. */ +function requireDeploymentOwner(actor: Actor) { + if (!actor.isDeploymentOwner) throw new ORPCError("FORBIDDEN"); +} + +async function ownSource(deps: MediaLibraryRouteDeps, actor: Actor, sourceId: string) { + const source = await deps.prisma.mediaLibrarySource.findFirst({ + where: { id: sourceId, userId: actor.userId }, + }); + if (!source) throw new ORPCError("NOT_FOUND"); + return source; +} + +export async function listMediaLibrarySources(deps: MediaLibraryRouteDeps, actor: Actor) { + requireDeploymentOwner(actor); + const rows = await deps.prisma.mediaLibrarySource.findMany({ + where: { userId: actor.userId }, + orderBy: { createdAt: "asc" }, + }); + return rows.map(mediaLibrarySourceView); +} + +export async function addMediaLibrarySource( + deps: MediaLibraryRouteDeps, + actor: Actor, + input: MediaLibrarySourceAddInput, +) { + requireDeploymentOwner(actor); + const problem = await checkMediaLibraryFolder(input.path); + if (problem) throw new ORPCError("BAD_REQUEST", { message: problem }); + const folder = path.resolve(input.path); + const existing = await deps.prisma.mediaLibrarySource.findUnique({ + where: { userId_path: { userId: actor.userId, path: folder } }, + }); + if (existing) return mediaLibrarySourceView(existing); + const source = await deps.prisma.mediaLibrarySource.create({ + data: { userId: actor.userId, path: folder, status: "syncing" }, + }); + await deps.jobs.enqueue(mediaLibrarySyncJob(source.id)); + return mediaLibrarySourceView(source); +} + +export async function removeMediaLibrarySource( + deps: MediaLibraryRouteDeps, + actor: Actor, + sourceId: string, +) { + requireDeploymentOwner(actor); + await ownSource(deps, actor, sourceId); + await deps.prisma.mediaLibrarySource.deleteMany({ + where: { id: sourceId, userId: actor.userId }, + }); + return { ok: true as const }; +} + +export async function syncMediaLibrarySourceNow( + deps: MediaLibraryRouteDeps, + actor: Actor, + sourceId: string, +) { + requireDeploymentOwner(actor); + await ownSource(deps, actor, sourceId); + const source = await deps.prisma.mediaLibrarySource.update({ + where: { id: sourceId }, + data: { status: "syncing", error: null }, + }); + await deps.jobs.enqueue(mediaLibrarySyncJob(sourceId)); + return mediaLibrarySourceView(source); +} diff --git a/apps/api/src/router.ts b/apps/api/src/router.ts index 6fc50df..0c19853 100644 --- a/apps/api/src/router.ts +++ b/apps/api/src/router.ts @@ -205,6 +205,12 @@ import { startMediaJob, updateMediaSettings, } from "./media-jobs.js"; +import { + addMediaLibrarySource, + listMediaLibrarySources, + removeMediaLibrarySource, + syncMediaLibrarySourceNow, +} from "./media-library.js"; import { disconnectMemoryProvider, getMemoryProviderConfig, @@ -3797,6 +3803,20 @@ export function createRouter(deps: RouterDeps) { cancelMediaJob(deps, context.actor, input.jobId), ), }, + library: { + list: authed.media.library.list.handler(({ context }) => + listMediaLibrarySources(deps, context.actor), + ), + add: authed.media.library.add.handler(({ context, input }) => + addMediaLibrarySource(deps, context.actor, input), + ), + remove: authed.media.library.remove.handler(({ context, input }) => + removeMediaLibrarySource(deps, context.actor, input.sourceId), + ), + sync: authed.media.library.sync.handler(({ context, input }) => + syncMediaLibrarySourceNow(deps, context.actor, input.sourceId), + ), + }, }, connections: { catalog: authed.connections.catalog.handler(async ({ context, input }) => { diff --git a/apps/web/e2e/media-settings.spec.ts b/apps/web/e2e/media-settings.spec.ts index bf606b7..b579236 100644 --- a/apps/web/e2e/media-settings.spec.ts +++ b/apps/web/e2e/media-settings.spec.ts @@ -37,6 +37,68 @@ function view(draft: Draft, previous: { comfyui: boolean; transcription: boolean }; } +type Source = { + id: string; + path: string; + status: "idle" | "syncing" | "failed"; + error: string | null; + assetCount: number; + lastSyncedAt: string | null; +}; + +/** Library folders on the fake server: a new folder indexes on its next list. */ +async function fakeLibrary(page: Page, initial: Source[] = []) { + let sources = initial; + const calls: { proc: string; input: unknown }[] = []; + await page.route("**/rpc/media/library/**", (route) => { + const proc = new URL(route.request().url()).pathname.split("/").at(-1) ?? ""; + const input = (route.request().postDataJSON() as { json?: unknown } | null)?.json; + calls.push({ proc, input }); + if (proc === "list") { + const listed = sources; + sources = sources.map((source) => + source.status === "syncing" + ? { ...source, status: "idle", assetCount: 1284, lastSyncedAt: new Date().toISOString() } + : source, + ); + return route.fulfill({ json: { json: listed } }); + } + if (proc === "add") { + const { path } = input as { path: string }; + if (!path.startsWith("/")) { + return route.fulfill({ + status: 400, + json: { + json: { + defined: false, + code: "BAD_REQUEST", + status: 400, + message: "Use an absolute folder path.", + }, + }, + }); + } + const source: Source = { + id: `source-${sources.length + 1}`, + path, + status: "syncing", + error: null, + assetCount: 0, + lastSyncedAt: null, + }; + sources = [...sources, source]; + return route.fulfill({ json: { json: source } }); + } + if (proc === "remove") { + const { sourceId } = input as { sourceId: string }; + sources = sources.filter((source) => source.id !== sourceId); + return route.fulfill({ json: { json: { ok: true } } }); + } + return route.fallback(); + }); + return calls; +} + async function asDeploymentOwner(page: Page, isDeploymentOwner: boolean) { await page.route("**/rpc/bootstrap", async (route) => { const response = await route.fetch(); @@ -50,6 +112,7 @@ async function asDeploymentOwner(page: Page, isDeploymentOwner: boolean) { test("the deployment owner points media at their own servers", async ({ page }, testInfo) => { await asDeploymentOwner(page, true); + await fakeLibrary(page); let current: ReturnType = { comfyui: null, transcription: null }; const saved: Draft[] = []; await page.route("**/rpc/media/settings/get", (route) => @@ -113,6 +176,55 @@ test("the deployment owner points media at their own servers", async ({ page }, await expect(comfyKey).toHaveAttribute("placeholder", "Optional"); }); +test("the deployment owner adds a photo folder to the library", async ({ page }, testInfo) => { + await asDeploymentOwner(page, true); + const calls = await fakeLibrary(page, [ + { + id: "source-0", + path: "/srv/media/Videos", + status: "failed", + error: "Folder not found.", + assetCount: 0, + lastSyncedAt: null, + }, + ]); + await page.route("**/rpc/media/settings/get", (route) => + route.fulfill({ json: { json: { comfyui: null, transcription: null } } }), + ); + + await signup(page, `media-library-${Date.now()}@rakazo.test`, "password12", "Library Owner"); + await completeOnboarding(page); + const settings = await openUserSettings(page); + await settings.getByTestId("settings-nav-media").click(); + const library = settings.getByTestId("media-library"); + await expect(library.getByText("Folder not found.")).toBeVisible(); + + const folder = library.getByLabel("Folder", { exact: true }); + await folder.fill("photos"); + await library.getByRole("button", { name: "Add", exact: true }).click(); + await expect(library.getByRole("alert")).toHaveText("Use an absolute folder path."); + + await folder.fill("/srv/media/Photos"); + await library.getByRole("button", { name: "Add", exact: true }).click(); + await expect(folder).toHaveValue(""); + await expect(library.getByText("/srv/media/Photos")).toBeVisible(); + // The new folder indexes in the background; the list catches up on its own. + await expect(library.getByText("1,284 files")).toBeVisible({ timeout: 10_000 }); + await captureScreenshot(page, testInfo, "media-library"); + + await library + .getByRole("listitem") + .filter({ hasText: "/srv/media/Videos" }) + .getByRole("button", { name: "Remove folder" }) + .click(); + await expect(library.getByText("/srv/media/Videos")).toHaveCount(0); + expect(calls.filter((call) => call.proc !== "list")).toEqual([ + { proc: "add", input: { path: "photos" } }, + { proc: "add", input: { path: "/srv/media/Photos" } }, + { proc: "remove", input: { sourceId: "source-0" } }, + ]); +}); + test("members who do not own the deployment have no media settings", async ({ page }) => { await asDeploymentOwner(page, false); await signup(page, `media-member-${Date.now()}@rakazo.test`, "password12", "Media Member"); diff --git a/apps/web/e2e/mobile-font-size.spec.ts b/apps/web/e2e/mobile-font-size.spec.ts new file mode 100644 index 0000000..759b6af --- /dev/null +++ b/apps/web/e2e/mobile-font-size.spec.ts @@ -0,0 +1,41 @@ +import { expect, type Locator, test } from "@playwright/test"; +import { captureScreenshot, completeOnboarding, signup } from "./helpers"; + +async function fontSize(locator: Locator) { + return locator.evaluate((element) => Number.parseFloat(getComputedStyle(element).fontSize)); +} + +test("phone widths read text at 16px and up while desktop keeps its sizes", async ({ + page, +}, testInfo) => { + const stamp = Date.now(); + await signup(page, `mobile-font-${stamp}@rakazo.test`, "password12", "Font QA"); + await completeOnboarding(page); + + const composer = page.getByRole("combobox", { name: /^Message/ }); + await composer.fill("Please review `shared/PROJECT_CHECKPOINT_WRAPUP.md`."); + await composer.press("Enter"); + const reply = page + .getByTestId("transcript") + .locator("[data-message-id]") + .filter({ hasText: "done. i handled:" }); + await expect(reply).toBeVisible({ timeout: 30_000 }); + + const smallText = page.locator(".text-ui-body-sm").first(); + const desktopSmall = await fontSize(smallText); + expect(desktopSmall).toBe(14); + + await page.setViewportSize({ width: 390, height: 844 }); + await expect.poll(() => fontSize(smallText)).toBeCloseTo(16, 1); + expect(await fontSize(composer)).toBeGreaterThanOrEqual(16); + + await page.emulateMedia({ colorScheme: "dark" }); + await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); + await captureScreenshot(page, testInfo, "mobile-font-size-dark"); + await page.emulateMedia({ colorScheme: "light" }); + await expect(page.locator("html")).toHaveAttribute("data-theme", "light"); + await captureScreenshot(page, testInfo, "mobile-font-size-light"); + + await page.setViewportSize({ width: 1280, height: 720 }); + await expect.poll(() => fontSize(smallText)).toBe(desktopSmall); +}); diff --git a/apps/web/src/components/MediaLibraryFolders.tsx b/apps/web/src/components/MediaLibraryFolders.tsx new file mode 100644 index 0000000..7878daf --- /dev/null +++ b/apps/web/src/components/MediaLibraryFolders.tsx @@ -0,0 +1,162 @@ +import { Plural, Trans, useLingui } from "@lingui/react/macro"; +import type { MediaLibrarySource } from "@rakazo/contracts"; +import { Button, Input } from "@rakazo/ui-web"; +import { RefreshCw, X } from "lucide-react"; +import { useEffect, useId, useState } from "react"; +import { rpc } from "../lib/rpc"; +import { Shimmer } from "./ai/primitives"; + +const POLL_MS = 2000; + +/** Folders on the server whose photos and videos the owner's bots can search and use. */ +export function MediaLibraryFolders() { + const { t } = useLingui(); + const id = useId(); + const [sources, setSources] = useState(null); + const [folder, setFolder] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + rpc.media.library + .list() + .then((next) => { + if (!cancelled) setSources(next); + }) + .catch((err: unknown) => { + if (!cancelled) setError(err instanceof Error ? err.message : t`Couldn't load`); + }); + return () => { + cancelled = true; + }; + }, [t]); + + const syncing = sources?.some((source) => source.status === "syncing") ?? false; + useEffect(() => { + if (!syncing) return; + const timer = setInterval(() => { + rpc.media.library + .list() + .then(setSources) + .catch(() => undefined); + }, POLL_MS); + return () => clearInterval(timer); + }, [syncing]); + + async function act(action: () => Promise) { + if (busy) return; + setBusy(true); + setError(null); + try { + await action(); + } catch (err) { + setError(err instanceof Error ? err.message : t`Couldn't save`); + } finally { + setBusy(false); + } + } + + function replace(next: MediaLibrarySource) { + setSources((current) => [...(current ?? []).filter((source) => source.id !== next.id), next]); + } + + const disabled = busy || sources === null; + + return ( +
+

+ Library +

+ {sources?.length ? ( +
    + {sources.map((source) => ( +
  • +
    +

    {source.path}

    + {source.status === "syncing" ? ( +

    + + Indexing… + +

    + ) : source.status === "failed" ? ( +

    {source.error}

    + ) : ( +

    + +

    + )} +
    + + +
  • + ))} +
+ ) : null} +
{ + event.preventDefault(); + const path = folder.trim(); + if (!path) return; + void act(async () => { + replace(await rpc.media.library.add({ path })); + setFolder(""); + }); + }} + > + setFolder(event.target.value)} + /> + +
+ {error ? ( +

+ {error} +

+ ) : null} +
+ ); +} diff --git a/apps/web/src/components/MediaSettings.tsx b/apps/web/src/components/MediaSettings.tsx index 2ff24d2..0e667e1 100644 --- a/apps/web/src/components/MediaSettings.tsx +++ b/apps/web/src/components/MediaSettings.tsx @@ -4,6 +4,7 @@ import { Button, Field, FieldLabel, Input, Textarea } from "@rakazo/ui-web"; import { useEffect, useId, useRef, useState } from "react"; import { rpc } from "../lib/rpc"; import { SuccessPop } from "./ai/primitives"; +import { MediaLibraryFolders } from "./MediaLibraryFolders"; type KeyDraft = { value: string; remove: boolean }; @@ -67,7 +68,9 @@ function ApiKeyField({ ); } -/** The deployment owner's media servers: ComfyUI for generation, a transcription server. */ +/** + * The deployment owner's media: library folders, ComfyUI for generation, a transcription server. + */ export function MediaSettings() { const { t } = useLingui(); const ids = useId(); @@ -152,6 +155,7 @@ export function MediaSettings() { return (
+

ComfyUI

diff --git a/apps/web/src/components/stage/OoStage.tsx b/apps/web/src/components/stage/OoStage.tsx index e30c59d..c6b5d23 100644 --- a/apps/web/src/components/stage/OoStage.tsx +++ b/apps/web/src/components/stage/OoStage.tsx @@ -251,7 +251,7 @@ export function OoStage({ ) : null} ) : ( -

+

{subtitle}

)} @@ -548,7 +548,7 @@ export function OoGroupStage({ {subtitle}

) : ( -

+

{subtitle}

)} diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index 475ab5f..cd2bc38 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -27,6 +27,11 @@ msgstr "(optional)" msgid "{0, plural, one {# day} other {# days}}" msgstr "{0, plural, one {# Tag} other {# Tage}}" +#. placeholder {0}: source.assetCount +#: src/components/MediaLibraryFolders.tsx +msgid "{0, plural, one {# file} other {# files}}" +msgstr "{0, plural, one {# Datei} other {# Dateien}}" + #. placeholder {0}: group.entries.length #: src/pages/ModelSettingsOverlay.tsx msgid "{0, plural, one {# model} other {# models}}" @@ -368,6 +373,7 @@ msgstr "Aktives Modell" msgid "Activity" msgstr "Aktivität" +#: src/components/MediaLibraryFolders.tsx #: src/components/onboarding/OnboardingScanStep.tsx #: src/pages/PluginsOverlay.tsx #: src/pages/ScratchpadSection.tsx @@ -1829,6 +1835,7 @@ msgstr "Stimme konnte nicht aktualisiert werden" msgid "Could not upload {name}. Try again." msgstr "{name} konnte nicht hochgeladen werden. Versuche es erneut." +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -1839,6 +1846,7 @@ msgstr "Konnte nicht geladen werden" msgid "Couldn't load messaging settings" msgstr "Messaging-Einstellungen konnten nicht geladen werden" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -2519,6 +2527,10 @@ msgstr "Schwung" msgid "Flower" msgstr "Blume" +#: src/components/MediaLibraryFolders.tsx +msgid "Folder" +msgstr "Ordner" + #: src/pages/PrivacySettingsOverlay.tsx msgid "Fonts loaded locally" msgstr "Schriften lokal geladen" @@ -2821,6 +2833,14 @@ msgstr "In Arbeit" msgid "Inbox & email" msgstr "Posteingang & E-Mail" +#: src/components/MediaLibraryFolders.tsx +msgid "Index again" +msgstr "Neu einlesen" + +#: src/components/MediaLibraryFolders.tsx +msgid "Indexing…" +msgstr "Wird eingelesen…" + #: src/pages/shell/bot-panel.tsx msgid "Inherit default" msgstr "Standard übernehmen" @@ -2927,6 +2947,10 @@ msgstr "Später" msgid "Leave" msgstr "Verlassen" +#: src/components/MediaLibraryFolders.tsx +msgid "Library" +msgstr "Mediathek" + #: src/lib/voice-persona-copy.ts msgid "Life channel — I'm here." msgstr "Privatkanal — ich bin da." @@ -4157,6 +4181,10 @@ msgstr "Entfernen" msgid "Remove {0}" msgstr "{0} entfernen" +#: src/components/MediaLibraryFolders.tsx +msgid "Remove folder" +msgstr "Ordner entfernen" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "Git-Ereignis entfernen" diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index f7c88f4..6e1b187 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -27,6 +27,11 @@ msgstr "(optional)" msgid "{0, plural, one {# day} other {# days}}" msgstr "{0, plural, one {# day} other {# days}}" +#. placeholder {0}: source.assetCount +#: src/components/MediaLibraryFolders.tsx +msgid "{0, plural, one {# file} other {# files}}" +msgstr "{0, plural, one {# file} other {# files}}" + #. placeholder {0}: group.entries.length #: src/pages/ModelSettingsOverlay.tsx msgid "{0, plural, one {# model} other {# models}}" @@ -368,6 +373,7 @@ msgstr "Active model" msgid "Activity" msgstr "Activity" +#: src/components/MediaLibraryFolders.tsx #: src/components/onboarding/OnboardingScanStep.tsx #: src/pages/PluginsOverlay.tsx #: src/pages/ScratchpadSection.tsx @@ -1829,6 +1835,7 @@ msgstr "Could not update voice" msgid "Could not upload {name}. Try again." msgstr "Could not upload {name}. Try again." +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -1839,6 +1846,7 @@ msgstr "Couldn't load" msgid "Couldn't load messaging settings" msgstr "Couldn't load messaging settings" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -2519,6 +2527,10 @@ msgstr "Flick" msgid "Flower" msgstr "Flower" +#: src/components/MediaLibraryFolders.tsx +msgid "Folder" +msgstr "Folder" + #: src/pages/PrivacySettingsOverlay.tsx msgid "Fonts loaded locally" msgstr "Fonts loaded locally" @@ -2821,6 +2833,14 @@ msgstr "In progress" msgid "Inbox & email" msgstr "Inbox & email" +#: src/components/MediaLibraryFolders.tsx +msgid "Index again" +msgstr "Index again" + +#: src/components/MediaLibraryFolders.tsx +msgid "Indexing…" +msgstr "Indexing…" + #: src/pages/shell/bot-panel.tsx msgid "Inherit default" msgstr "Inherit default" @@ -2927,6 +2947,10 @@ msgstr "Later" msgid "Leave" msgstr "Leave" +#: src/components/MediaLibraryFolders.tsx +msgid "Library" +msgstr "Library" + #: src/lib/voice-persona-copy.ts msgid "Life channel — I'm here." msgstr "Life channel — I'm here." @@ -4157,6 +4181,10 @@ msgstr "Remove" msgid "Remove {0}" msgstr "Remove {0}" +#: src/components/MediaLibraryFolders.tsx +msgid "Remove folder" +msgstr "Remove folder" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "Remove Git event" diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index 18419fe..eace18d 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -27,6 +27,11 @@ msgstr "" msgid "{0, plural, one {# day} other {# days}}" msgstr "" +#. placeholder {0}: source.assetCount +#: src/components/MediaLibraryFolders.tsx +msgid "{0, plural, one {# file} other {# files}}" +msgstr "" + #. placeholder {0}: group.entries.length #: src/pages/ModelSettingsOverlay.tsx msgid "{0, plural, one {# model} other {# models}}" @@ -368,6 +373,7 @@ msgstr "Modelo activo" msgid "Activity" msgstr "Actividad" +#: src/components/MediaLibraryFolders.tsx #: src/components/onboarding/OnboardingScanStep.tsx #: src/pages/PluginsOverlay.tsx #: src/pages/ScratchpadSection.tsx @@ -1829,6 +1835,7 @@ msgstr "" msgid "Could not upload {name}. Try again." msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -1839,6 +1846,7 @@ msgstr "" msgid "Couldn't load messaging settings" msgstr "No se pudo cargar la configuración de mensajería" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -2519,6 +2527,10 @@ msgstr "" msgid "Flower" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Folder" +msgstr "" + #: src/pages/PrivacySettingsOverlay.tsx msgid "Fonts loaded locally" msgstr "" @@ -2821,6 +2833,14 @@ msgstr "" msgid "Inbox & email" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Index again" +msgstr "" + +#: src/components/MediaLibraryFolders.tsx +msgid "Indexing…" +msgstr "" + #: src/pages/shell/bot-panel.tsx msgid "Inherit default" msgstr "Heredar predeterminado" @@ -2927,6 +2947,10 @@ msgstr "" msgid "Leave" msgstr "Salir" +#: src/components/MediaLibraryFolders.tsx +msgid "Library" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "Life channel — I'm here." msgstr "" @@ -4157,6 +4181,10 @@ msgstr "Quitar" msgid "Remove {0}" msgstr "Quitar {0}" +#: src/components/MediaLibraryFolders.tsx +msgid "Remove folder" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" diff --git a/apps/web/src/locales/hi/messages.po b/apps/web/src/locales/hi/messages.po index ca3108b..25e8955 100644 --- a/apps/web/src/locales/hi/messages.po +++ b/apps/web/src/locales/hi/messages.po @@ -27,6 +27,11 @@ msgstr "" msgid "{0, plural, one {# day} other {# days}}" msgstr "" +#. placeholder {0}: source.assetCount +#: src/components/MediaLibraryFolders.tsx +msgid "{0, plural, one {# file} other {# files}}" +msgstr "" + #. placeholder {0}: group.entries.length #: src/pages/ModelSettingsOverlay.tsx msgid "{0, plural, one {# model} other {# models}}" @@ -368,6 +373,7 @@ msgstr "सक्रिय मॉडल" msgid "Activity" msgstr "गतिविधि" +#: src/components/MediaLibraryFolders.tsx #: src/components/onboarding/OnboardingScanStep.tsx #: src/pages/PluginsOverlay.tsx #: src/pages/ScratchpadSection.tsx @@ -1829,6 +1835,7 @@ msgstr "" msgid "Could not upload {name}. Try again." msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -1839,6 +1846,7 @@ msgstr "" msgid "Couldn't load messaging settings" msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -2519,6 +2527,10 @@ msgstr "" msgid "Flower" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Folder" +msgstr "" + #: src/pages/PrivacySettingsOverlay.tsx msgid "Fonts loaded locally" msgstr "" @@ -2821,6 +2833,14 @@ msgstr "" msgid "Inbox & email" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Index again" +msgstr "" + +#: src/components/MediaLibraryFolders.tsx +msgid "Indexing…" +msgstr "" + #: src/pages/shell/bot-panel.tsx msgid "Inherit default" msgstr "डिफ़ॉल्ट का पालन करें" @@ -2927,6 +2947,10 @@ msgstr "" msgid "Leave" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Library" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "Life channel — I'm here." msgstr "" @@ -4157,6 +4181,10 @@ msgstr "हटाएं" msgid "Remove {0}" msgstr "{0} हटाएं" +#: src/components/MediaLibraryFolders.tsx +msgid "Remove folder" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" diff --git a/apps/web/src/locales/ko/messages.po b/apps/web/src/locales/ko/messages.po index fef5901..ad7e9ff 100644 --- a/apps/web/src/locales/ko/messages.po +++ b/apps/web/src/locales/ko/messages.po @@ -27,6 +27,11 @@ msgstr "" msgid "{0, plural, one {# day} other {# days}}" msgstr "" +#. placeholder {0}: source.assetCount +#: src/components/MediaLibraryFolders.tsx +msgid "{0, plural, one {# file} other {# files}}" +msgstr "" + #. placeholder {0}: group.entries.length #: src/pages/ModelSettingsOverlay.tsx msgid "{0, plural, one {# model} other {# models}}" @@ -368,6 +373,7 @@ msgstr "현재 모델" msgid "Activity" msgstr "활동" +#: src/components/MediaLibraryFolders.tsx #: src/components/onboarding/OnboardingScanStep.tsx #: src/pages/PluginsOverlay.tsx #: src/pages/ScratchpadSection.tsx @@ -1829,6 +1835,7 @@ msgstr "" msgid "Could not upload {name}. Try again." msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -1839,6 +1846,7 @@ msgstr "" msgid "Couldn't load messaging settings" msgstr "메시징 설정을 불러올 수 없습니다" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -2519,6 +2527,10 @@ msgstr "" msgid "Flower" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Folder" +msgstr "" + #: src/pages/PrivacySettingsOverlay.tsx msgid "Fonts loaded locally" msgstr "" @@ -2821,6 +2833,14 @@ msgstr "" msgid "Inbox & email" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Index again" +msgstr "" + +#: src/components/MediaLibraryFolders.tsx +msgid "Indexing…" +msgstr "" + #: src/pages/shell/bot-panel.tsx msgid "Inherit default" msgstr "기본 설정 사용" @@ -2927,6 +2947,10 @@ msgstr "" msgid "Leave" msgstr "나가기" +#: src/components/MediaLibraryFolders.tsx +msgid "Library" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "Life channel — I'm here." msgstr "" @@ -4157,6 +4181,10 @@ msgstr "삭제" msgid "Remove {0}" msgstr "{0} 삭제" +#: src/components/MediaLibraryFolders.tsx +msgid "Remove folder" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" diff --git a/apps/web/src/locales/pt-BR/messages.po b/apps/web/src/locales/pt-BR/messages.po index 310aaef..7c2ba5e 100644 --- a/apps/web/src/locales/pt-BR/messages.po +++ b/apps/web/src/locales/pt-BR/messages.po @@ -27,6 +27,11 @@ msgstr "" msgid "{0, plural, one {# day} other {# days}}" msgstr "" +#. placeholder {0}: source.assetCount +#: src/components/MediaLibraryFolders.tsx +msgid "{0, plural, one {# file} other {# files}}" +msgstr "" + #. placeholder {0}: group.entries.length #: src/pages/ModelSettingsOverlay.tsx msgid "{0, plural, one {# model} other {# models}}" @@ -368,6 +373,7 @@ msgstr "Modelo ativo" msgid "Activity" msgstr "Atividade" +#: src/components/MediaLibraryFolders.tsx #: src/components/onboarding/OnboardingScanStep.tsx #: src/pages/PluginsOverlay.tsx #: src/pages/ScratchpadSection.tsx @@ -1829,6 +1835,7 @@ msgstr "" msgid "Could not upload {name}. Try again." msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -1839,6 +1846,7 @@ msgstr "" msgid "Couldn't load messaging settings" msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -2519,6 +2527,10 @@ msgstr "" msgid "Flower" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Folder" +msgstr "" + #: src/pages/PrivacySettingsOverlay.tsx msgid "Fonts loaded locally" msgstr "" @@ -2821,6 +2833,14 @@ msgstr "" msgid "Inbox & email" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Index again" +msgstr "" + +#: src/components/MediaLibraryFolders.tsx +msgid "Indexing…" +msgstr "" + #: src/pages/shell/bot-panel.tsx msgid "Inherit default" msgstr "Herdar (padrão)" @@ -2927,6 +2947,10 @@ msgstr "" msgid "Leave" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Library" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "Life channel — I'm here." msgstr "" @@ -4157,6 +4181,10 @@ msgstr "Remover" msgid "Remove {0}" msgstr "Remover {0}" +#: src/components/MediaLibraryFolders.tsx +msgid "Remove folder" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" diff --git a/apps/web/src/locales/ru/messages.po b/apps/web/src/locales/ru/messages.po index 9cc5bf3..500d5f8 100644 --- a/apps/web/src/locales/ru/messages.po +++ b/apps/web/src/locales/ru/messages.po @@ -27,6 +27,11 @@ msgstr "" msgid "{0, plural, one {# day} other {# days}}" msgstr "" +#. placeholder {0}: source.assetCount +#: src/components/MediaLibraryFolders.tsx +msgid "{0, plural, one {# file} other {# files}}" +msgstr "" + #. placeholder {0}: group.entries.length #: src/pages/ModelSettingsOverlay.tsx msgid "{0, plural, one {# model} other {# models}}" @@ -368,6 +373,7 @@ msgstr "Активная модель" msgid "Activity" msgstr "Активность" +#: src/components/MediaLibraryFolders.tsx #: src/components/onboarding/OnboardingScanStep.tsx #: src/pages/PluginsOverlay.tsx #: src/pages/ScratchpadSection.tsx @@ -1829,6 +1835,7 @@ msgstr "" msgid "Could not upload {name}. Try again." msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -1839,6 +1846,7 @@ msgstr "" msgid "Couldn't load messaging settings" msgstr "Не удалось загрузить настройки обмена сообщениями." +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -2519,6 +2527,10 @@ msgstr "" msgid "Flower" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Folder" +msgstr "" + #: src/pages/PrivacySettingsOverlay.tsx msgid "Fonts loaded locally" msgstr "" @@ -2821,6 +2833,14 @@ msgstr "" msgid "Inbox & email" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Index again" +msgstr "" + +#: src/components/MediaLibraryFolders.tsx +msgid "Indexing…" +msgstr "" + #: src/pages/shell/bot-panel.tsx msgid "Inherit default" msgstr "Наследовать значение по умолчанию" @@ -2927,6 +2947,10 @@ msgstr "Позже" msgid "Leave" msgstr "Выйти" +#: src/components/MediaLibraryFolders.tsx +msgid "Library" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "Life channel — I'm here." msgstr "" @@ -4157,6 +4181,10 @@ msgstr "Удалить" msgid "Remove {0}" msgstr "Удалить {0}" +#: src/components/MediaLibraryFolders.tsx +msgid "Remove folder" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "Удалить событие Git" diff --git a/apps/web/src/locales/tr/messages.po b/apps/web/src/locales/tr/messages.po index b696484..e7392ad 100644 --- a/apps/web/src/locales/tr/messages.po +++ b/apps/web/src/locales/tr/messages.po @@ -27,6 +27,11 @@ msgstr "" msgid "{0, plural, one {# day} other {# days}}" msgstr "" +#. placeholder {0}: source.assetCount +#: src/components/MediaLibraryFolders.tsx +msgid "{0, plural, one {# file} other {# files}}" +msgstr "" + #. placeholder {0}: group.entries.length #: src/pages/ModelSettingsOverlay.tsx msgid "{0, plural, one {# model} other {# models}}" @@ -368,6 +373,7 @@ msgstr "Etkin model" msgid "Activity" msgstr "Etkinlik" +#: src/components/MediaLibraryFolders.tsx #: src/components/onboarding/OnboardingScanStep.tsx #: src/pages/PluginsOverlay.tsx #: src/pages/ScratchpadSection.tsx @@ -1829,6 +1835,7 @@ msgstr "" msgid "Could not upload {name}. Try again." msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -1839,6 +1846,7 @@ msgstr "" msgid "Couldn't load messaging settings" msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -2519,6 +2527,10 @@ msgstr "" msgid "Flower" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Folder" +msgstr "" + #: src/pages/PrivacySettingsOverlay.tsx msgid "Fonts loaded locally" msgstr "" @@ -2821,6 +2833,14 @@ msgstr "" msgid "Inbox & email" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Index again" +msgstr "" + +#: src/components/MediaLibraryFolders.tsx +msgid "Indexing…" +msgstr "" + #: src/pages/shell/bot-panel.tsx msgid "Inherit default" msgstr "Varsayılanı devral" @@ -2927,6 +2947,10 @@ msgstr "" msgid "Leave" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Library" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "Life channel — I'm here." msgstr "" @@ -4157,6 +4181,10 @@ msgstr "Kaldır" msgid "Remove {0}" msgstr "{0} kaldır" +#: src/components/MediaLibraryFolders.tsx +msgid "Remove folder" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" diff --git a/apps/web/src/locales/zh-CN/messages.po b/apps/web/src/locales/zh-CN/messages.po index 42ccc7e..7b40090 100644 --- a/apps/web/src/locales/zh-CN/messages.po +++ b/apps/web/src/locales/zh-CN/messages.po @@ -27,6 +27,11 @@ msgstr "" msgid "{0, plural, one {# day} other {# days}}" msgstr "" +#. placeholder {0}: source.assetCount +#: src/components/MediaLibraryFolders.tsx +msgid "{0, plural, one {# file} other {# files}}" +msgstr "" + #. placeholder {0}: group.entries.length #: src/pages/ModelSettingsOverlay.tsx msgid "{0, plural, one {# model} other {# models}}" @@ -368,6 +373,7 @@ msgstr "当前模型" msgid "Activity" msgstr "动态" +#: src/components/MediaLibraryFolders.tsx #: src/components/onboarding/OnboardingScanStep.tsx #: src/pages/PluginsOverlay.tsx #: src/pages/ScratchpadSection.tsx @@ -1829,6 +1835,7 @@ msgstr "" msgid "Could not upload {name}. Try again." msgstr "" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -1839,6 +1846,7 @@ msgstr "" msgid "Couldn't load messaging settings" msgstr "无法加载消息设置" +#: src/components/MediaLibraryFolders.tsx #: src/components/MediaSettings.tsx #: src/components/NotificationSettingsPanel.tsx #: src/components/UserProfileSettings.tsx @@ -2519,6 +2527,10 @@ msgstr "" msgid "Flower" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Folder" +msgstr "" + #: src/pages/PrivacySettingsOverlay.tsx msgid "Fonts loaded locally" msgstr "" @@ -2821,6 +2833,14 @@ msgstr "" msgid "Inbox & email" msgstr "" +#: src/components/MediaLibraryFolders.tsx +msgid "Index again" +msgstr "" + +#: src/components/MediaLibraryFolders.tsx +msgid "Indexing…" +msgstr "" + #: src/pages/shell/bot-panel.tsx msgid "Inherit default" msgstr "沿用默认" @@ -2927,6 +2947,10 @@ msgstr "" msgid "Leave" msgstr "退出" +#: src/components/MediaLibraryFolders.tsx +msgid "Library" +msgstr "" + #: src/lib/voice-persona-copy.ts msgid "Life channel — I'm here." msgstr "" @@ -4157,6 +4181,10 @@ msgstr "移除" msgid "Remove {0}" msgstr "移除 {0}" +#: src/components/MediaLibraryFolders.tsx +msgid "Remove folder" +msgstr "" + #: src/pages/RoutineEditor.tsx msgid "Remove Git event" msgstr "" diff --git a/apps/web/src/pages/ExternalConversationSettings.tsx b/apps/web/src/pages/ExternalConversationSettings.tsx index 0b00102..396bb96 100644 --- a/apps/web/src/pages/ExternalConversationSettings.tsx +++ b/apps/web/src/pages/ExternalConversationSettings.tsx @@ -80,7 +80,10 @@ export function ExternalConversationSettings({ return (
-

+

{conversation.displayName || t`External conversation`}

diff --git a/apps/web/src/pages/PeerMessagesOverlay.tsx b/apps/web/src/pages/PeerMessagesOverlay.tsx index 5c61241..b8c8451 100644 --- a/apps/web/src/pages/PeerMessagesOverlay.tsx +++ b/apps/web/src/pages/PeerMessagesOverlay.tsx @@ -134,7 +134,7 @@ export function PeerMessagesOverlay({

{sent ? botName : peerBotName}
-
+
{peerMessage.text}
diff --git a/apps/web/src/pages/PluginsOverlay.tsx b/apps/web/src/pages/PluginsOverlay.tsx index 0c56fdc..01daf66 100644 --- a/apps/web/src/pages/PluginsOverlay.tsx +++ b/apps/web/src/pages/PluginsOverlay.tsx @@ -558,7 +558,9 @@ export function PluginsOverlay({ {item.name[0]}
)} -
{item.name}
+
+ {item.name} +