feat(phase-06): add reference file contract and submit payload helper
This commit is contained in:
parent
b4fe531a0c
commit
99e0fe13fd
|
|
@ -468,6 +468,16 @@ export const PromptInputActionAddAttachments = ({
|
|||
export type PromptInputMessage = {
|
||||
text: string;
|
||||
files: FileUIPart[];
|
||||
references?: PromptInputReference[];
|
||||
};
|
||||
|
||||
export type PromptInputReference = {
|
||||
filename: string;
|
||||
path?: string;
|
||||
size?: number;
|
||||
ref_kind: "mention";
|
||||
ref_source: "artifact" | "upload";
|
||||
stale?: boolean;
|
||||
};
|
||||
|
||||
export type PromptInputProps = Omit<
|
||||
|
|
@ -866,6 +876,8 @@ export type PromptInputTextareaProps = ComponentProps<
|
|||
|
||||
export const PromptInputTextarea = ({
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onPaste,
|
||||
className,
|
||||
placeholder = "What would you like to know?",
|
||||
submitOnEnter = true,
|
||||
|
|
@ -876,6 +888,10 @@ export const PromptInputTextarea = ({
|
|||
const [isComposing, setIsComposing] = useState(false);
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
onKeyDown?.(e);
|
||||
if (e.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
if (isComposing || e.nativeEvent.isComposing) {
|
||||
return;
|
||||
|
|
@ -940,6 +956,10 @@ export const PromptInputTextarea = ({
|
|||
};
|
||||
|
||||
const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {
|
||||
onPaste?.(event);
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
const items = event.clipboardData?.items;
|
||||
|
||||
if (!items) {
|
||||
|
|
|
|||
|
|
@ -332,6 +332,8 @@ export interface FileInMessage {
|
|||
size: number; // bytes
|
||||
path?: string; // virtual path, may not be set during upload
|
||||
status?: "uploading" | "uploaded";
|
||||
ref_kind?: "mention";
|
||||
ref_source?: "artifact" | "upload";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,29 +1,50 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { resolveThreadQueryIntent } = await import(
|
||||
new URL("./utils.ts", import.meta.url).href
|
||||
const { buildFilesForSubmit } = await import(
|
||||
new URL("./submit-files.ts", import.meta.url).href
|
||||
);
|
||||
|
||||
void test("uses /chats/new route as the only new-session signal", () => {
|
||||
const intent = resolveThreadQueryIntent({
|
||||
pathThreadId: "new",
|
||||
queryThreadId: "thread-from-query",
|
||||
isNewRoute: true,
|
||||
});
|
||||
void test("buildFilesForSubmit keeps uploads and appends valid references", () => {
|
||||
const result = buildFilesForSubmit(
|
||||
[
|
||||
{
|
||||
filename: "uploaded.md",
|
||||
size: 12,
|
||||
virtual_path: "/mnt/user-data/uploads/uploaded.md",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
filename: "artifact.md",
|
||||
path: "/mnt/user-data/artifacts/artifact.md",
|
||||
ref_kind: "mention",
|
||||
ref_source: "artifact",
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
assert.equal(intent.isNewThread, true);
|
||||
assert.equal(intent.showWelcomeStyle, true);
|
||||
assert.equal(intent.threadId, "thread-from-query");
|
||||
assert.equal(result.staleCount, 0);
|
||||
assert.equal(result.files.length, 2);
|
||||
assert.equal(result.files[0]?.filename, "uploaded.md");
|
||||
assert.equal(result.files[1]?.ref_kind, "mention");
|
||||
assert.equal(result.files[1]?.ref_source, "artifact");
|
||||
});
|
||||
|
||||
void test("prefers path thread id over query thread id when not on /new", () => {
|
||||
const intent = resolveThreadQueryIntent({
|
||||
pathThreadId: "thread-from-path",
|
||||
queryThreadId: "thread-from-query",
|
||||
isNewRoute: false,
|
||||
});
|
||||
void test("buildFilesForSubmit drops stale references without blocking submit", () => {
|
||||
const result = buildFilesForSubmit(
|
||||
[],
|
||||
[
|
||||
{
|
||||
filename: "stale.md",
|
||||
path: "/stale.md",
|
||||
ref_kind: "mention",
|
||||
ref_source: "upload",
|
||||
stale: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
assert.equal(intent.isNewThread, false);
|
||||
assert.equal(intent.threadId, "thread-from-path");
|
||||
assert.equal(result.staleCount, 1);
|
||||
assert.equal(result.files.length, 0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import type { FileInMessage } from "../messages/utils";
|
||||
import type { UploadedFileInfo } from "../uploads/api";
|
||||
|
||||
export type MentionReference = {
|
||||
filename: string;
|
||||
path?: string;
|
||||
size?: number;
|
||||
ref_kind: "mention";
|
||||
ref_source: "artifact" | "upload";
|
||||
stale?: boolean;
|
||||
};
|
||||
|
||||
export function buildFilesForSubmit(
|
||||
uploadedFileInfo: UploadedFileInfo[],
|
||||
references: MentionReference[] = [],
|
||||
) {
|
||||
const uploadedFiles: FileInMessage[] = uploadedFileInfo.map((info) => ({
|
||||
filename: info.filename,
|
||||
size: info.size,
|
||||
path: info.virtual_path,
|
||||
status: "uploaded" as const,
|
||||
}));
|
||||
|
||||
const referenceFiles: FileInMessage[] = [];
|
||||
let staleCount = 0;
|
||||
|
||||
for (const reference of references) {
|
||||
if (reference.stale) {
|
||||
staleCount += 1;
|
||||
continue;
|
||||
}
|
||||
referenceFiles.push({
|
||||
filename: reference.filename,
|
||||
size: reference.size ?? 0,
|
||||
path: reference.path,
|
||||
status: "uploaded" as const,
|
||||
ref_kind: "mention",
|
||||
ref_source: reference.ref_source,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
files: [...uploadedFiles, ...referenceFiles],
|
||||
staleCount,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue