feat: message-per-row transcript, session resume, drop the composer (#4)
This commit was merged in pull request #4.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { previewText } from "./markdown";
|
||||
import type { ChatTurn, ThreadMessage } from "./types";
|
||||
|
||||
/**
|
||||
* Flattens stored turns into the flat message list the transcript renders.
|
||||
*
|
||||
* Storage keeps a question and its answer together as a turn, because that is
|
||||
* what gets replayed to the model. The UI needs the opposite shape: a flat
|
||||
* sequence so the user's line and Lam's line can each carry their own icon and
|
||||
* weight. This is the one place that translation happens.
|
||||
*/
|
||||
|
||||
/** Rows are a single line, so previews are clipped well before the renderer elides them. */
|
||||
export const MESSAGE_PREVIEW_LENGTH = 140;
|
||||
|
||||
export interface LiveState {
|
||||
isStreaming: boolean;
|
||||
/** Partial answer for the turn currently in flight. */
|
||||
response: string;
|
||||
thinking: string;
|
||||
/** Activity line, e.g. "Searching the web". */
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export function buildThread(turns: ChatTurn[], live: LiveState): ThreadMessage[] {
|
||||
const messages: ThreadMessage[] = [];
|
||||
const lastIndex = turns.length - 1;
|
||||
|
||||
turns.forEach((turn, index) => {
|
||||
messages.push({
|
||||
id: `${turn.id}:user`,
|
||||
role: "user",
|
||||
text: turn.prompt,
|
||||
preview: previewText(turn.prompt, MESSAGE_PREVIEW_LENGTH),
|
||||
createdAt: turn.createdAt,
|
||||
isStreaming: false,
|
||||
thinking: "",
|
||||
toolCalls: [],
|
||||
});
|
||||
|
||||
// Only the newest turn can be mid-flight.
|
||||
const isLive = index === lastIndex && live.isStreaming;
|
||||
const text = isLive ? live.response : turn.response;
|
||||
|
||||
messages.push({
|
||||
id: `${turn.id}:assistant`,
|
||||
role: "assistant",
|
||||
text,
|
||||
preview: assistantPreview(text, isLive, live.status, turn.error),
|
||||
createdAt: turn.createdAt,
|
||||
isStreaming: isLive,
|
||||
thinking: isLive ? live.thinking : turn.thinking,
|
||||
toolCalls: turn.toolCalls,
|
||||
error: isLive ? undefined : turn.error,
|
||||
});
|
||||
});
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* The row label for an answer.
|
||||
*
|
||||
* Before the first token lands there is nothing to preview, so the activity
|
||||
* line stands in — otherwise the row would sit blank while the model works.
|
||||
*/
|
||||
function assistantPreview(text: string, isLive: boolean, status: string | undefined, error?: string): string {
|
||||
if (text.trim().length > 0) return previewText(text, MESSAGE_PREVIEW_LENGTH);
|
||||
if (isLive) return status ?? "Thinking…";
|
||||
if (error) return error;
|
||||
return "No response";
|
||||
}
|
||||
Reference in New Issue
Block a user