Repository: ggerganov/llama.cpp
File: tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte (Svelte)

Code before:
		onCopy,
		onDelete,
		onEdit,
		onEditKeydown,
		onEditedContentChange,
		onNavigateToSibling,
		onRegenerate,
		onSaveEdit,
		onShowDeleteDialogChange,
		onShouldBranchAfterEditChange,
		showDeleteDialog,
		shouldBranchAfterEdit = false,
		siblingInfo = null,
		textareaElement = $bindable(),
		thinkingContent,
		toolCallContent = null
	}: Props = $props();

	const toolCalls = $derived(
		Array.isArray(toolCallContent) ? (toolCallContent as ApiChatCompletionToolCall[]) : null
	);
	const fallbackToolCalls = $derived(typeof toolCallContent === 'string' ? toolCallContent : null);

	const processingState = useProcessingState();
	const promptProgressValue = $derived(processingState.processingState?.promptProgress);
	const promptProgressText = $derived(() => {
		if (!promptProgressValue) return null;
		const { processed, total, time_ms, cache } = promptProgressValue;

		// Subtract cached tokens since they're processed instantly from KV cache
		const actualProcessed = processed - cache;
		const actualTotal = total - cache;
		const percent = Math.round((actualProcessed / actualTotal) * 100);

		// First chunk: show progress without ETA
		if (actualProcessed === 0 || time_ms === 0) {
			return `Processing (${actualProcessed.toLocaleString()} / ${actualTotal.toLocaleString()} tokens - ${percent}%)`;
		}

		// Subsequent chunks: calculate and show ETA
		const tokensPerSec = actualProcessed / (time_ms / 1000);
		const remaining = actualTotal - actualProcessed;
		const etaSec = Math.ceil(remaining / tokensPerSec);
		const etaDisplay = etaSec >= 60 ? `${Math.floor(etaSec / 60)}m${etaSec % 60}s` : `${etaSec}s`;

		return `Processing (${actualProcessed.toLocaleString()} / ${actualTotal.toLocaleString()} tokens - ${percent}% - ETA: ${etaDisplay})`;
	});

	let currentConfig = $derived(config());
	let isRouter = $derived(isRouterMode());
	let displayedModel = $derived((): string | null => {

Review comment (suggestion):
```suggestion
	const promptProgressText = $derived.by(() => {
```

Code after:
		onCopy,
		onDelete,
		onEdit,
		onEditKeydown,
		onEditedContentChange,
		onNavigateToSibling,
		onRegenerate,
		onSaveEdit,
		onShowDeleteDialogChange,
		onShouldBranchAfterEditChange,
		showDeleteDialog,
		shouldBranchAfterEdit = false,
		siblingInfo = null,
		textareaElement = $bindable(),
		thinkingContent,
		toolCallContent = null
	}: Props = $props();

	const toolCalls = $derived(
		Array.isArray(toolCallContent) ? (toolCallContent as ApiChatCompletionToolCall[]) : null
	);
	const fallbackToolCalls = $derived(typeof toolCallContent === 'string' ? toolCallContent : null);

	const processingState = useProcessingState();

	let currentConfig = $derived(config());
	let isRouter = $derived(isRouterMode());
	let displayedModel = $derived((): string | null => {
		if (message.model) {
			return message.model;
		}

		return null;
	});

	const { handleModelChange } = useModelChangeValidation({
		getRequiredModalities: () => conversationsStore.getModalitiesUpToMessage(message.id),
		onSuccess: (modelName) => onRegenerate(modelName)
	});

	function handleCopyModel() {
		const model = displayedModel();

		void copyToClipboard(model ?? '');
	}

	$effect(() => {
		if (isEditing && textareaElement) {
			autoResizeTextarea(textareaElement);
		}
	});