feat(composer): carry reference image names and submit on Enter

Propagate an optional imageName through the agent content pipeline
(frontend types → API → domain → prompt building) so reference images
render with their name instead of a bare "image" label.

Also switch PromptComposer to submit on plain Enter (Shift+Enter for
newline, IME-safe) and add a submitDisabled guard wired from the canvas
and home composers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 08:41:15 +08:00
parent 398dec422d
commit 3c5006e2f3
11 changed files with 34 additions and 7 deletions
+1
View File
@@ -306,6 +306,7 @@ export type AgentContent =
| { | {
type: "image"; type: "image";
imageUrl: string; imageUrl: string;
imageName?: string;
imageWidth?: number; imageWidth?: number;
imageHeight?: number; imageHeight?: number;
}; };
@@ -190,6 +190,7 @@ export const CanvasAgentComposer = forwardRef<PromptComposerHandle, CanvasAgentC
onAnnotationPreviewChange={onAnnotationPreviewChange} onAnnotationPreviewChange={onAnnotationPreviewChange}
onPasteFile={onUploadFile} onPasteFile={onUploadFile}
onSubmit={onSubmit} onSubmit={onSubmit}
submitDisabled={generating || !canSubmit}
/> />
{previewSlot} {previewSlot}
<div className="assistant-composer-toolbar"> <div className="assistant-composer-toolbar">
@@ -632,6 +632,7 @@ export const PromptComposer = forwardRef<
onAnnotationPreviewChange?: (preview: AnnotationPreviewRequest | null) => void; onAnnotationPreviewChange?: (preview: AnnotationPreviewRequest | null) => void;
onPasteFile?: (file: File | null) => void | Promise<void>; onPasteFile?: (file: File | null) => void | Promise<void>;
onSubmit?: () => void; onSubmit?: () => void;
submitDisabled?: boolean;
} }
>(function PromptComposer( >(function PromptComposer(
{ {
@@ -648,7 +649,8 @@ export const PromptComposer = forwardRef<
onAnnotationsChange, onAnnotationsChange,
onAnnotationPreviewChange, onAnnotationPreviewChange,
onPasteFile, onPasteFile,
onSubmit onSubmit,
submitDisabled = false
}, },
ref ref
) { ) {
@@ -883,7 +885,8 @@ export const PromptComposer = forwardRef<
} }
return { return {
type: "image", type: "image",
imageUrl: part.reference.publicUrl imageUrl: part.reference.publicUrl,
imageName: part.reference.name
}; };
}) })
.filter((part): part is AgentContent => Boolean(part)); .filter((part): part is AgentContent => Boolean(part));
@@ -1067,9 +1070,10 @@ export const PromptComposer = forwardRef<
} }
} }
} }
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { const isComposing = event.nativeEvent.isComposing || event.keyCode === 229;
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
event.preventDefault(); event.preventDefault();
onSubmit?.(); if (!submitDisabled) onSubmit?.();
} }
}} }}
onPaste={handlePaste} onPaste={handlePaste}
@@ -5393,18 +5393,22 @@ function agentContentsToDisplayText(contents: AgentContent[]) {
return contents return contents
.map((content, index) => { .map((content, index) => {
if (content.type === "text") return content.text; if (content.type === "text") return content.text;
return `参考图 ${index + 1}image):${content.imageUrl}`; return `参考图 ${index + 1}${agentContentImageName(content)}):${content.imageUrl}`;
}) })
.filter((item) => item.trim()) .filter((item) => item.trim())
.join("\n\n"); .join("\n\n");
} }
function agentContentImageName(content: Extract<AgentContent, { type: "image" }>) {
return content.imageName?.replace(/\s+/g, " ").trim() || "image";
}
function imageGeneratorSubmittedPrompt(contents: AgentContent[]) { function imageGeneratorSubmittedPrompt(contents: AgentContent[]) {
return cleanImageGeneratorPrompt( return cleanImageGeneratorPrompt(
contents contents
.map((content, index) => { .map((content, index) => {
if (content.type === "text" && !isSettingsTextSource(content.textSource)) return content.text; if (content.type === "text" && !isSettingsTextSource(content.textSource)) return content.text;
if (content.type === "image") return `参考图 ${index + 1}${content.imageUrl}`; if (content.type === "image") return `参考图 ${index + 1}${agentContentImageName(content)}${content.imageUrl}`;
return ""; return "";
}) })
.filter((item) => item.trim()) .filter((item) => item.trim())
+1
View File
@@ -736,6 +736,7 @@ export function HomePage() {
onReferenceRemoved={handleReferenceRemoved} onReferenceRemoved={handleReferenceRemoved}
onPasteFile={attachImage} onPasteFile={attachImage}
onSubmit={() => createProject()} onSubmit={() => createProject()}
submitDisabled={loading || !canSubmitPrompt}
/> />
<div className="prompt-actions"> <div className="prompt-actions">
<input ref={fileInputRef} type="file" accept="image/*" hidden onChange={(event) => void attachImage(event.target.files?.[0] ?? null)} /> <input ref={fileInputRef} type="file" accept="image/*" hidden onChange={(event) => void attachImage(event.target.files?.[0] ?? null)} />
+1
View File
@@ -369,6 +369,7 @@ type AgentContent {
Text string `json:"text,optional"` Text string `json:"text,optional"`
TextSource string `json:"textSource,optional"` TextSource string `json:"textSource,optional"`
ImageUrl string `json:"imageUrl,optional"` ImageUrl string `json:"imageUrl,optional"`
ImageName string `json:"imageName,optional"`
ImageWidth int64 `json:"imageWidth,optional"` ImageWidth int64 `json:"imageWidth,optional"`
ImageHeight int64 `json:"imageHeight,optional"` ImageHeight int64 `json:"imageHeight,optional"`
} }
+11 -1
View File
@@ -4510,7 +4510,13 @@ func promptFromAgentMessages(messages []design.AgentChatMessage) string {
if builder.Len() > 0 { if builder.Len() > 0 {
builder.WriteString("\n") builder.WriteString("\n")
} }
builder.WriteString("Reference image: ") builder.WriteString("Reference image")
if imageName := strings.TrimSpace(content.ImageName); imageName != "" {
builder.WriteString(" (")
builder.WriteString(compactReferenceImageName(imageName))
builder.WriteString(")")
}
builder.WriteString(": ")
builder.WriteString(imageURL) builder.WriteString(imageURL)
} }
} }
@@ -4519,6 +4525,10 @@ func promptFromAgentMessages(messages []design.AgentChatMessage) string {
return builder.String() return builder.String()
} }
func compactReferenceImageName(value string) string {
return strings.Join(strings.Fields(value), " ")
}
func hasUserPromptTextInAgentMessages(messages []design.AgentChatMessage) bool { func hasUserPromptTextInAgentMessages(messages []design.AgentChatMessage) bool {
for _, message := range messages { for _, message := range messages {
if strings.TrimSpace(message.Role) != "" && strings.ToLower(strings.TrimSpace(message.Role)) != "user" { if strings.TrimSpace(message.Role) != "" && strings.ToLower(strings.TrimSpace(message.Role)) != "user" {
+1
View File
@@ -18,6 +18,7 @@ type AgentContent struct {
Text string Text string
TextSource string TextSource string
ImageURL string ImageURL string
ImageName string
ImageWidth int64 ImageWidth int64
ImageHeight int64 ImageHeight int64
} }
@@ -70,6 +70,7 @@ type socketContent struct {
Text string `json:"text"` Text string `json:"text"`
TextSource string `json:"text_source"` TextSource string `json:"text_source"`
ImageURL string `json:"image_url"` ImageURL string `json:"image_url"`
ImageName string `json:"image_name"`
ImageWidth int64 `json:"image_width"` ImageWidth int64 `json:"image_width"`
ImageHeight int64 `json:"image_height"` ImageHeight int64 `json:"image_height"`
} }
@@ -155,6 +156,7 @@ func (data socketStartData) toDomainChat(projectID string) design.AgentChatReque
Text: content.Text, Text: content.Text,
TextSource: content.TextSource, TextSource: content.TextSource,
ImageURL: content.ImageURL, ImageURL: content.ImageURL,
ImageName: content.ImageName,
ImageWidth: content.ImageWidth, ImageWidth: content.ImageWidth,
ImageHeight: content.ImageHeight, ImageHeight: content.ImageHeight,
}) })
+1
View File
@@ -519,6 +519,7 @@ func toDomainAgentChat(req *types.AgentChatRequest) design.AgentChatRequest {
Text: content.Text, Text: content.Text,
TextSource: content.TextSource, TextSource: content.TextSource,
ImageURL: content.ImageUrl, ImageURL: content.ImageUrl,
ImageName: content.ImageName,
ImageWidth: content.ImageWidth, ImageWidth: content.ImageWidth,
ImageHeight: content.ImageHeight, ImageHeight: content.ImageHeight,
}) })
+1
View File
@@ -49,6 +49,7 @@ type AgentContent struct {
Text string `json:"text,optional"` Text string `json:"text,optional"`
TextSource string `json:"textSource,optional"` TextSource string `json:"textSource,optional"`
ImageUrl string `json:"imageUrl,optional"` ImageUrl string `json:"imageUrl,optional"`
ImageName string `json:"imageName,optional"`
ImageWidth int64 `json:"imageWidth,optional"` ImageWidth int64 `json:"imageWidth,optional"`
ImageHeight int64 `json:"imageHeight,optional"` ImageHeight int64 `json:"imageHeight,optional"`
} }