feat(cli): références d'attachments [file n]/[image n] dans le transcript + zone chat redimensionnable — #188 (QA verte)

- Attachments: projection canonique ChatPromptAttachmentDto (référence [file n]/[image n] par compteur de type, ordre d'import) insérée dans le prompt modèle et transportée par ReplyChunk::UserPrompt (attachmentRefs) ; rendu compact côté CustomAgentChatView.
- Resize: la zone chat (LeafView/LayoutGrid) est redimensionnable, taille actuelle conservée par défaut, préférence locale persistée.
- Tests: commands (projection des refs), dto_chat, chat_bridge, CustomAgentChatView, LayoutGrid.chat.
This commit is contained in:
2026-08-13 13:01:03 +02:00
parent 8b8468dacd
commit 4f38e1c506
10 changed files with 513 additions and 42 deletions

View File

@ -17,6 +17,7 @@ import {
import type {
AgentProfile,
ChatPromptAttachment,
GatewayError,
ReplyChunk,
ReplyProgress,
@ -50,7 +51,7 @@ export interface CustomAgentChatViewProps {
}
type ChatTurn =
| { role: "user"; text: string; attachments?: string[] }
| { role: "user"; text: string; attachments?: ChatPromptAttachment[] }
| { role: "agent"; text: string; pending?: boolean }
| { role: "tool"; label: string }
| { role: "progress"; progress: ReplyProgress; settled?: boolean }
@ -158,10 +159,62 @@ function attachmentLabels(value: unknown): string[] {
: [];
}
function isImageFilename(filename: string): boolean {
return /\.(apng|avif|gif|jpe?g|png|svg|webp)$/i.test(filename.trim());
}
function fallbackAttachmentRefs(labels: string[]): ChatPromptAttachment[] {
let imageCount = 0;
let fileCount = 0;
return labels.map((filename) => {
const image = isImageFilename(filename);
const reference = image ? `[image ${++imageCount}]` : `[file ${++fileCount}]`;
return { reference, filename };
});
}
function attachmentRefs(value: unknown, labels: string[] = []): ChatPromptAttachment[] {
if (Array.isArray(value)) {
const refs = value
.map((item): ChatPromptAttachment | null => {
if (!item || typeof item !== "object") return null;
const record = item as Record<string, unknown>;
const reference = optionalString(record.reference);
const filename = optionalString(record.filename);
if (!reference || !filename) return null;
return {
reference,
filename,
mime: optionalString(record.mime),
sizeBytes: typeof record.sizeBytes === "number" ? record.sizeBytes : undefined,
readablePath: optionalString(record.readablePath),
};
})
.filter((item): item is ChatPromptAttachment => Boolean(item));
if (refs.length > 0) return refs;
}
return fallbackAttachmentRefs(labels);
}
function attachmentRefsFromDrafts(attachments: AttachmentDraft[]): ChatPromptAttachment[] {
let imageCount = 0;
let fileCount = 0;
return attachments.map((attachment) => {
const mime = attachment.input.mime ?? "";
const image = mime.startsWith("image/") || (!mime && isImageFilename(attachment.label));
const reference = image ? `[image ${++imageCount}]` : `[file ${++fileCount}]`;
return {
reference,
filename: attachment.label,
mime: attachment.input.mime,
};
});
}
function appendUserPrompt(
turns: ChatTurn[],
text: string,
attachments: string[] = [],
attachments: ChatPromptAttachment[] = [],
): ChatTurn[] {
const last = turns[turns.length - 1];
if (
@ -589,7 +642,7 @@ function foldChunk(state: ChatViewState, raw: unknown): ChatViewState {
turns: appendUserPrompt(
turns,
String(raw.text ?? raw.prompt ?? ""),
attachmentLabels(raw.attachments),
attachmentRefs(raw.attachmentRefs, attachmentLabels(raw.attachments)),
),
entityProgressMap: state.entityProgressMap,
};
@ -1359,7 +1412,7 @@ export function CustomAgentChatView({
}
const outgoingAttachments = attachments;
const attachmentInputs = outgoingAttachments.map((item) => item.input);
const attachmentLabels = outgoingAttachments.map((item) => item.label);
const attachmentRefs = attachmentRefsFromDrafts(outgoingAttachments);
const displayText = text || "Pièce jointe";
const interactionId = `${agentId}:${++interactionSeqRef.current}`;
activeInteractionIdRef.current = interactionId;
@ -1380,7 +1433,7 @@ export function CustomAgentChatView({
{
role: "user",
text: displayText,
...(attachmentLabels.length > 0 ? { attachments: attachmentLabels } : {}),
...(attachmentRefs.length > 0 ? { attachments: attachmentRefs } : {}),
},
],
entityProgressMap: prev.entityProgressMap,
@ -1855,8 +1908,8 @@ function ChatBubble({
{user && turn.attachments && turn.attachments.length > 0 && (
<div className="mt-1 flex flex-col gap-0.5 text-xs text-muted">
{turn.attachments.map((attachment) => (
<p key={attachment} className="truncate">
Fichier: {attachment}
<p key={`${attachment.reference}-${attachment.filename}`} className="truncate">
{attachment.reference} {attachment.filename}
</p>
))}
</div>