feat(open-webui): full attachment menu with all source types

Replace the single attach popover with an Open WebUI-style multi-level
menu covering every attachment source:

- Upload Files: images stay inline (vision); other files are uploaded and
  grounded via retrieval (`file-{id}` collection).
- Capture: take a photo from the webcam as an inline image.
- Attach Webpage: ingest a URL (existing).
- Attach Files: pick files already stored in Open WebUI.
- Attach Notes: inject the contents of a note as context.
- Attach Knowledge: nested picker with checkboxes, expandable to a base's
  files (replaces the flat checkbox list).
- Reference Chats: bring a previous conversation in as context.

Knowledge bases, web pages and files are grounded by retrieval; notes and
referenced chats are injected verbatim as context blocks. Adds the
supporting integration methods (list files/notes, knowledge files, upload
file), request handlers and tRPC procedures.
This commit is contained in:
Lok3sh-Kumar committed 2026-06-27 14:16:13 +05:30
1 parent 800caf6e5e
commit 78bd66e803
8 files changed
+809 -124

No files matched your search

@@ -1,3 +1,5 @@
import { FormData } from "undici";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
@@ -10,8 +12,10 @@ import type {
OpenWebUiChatListItem,
OpenWebUiChatPayload,
OpenWebUiCompletionMessage,
OpenWebUiFileSummary,
OpenWebUiKnowledge,
OpenWebUiModel,
OpenWebUiNote,
OpenWebUiWebDocument,
} from "./open-webui-types";
import {
@@ -19,9 +23,13 @@ import {
openWebUiChatSchema,
openWebUiCollectionQueryResponseSchema,
openWebUiCompletionChunkSchema,
openWebUiFileListSchema,
openWebUiKnowledgeDetailSchema,
openWebUiKnowledgeListSchema,
openWebUiModelsResponseSchema,
openWebUiNoteListSchema,
openWebUiProcessWebResponseSchema,
openWebUiUploadedFileSchema,
} from "./open-webui-types";
const logger = createLogger({ module: "open-webui-integration" });
@@ -70,6 +78,86 @@ export class OpenWebUiIntegration extends Integration {
return Array.isArray(parsed) ? parsed : parsed.items;
}
/**
* List the user's uploaded files (for the "Attach Files" picker).
*/
public async listFilesAsync(): Promise<OpenWebUiFileSummary[]> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/files/"), {
headers: this.getAuthHeaders(),
});
if (!response.ok) {
throw new ResponseError(response);
}
const parsed = openWebUiFileListSchema.parse(await response.json());
const files = Array.isArray(parsed) ? parsed : parsed.items;
return files.map((file) => ({ id: file.id, name: file.meta?.name ?? file.filename ?? file.id }));
}
/**
* List the files contained in a knowledge base (for the expandable picker).
*/
public async getKnowledgeFilesAsync(knowledgeId: string): Promise<OpenWebUiFileSummary[]> {
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/knowledge/${knowledgeId}`), {
headers: this.getAuthHeaders(),
});
if (!response.ok) {
throw new ResponseError(response);
}
const parsed = openWebUiKnowledgeDetailSchema.parse(await response.json());
return (parsed.files ?? []).map((file) => ({ id: file.id, name: file.meta?.name ?? file.filename ?? file.id }));
}
/**
* List the user's notes (for the "Attach Notes" picker).
*/
public async listNotesAsync(): Promise<OpenWebUiNote[]> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/notes/"), {
headers: this.getAuthHeaders(),
});
if (!response.ok) {
throw new ResponseError(response);
}
const parsed = openWebUiNoteListSchema.parse(await response.json());
const notes = Array.isArray(parsed) ? parsed : parsed.items;
return notes.map((note) => ({
id: note.id,
title: note.title ?? "Untitled note",
content: note.data?.content?.md ?? "",
}));
}
/**
* Upload a file to Open WebUI so it can be retrieved from later. Returns the
* new file id (its vector collection is `file-{id}`).
*/
public async uploadFileAsync(
filename: string,
base64Content: string,
contentType: string,
): Promise<OpenWebUiFileSummary> {
const form = new FormData();
form.append("file", new Blob([Buffer.from(base64Content, "base64")], { type: contentType }), filename);
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/files/"), {
method: "POST",
headers: this.getAuthHeaders(),
body: form,
});
if (!response.ok) {
throw new ResponseError(response);
}
const parsed = openWebUiUploadedFileSchema.parse(await response.json());
return { id: parsed.id, name: parsed.meta?.name ?? parsed.filename ?? filename };
}
/**
* Ingest a web page into a temporary vector collection and return a handle to
* retrieve from it. Open WebUI does not crawl URLs at completion time, so we
@@ -86,6 +86,61 @@ export const openWebUiKnowledgeListSchema = z.union([
z.object({ items: z.array(openWebUiKnowledgeSchema) }),
]);
// A file stored in Open WebUI (uploaded directly or part of a knowledge base).
export const openWebUiFileSchema = z.object({
id: z.string(),
filename: z.string().nullish(),
meta: z.object({ name: z.string().nullish(), content_type: z.string().nullish() }).nullish(),
});
export type OpenWebUiFile = z.infer<typeof openWebUiFileSchema>;
export const openWebUiFileListSchema = z.union([
z.array(openWebUiFileSchema),
z.object({ items: z.array(openWebUiFileSchema) }),
]);
// POST /api/v1/files/ — upload response (we only need the new file id + name).
export const openWebUiUploadedFileSchema = z.object({
id: z.string(),
filename: z.string().nullish(),
meta: z.object({ name: z.string().nullish() }).nullish(),
});
// GET /api/v1/knowledge/{id} — knowledge base with its files.
export const openWebUiKnowledgeDetailSchema = z.object({
id: z.string(),
name: z.string().nullish(),
files: z.array(openWebUiFileSchema).nullish(),
});
export type OpenWebUiKnowledgeDetail = z.infer<typeof openWebUiKnowledgeDetailSchema>;
// GET /api/v1/notes/ — user notes (markdown content under data.content.md).
export const openWebUiNoteSchema = z.object({
id: z.string(),
title: z.string().nullish(),
data: z.object({ content: z.object({ md: z.string().nullish() }).nullish() }).nullish(),
});
export const openWebUiNoteListSchema = z.union([
z.array(openWebUiNoteSchema),
z.object({ items: z.array(openWebUiNoteSchema) }),
]);
// A note flattened for the widget: id, title and its markdown body.
export interface OpenWebUiNote {
id: string;
title: string;
content: string;
}
// A file flattened for the widget: id and a display name.
export interface OpenWebUiFileSummary {
id: string;
name: string;
}
// POST /api/v1/retrieval/process/web — ingests a URL into a temporary vector
// collection. We retrieve from that collection at send time and inject the
// results as context (Open WebUI does not fetch URLs at completion time).