fix: UI polish and provider fixes

- Add animate-pulse CSS animation for loading states
- Add Ctrl+I/Ctrl+B markdown shortcuts in chat input
- Improve chat input resize with mirror element
- Add reasoning duration display and shimmer effect
- Add confirmation dialog for deleting all models
- Simplify RowVirtualizerDynamic resize handling
- Fix DialogType imports (type -> value)
- Fix ollama cloud model name resolution
- Make vllm auth header optional
- Various icon and styling fixes
This commit is contained in:
Zoe
2026-05-11 17:44:00 -05:00
parent 6ee5b2cbbe
commit 57e7a92cd1
14 changed files with 177 additions and 39 deletions
+74 -5
View File
@@ -32,6 +32,7 @@ watch(files, (newFiles) => {
const emit = defineEmits<{
submit: [value: BaseMessage, model: ModelWithProvider | null];
cancel: [];
resize: [];
}>();
const props = defineProps<{
@@ -147,11 +148,50 @@ const handleSubmit = () => {
files.value = [];
textAreaValue.value = '';
}
// Reset height after sending
if (inputRef.value) inputRef.value.style.height = 'auto';
};
const toggleMarkdown = (marker: '*' | '**') => {
const textarea = inputRef.value;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
if (start === end) return;
const text = textAreaValue.value;
const len = marker.length;
let countBefore = 0;
for (let i = start - 1; i >= 0 && text[i] === '*'; i--) countBefore++;
let countAfter = 0;
for (let i = end; i < text.length && text[i] === '*'; i++) countAfter++;
const shouldRemove = len === 1
? countBefore >= 1 && countAfter >= 1 && countBefore % 2 === 1 && countAfter % 2 === 1
: countBefore >= 2 && countAfter >= 2;
if (shouldRemove) {
textAreaValue.value = text.slice(0, start - len) + text.slice(start, end) + text.slice(end + len);
nextTick(() => textarea.setSelectionRange(start - len, end - len));
} else {
textAreaValue.value = text.slice(0, start) + marker + text.slice(start, end) + marker + text.slice(end);
nextTick(() => textarea.setSelectionRange(start + len, end + len));
}
};
const handleKeyDown = async (event: KeyboardEvent) => {
if (event.ctrlKey && event.key.toLowerCase() === 'i') {
event.preventDefault();
event.stopPropagation();
toggleMarkdown('*');
return;
}
if (event.ctrlKey && event.key.toLowerCase() === 'b') {
event.preventDefault();
event.stopPropagation();
toggleMarkdown('**');
return;
}
if (event.key === 'Enter') {
if (event.shiftKey) return;
if (event.ctrlKey || event.metaKey) {
@@ -195,16 +235,43 @@ const handleWindowKeyDown = async (event: KeyboardEvent) => {
}
};
const mirrorRef = ref<HTMLDivElement | null>(null);
const resizeTextArea = () => {
const textarea = inputRef.value;
if (!textarea) return;
const mirror = mirrorRef.value;
if (!textarea || !mirror) return;
inputHeight.value = 'auto';
emit('resize');
nextTick().then(() => {
mirror.textContent = (textarea.value || '\u200b') + '\n';
const textareaStyles = getComputedStyle(textarea);
mirror.style.width = textareaStyles.width;
mirror.style.fontFamily = textareaStyles.fontFamily;
mirror.style.fontSize = textareaStyles.fontSize;
mirror.style.lineHeight = textareaStyles.lineHeight;
mirror.style.letterSpacing = textareaStyles.letterSpacing;
mirror.style.wordSpacing = textareaStyles.wordSpacing;
mirror.style.textAlign = textareaStyles.textAlign;
mirror.style.paddingTop = textareaStyles.paddingTop;
mirror.style.paddingBottom = textareaStyles.paddingBottom;
mirror.style.paddingLeft = textareaStyles.paddingLeft;
mirror.style.paddingRight = textareaStyles.paddingRight;
mirror.style.borderTopWidth = textareaStyles.borderTopWidth;
mirror.style.borderBottomWidth = textareaStyles.borderBottomWidth;
mirror.style.borderLeftWidth = textareaStyles.borderLeftWidth;
mirror.style.borderRightWidth = textareaStyles.borderRightWidth;
mirror.style.whiteSpace = 'pre-wrap';
mirror.style.wordWrap = 'break-word';
mirror.style.overflow = 'hidden';
const lineHeight = 24;
const minLines = 2;
const maxLines = 10;
const minHeight = minLines * lineHeight;
const maxHeight = maxLines * lineHeight;
const height = Math.min(textarea.scrollHeight, maxHeight);
const height = Math.min(Math.max(mirror.scrollHeight, minHeight), maxHeight);
inputHeight.value = `${height}px`;
});
};
@@ -249,6 +316,8 @@ onUnmounted(() => {
<template>
<div :class="['w-full flex max-h-full', $attrs.class]">
<div ref="mirrorRef" aria-hidden="true"
class="absolute top-0 left-0 pointer-events-none invisible overflow-hidden h-0"></div>
<div class="relative w-full flex flex-shrink-1 flex-col gap-3 p-2 rounded-2xl border transition-border ease-in-out duration-300 bg-[var(--bg-container)]
border-[var(--color-border)] focus-within:border-[var(--color-border-active)]">
<div v-if="files.length > 0" class="flex-1 flex gap-2 pt-2 px-2 pb-1 overflow-x-auto flex-wrap">