mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-25 21:34:47 -06:00
feat(attachments): accept pdf/audio uploads in the UI + admin capability toggles
- composer: accept pdf/audio in the upload picker; client-side kind inference for the optimistic chip (server classify_upload stays authoritative) - admin Models tab: supports_pdf + supports_audio_input toggles (flow through the field-aware capabilities merge into ModelCapabilities, so flipping supports_audio_input on an omni alias enables native input_audio) - docs: AttachmentInfo.kind, AttachmentUpload, and the TS SDK note pdf/audio
This commit is contained in:
@@ -96,7 +96,7 @@ export interface AttachmentInfo {
|
||||
filename: string;
|
||||
mime_type: string;
|
||||
size_bytes: number;
|
||||
/** "image" or "text". */
|
||||
/** "image", "text", "pdf", or "audio". */
|
||||
kind: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,10 @@ class AttachmentInfo(BaseModel):
|
||||
filename: str = Field(description="Original upload filename")
|
||||
mime_type: str = Field(description="Canonicalized MIME type")
|
||||
size_bytes: int = Field(description="Payload size in bytes")
|
||||
kind: str = Field(description="'image' or 'text'", examples=["image", "text"])
|
||||
kind: str = Field(
|
||||
description="'image', 'text', 'pdf', or 'audio'",
|
||||
examples=["image", "text", "pdf", "audio"],
|
||||
)
|
||||
|
||||
|
||||
class UploadAttachmentResponse(AttachmentInfo):
|
||||
|
||||
@@ -5097,22 +5097,26 @@ const _MODEL_CAP_KEYS = [
|
||||
"supports_tools",
|
||||
"supports_streaming",
|
||||
"supports_vision",
|
||||
"supports_pdf",
|
||||
"supports_web_search",
|
||||
"supports_temperature",
|
||||
"supports_effort",
|
||||
"supports_transcription",
|
||||
"supports_speech_synthesis",
|
||||
"supports_audio_input",
|
||||
"supports_rerank",
|
||||
];
|
||||
const _MODEL_CAP_DEFAULTS = {
|
||||
supports_tools: true,
|
||||
supports_streaming: true,
|
||||
supports_vision: false,
|
||||
supports_pdf: false,
|
||||
supports_web_search: false,
|
||||
supports_temperature: true,
|
||||
supports_effort: false,
|
||||
supports_transcription: false,
|
||||
supports_speech_synthesis: false,
|
||||
supports_audio_input: false,
|
||||
supports_rerank: false,
|
||||
};
|
||||
let _modelCapsBaseline = {}; // known-model table values (display + delta base)
|
||||
|
||||
@@ -1769,6 +1769,12 @@
|
||||
></span
|
||||
><span class="cap-name">Vision</span></label
|
||||
>
|
||||
<label class="cap"
|
||||
><input type="checkbox" data-cap="supports_pdf" /><span
|
||||
class="cap-led"
|
||||
></span
|
||||
><span class="cap-name">PDF documents</span></label
|
||||
>
|
||||
<label class="cap"
|
||||
><input type="checkbox" data-cap="supports_web_search" /><span
|
||||
class="cap-led"
|
||||
@@ -1802,6 +1808,12 @@
|
||||
/><span class="cap-led"></span
|
||||
><span class="cap-name">Speech (TTS)</span></label
|
||||
>
|
||||
<label class="cap"
|
||||
><input type="checkbox" data-cap="supports_audio_input" /><span
|
||||
class="cap-led"
|
||||
></span
|
||||
><span class="cap-name">Audio input</span></label
|
||||
>
|
||||
<label class="cap"
|
||||
><input type="checkbox" data-cap="supports_rerank" /><span
|
||||
class="cap-led"
|
||||
|
||||
@@ -11,8 +11,8 @@ class AttachmentUpload:
|
||||
|
||||
Used by ``upload_attachment`` and by ``create_workstream(attachments=...)``.
|
||||
``mime_type`` is advisory — the server applies its own magic-byte
|
||||
sniffing for images and UTF-8 validation for text documents and
|
||||
rejects anything that doesn't match its allowlist.
|
||||
sniffing (images, PDF, audio) and UTF-8 validation for text documents,
|
||||
and rejects anything that doesn't match its allowlist.
|
||||
"""
|
||||
|
||||
filename: str
|
||||
|
||||
@@ -35,9 +35,11 @@
|
||||
* anywhere onto the pane to attach.
|
||||
*/
|
||||
var ATTACH_DEFAULT_ACCEPT =
|
||||
"image/png,image/jpeg,image/gif,image/webp,text/*," +
|
||||
"image/png,image/jpeg,image/gif,image/webp,application/pdf,text/*," +
|
||||
"audio/wav,audio/mpeg,audio/ogg,audio/flac,audio/mp4,audio/aac,audio/webm," +
|
||||
".md,.py,.js,.ts,.tsx,.jsx,.json,.yaml,.yml,.toml,.html,.css,.sh," +
|
||||
".rs,.go,.java,.c,.cpp,.h,.hpp,.sql,.xml,.ini,.conf";
|
||||
".rs,.go,.java,.c,.cpp,.h,.hpp,.sql,.xml,.ini,.conf," +
|
||||
".pdf,.mp3,.wav,.ogg,.flac,.m4a,.aac,.webm";
|
||||
|
||||
var AUTO_RESIZE_MAX_PX = 200;
|
||||
|
||||
@@ -525,9 +527,7 @@ Composer.prototype._wireEvents = function () {
|
||||
});
|
||||
this._on(target, "drop", function (e) {
|
||||
target.classList.remove(dropClass);
|
||||
var files = Array.from(
|
||||
(e.dataTransfer && e.dataTransfer.files) || [],
|
||||
);
|
||||
var files = Array.from((e.dataTransfer && e.dataTransfer.files) || []);
|
||||
if (files.length > 0) {
|
||||
e.preventDefault();
|
||||
files.forEach(function (f) {
|
||||
@@ -613,9 +613,7 @@ Composer.prototype.setBusy = function (b) {
|
||||
// Placeholder swap applies whenever busy, not only in queue mode —
|
||||
// callers that don't set busyPlaceholder see no visible change
|
||||
// because it defaults to idlePlaceholder.
|
||||
this.inputEl.placeholder = b
|
||||
? this._busyPlaceholder
|
||||
: this._idlePlaceholder;
|
||||
this.inputEl.placeholder = b ? this._busyPlaceholder : this._idlePlaceholder;
|
||||
|
||||
// Disabled state — composer owns it unless caller opted out.
|
||||
if (!opts.externalDisable) {
|
||||
@@ -806,7 +804,6 @@ Composer.prototype.destroy = function () {
|
||||
this.el = null;
|
||||
};
|
||||
|
||||
|
||||
// --- Legacy window bridge ---------------------------------------------------
|
||||
// Still-classic consumers reach this as a global at event/boot time (after
|
||||
// this deferred module evaluated). New module code imports instead.
|
||||
|
||||
@@ -36,6 +36,17 @@ function formatSize(n) {
|
||||
return (n / (1024 * 1024)).toFixed(1) + " MB";
|
||||
}
|
||||
|
||||
function _inferKind(file) {
|
||||
// Optimistic placeholder-chip guess only; the server's classify_upload is
|
||||
// authoritative and the real kind arrives on the upload response.
|
||||
var t = (file.type || "").toLowerCase();
|
||||
var n = (file.name || "").toLowerCase();
|
||||
if (t.indexOf("image/") === 0) return "image";
|
||||
if (t === "application/pdf" || n.endsWith(".pdf")) return "pdf";
|
||||
if (t.indexOf("audio/") === 0) return "audio";
|
||||
return "text";
|
||||
}
|
||||
|
||||
function _toastError(msg) {
|
||||
if (typeof window.toast !== "undefined" && window.toast.error) {
|
||||
window.toast.error(msg);
|
||||
@@ -172,7 +183,7 @@ export function createAttachmentController(opts) {
|
||||
filename: file.name,
|
||||
size_bytes: file.size,
|
||||
mime_type: file.type || "",
|
||||
kind: (file.type || "").indexOf("image/") === 0 ? "image" : "text",
|
||||
kind: _inferKind(file),
|
||||
uploading: true,
|
||||
};
|
||||
pending.set(placeholderId, placeholder);
|
||||
@@ -300,7 +311,6 @@ export function createAttachmentController(opts) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// --- Legacy window bridge ---------------------------------------------------
|
||||
// Still-classic consumers reach this as a global at event/boot time (after
|
||||
// this deferred module evaluated). New module code imports instead.
|
||||
|
||||
Reference in New Issue
Block a user