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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user