mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add file/image attachment support to chat input (#22604)
This change adds support for image attachments to chat via add button and clipboard paste. Files are stored in a new `chat_files` table and referenced by ID in message content. File data is resolved from storage at LLM dispatch time, keeping the message content column small. Upload validates MIME types via content type or content sniffing against an allowlist (png, jpeg, gif, webp). The retrieval endpoint serves files with immutable caching headers. On the frontend, uploads start eagerly on attach with a background fetch to pre-warm the browser HTTP cache so the timeline renders instantly after send.
This commit is contained in:
+47
-2
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -96,6 +97,7 @@ type ChatMessagePart struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
MediaType string `json:"media_type,omitempty"`
|
||||
Data []byte `json:"data,omitempty"`
|
||||
FileID uuid.NullUUID `json:"file_id,omitempty" format:"uuid"`
|
||||
}
|
||||
|
||||
// ChatInputPartType represents an input part type for user chat input.
|
||||
@@ -103,12 +105,14 @@ type ChatInputPartType string
|
||||
|
||||
const (
|
||||
ChatInputPartTypeText ChatInputPartType = "text"
|
||||
ChatInputPartTypeFile ChatInputPartType = "file"
|
||||
)
|
||||
|
||||
// ChatInputPart is a single user input part for creating a chat.
|
||||
type ChatInputPart struct {
|
||||
Type ChatInputPartType `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Type ChatInputPartType `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
FileID uuid.UUID `json:"file_id,omitempty" format:"uuid"`
|
||||
}
|
||||
|
||||
// CreateChatRequest is the request to create a new chat.
|
||||
@@ -141,6 +145,11 @@ type CreateChatMessageResponse struct {
|
||||
Queued bool `json:"queued"`
|
||||
}
|
||||
|
||||
// UploadChatFileResponse is the response from uploading a chat file.
|
||||
type UploadChatFileResponse struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
}
|
||||
|
||||
// ChatWithMessages is a chat along with its messages.
|
||||
type ChatWithMessages struct {
|
||||
Chat Chat `json:"chat"`
|
||||
@@ -938,6 +947,42 @@ func (c *Client) GetChatDiffContents(ctx context.Context, chatID uuid.UUID) (Cha
|
||||
return diff, json.NewDecoder(res.Body).Decode(&diff)
|
||||
}
|
||||
|
||||
// UploadChatFile uploads a file for use in chat messages.
|
||||
func (c *Client) UploadChatFile(ctx context.Context, organizationID uuid.UUID, contentType string, filename string, rd io.Reader) (UploadChatFileResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/files?organization=%s", organizationID), rd, func(r *http.Request) {
|
||||
r.Header.Set("Content-Type", contentType)
|
||||
if filename != "" {
|
||||
r.Header.Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return UploadChatFileResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return UploadChatFileResponse{}, ReadBodyAsError(res)
|
||||
}
|
||||
var resp UploadChatFileResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// GetChatFile retrieves a previously uploaded chat file by ID.
|
||||
func (c *Client) GetChatFile(ctx context.Context, fileID uuid.UUID) ([]byte, string, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/files/%s", fileID), nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, "", ReadBodyAsError(res)
|
||||
}
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return data, res.Header.Get("Content-Type"), nil
|
||||
}
|
||||
|
||||
func formatChatStreamResponseError(response Response) string {
|
||||
message := strings.TrimSpace(response.Message)
|
||||
detail := strings.TrimSpace(response.Detail)
|
||||
|
||||
Reference in New Issue
Block a user