streaming, markdown, model selecting, and lots more
This commit is contained in:
+141
-33
@@ -1,38 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch, computed } from 'vue';
|
||||
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
|
||||
import type { Entity } from '@triplit/client';
|
||||
import { schema } from '#triplit/schema';
|
||||
|
||||
const inputRef = ref<HTMLTextAreaElement | null>(null);
|
||||
let tempInput = '';
|
||||
const inputValue = ref('');
|
||||
const isFocused = ref(false);
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [value: string];
|
||||
submit: [value: string, model: ModelWithProvider | null];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const props = defineProps<{
|
||||
loading?: boolean;
|
||||
agent?: Entity<typeof schema, 'agents'>;
|
||||
providers?: ProviderWithModels[];
|
||||
}>();
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (inputValue.value.trim()) {
|
||||
emit('submit', inputValue.value);
|
||||
inputValue.value = '';
|
||||
// Model selection state
|
||||
const selectedModel = ref<ModelWithProvider | null>(null);
|
||||
|
||||
// Get all available models from all providers
|
||||
const allModels = computed(() => {
|
||||
if (!props.providers) return [];
|
||||
return props.providers.flatMap((provider) =>
|
||||
provider.models.map((model) => ({
|
||||
...model,
|
||||
provider,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
// Initialize model selection based on agent's defaultModelId or first available
|
||||
const initializeModel = () => {
|
||||
if (selectedModel.value) return;
|
||||
if (!props.providers || !props.agent) return;
|
||||
|
||||
// Try to use agent's default model
|
||||
if (props.agent.defaultModelId) {
|
||||
const agentModel = allModels.value.find((m) => m.id === props.agent!.defaultModelId);
|
||||
if (agentModel) {
|
||||
selectedModel.value = agentModel;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to first available model
|
||||
if (allModels.value.length > 0) {
|
||||
selectedModel.value = allModels.value[0]!;
|
||||
// Update agent's default model
|
||||
updateAgentDefaultModel(selectedModel.value.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Update agent's default model in Triplit
|
||||
const updateAgentDefaultModel = async (modelId: string) => {
|
||||
if (!props.agent) return;
|
||||
try {
|
||||
await triplit.update('agents', props.agent.id, (agent) => {
|
||||
agent.defaultModelId = modelId;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update agent default model:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for model changes and persist to agent
|
||||
watch(selectedModel, (newModel) => {
|
||||
if (newModel && props.agent && newModel.id !== props.agent.defaultModelId) {
|
||||
updateAgentDefaultModel(newModel.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize when providers change
|
||||
watch(() => props.providers, initializeModel, { immediate: true });
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (props.loading) {
|
||||
emit('cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputValue.value.trim()) {
|
||||
emit('submit', inputValue.value, selectedModel.value);
|
||||
inputValue.value = '';
|
||||
}
|
||||
// Reset height after sending
|
||||
if (inputRef.value) inputRef.value.style.height = 'auto';
|
||||
};
|
||||
|
||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.shiftKey) return;
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (inputRef.value === null) return;
|
||||
|
||||
// inset new line
|
||||
let cursorPosition = inputRef.value.selectionStart;
|
||||
const cursorPosition = inputRef.value.selectionStart;
|
||||
if (cursorPosition === undefined) return;
|
||||
if (cursorPosition !== inputRef.value.selectionEnd) return;
|
||||
|
||||
inputValue.value = inputValue.value.slice(0, cursorPosition) + "\n" + inputValue.value.slice(cursorPosition);
|
||||
// inputRef.value.selectionStart = cursorPosition + 1;
|
||||
inputValue.value =
|
||||
inputValue.value.slice(0, cursorPosition) + '\n' + inputValue.value.slice(cursorPosition);
|
||||
await nextTick();
|
||||
handleInput();
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
@@ -40,41 +114,75 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = () => {
|
||||
const textarea = inputRef.value;
|
||||
if (!textarea) return;
|
||||
|
||||
textarea.style.height = 'auto';
|
||||
|
||||
const lineHeight = 24;
|
||||
const maxLines = 10;
|
||||
const maxHeight = maxLines * lineHeight;
|
||||
|
||||
const newHeight = textarea.scrollHeight;
|
||||
|
||||
if (newHeight > maxHeight) {
|
||||
textarea.style.height = `${maxHeight}px`;
|
||||
} else {
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
let hasCommandKey = false;
|
||||
if (import.meta.server) {
|
||||
let headers = useRequestHeaders();
|
||||
const headers = useRequestHeaders();
|
||||
hasCommandKey = headers['user-agent']?.includes('Mac OS') ?? false;
|
||||
} else {
|
||||
hasCommandKey = navigator.userAgent.includes('Mac OS');
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
tempInput = (document.getElementById('chat') as HTMLInputElement)?.value ?? '';
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
inputValue.value = tempInput;
|
||||
handleInput();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-full mx-auto">
|
||||
<div class="relative flex flex-col gap-3 p-3 rounded-2xl border transition-border ease-in-out duration-300 bg-[var(--color-input)]
|
||||
<div :class="['w-full flex max-h-full', $attrs.class]">
|
||||
<div class="relative w-full flex flex-shrink-1 flex-col gap-3 p-3 rounded-2xl border transition-border ease-in-out duration-300 bg-[var(--color-input)]
|
||||
border-[var(--color-highlight)] focus-within:border-[var(--color-highlight-high)]">
|
||||
<!-- Text Input -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<textarea v-model="inputValue" ref="inputRef"
|
||||
<div class="flex-1 min-w-0 max-h-full">
|
||||
<!-- Grammarly literally breaks everything, go fuck yourself -->
|
||||
<textarea data-gramm="false" id="chat" v-model="inputValue" ref="inputRef"
|
||||
:placeholder="`Start something great. Press ${hasCommandKey ? '⌘ + Enter' : 'ctrl + Enter'} to insert a new line.`"
|
||||
@focus="isFocused = true" @blur="isFocused = false" @keydown="handleKeyDown"
|
||||
class="w-full bg-transparent text-[var(--color-text)] placeholder-white/50 resize-none outline-none text-[15px] leading-6 min-h-[24px] max-h-32 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent"
|
||||
rows="2"></textarea>
|
||||
@keydown="handleKeyDown" @input="handleInput"
|
||||
class="[scrollbar-width:none] w-full bg-transparent text-[var(--color-text)] resize-none outline-none text-[15px] leading-6 min-h-0 overflow-y-auto">
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="flex">
|
||||
<div class="flex-1"></div>
|
||||
<!-- Send Button -->
|
||||
<button aria-label="Send message" @click="handleSubmit" :disabled="!inputValue.trim() || loading"
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1">
|
||||
<ModelSelector v-if="providers && providers.length > 0" v-model="selectedModel"
|
||||
:providers="providers"></ModelSelector>
|
||||
</div>
|
||||
<!-- Send/Stop Button -->
|
||||
<button aria-label="Send message" @click="handleSubmit" :disabled="!inputValue.trim() && !loading"
|
||||
:class="[
|
||||
'p-2 rounded-xl transition-all duration-200 flex items-center justify-center',
|
||||
inputValue.trim()
|
||||
'h-8 w-8 rounded-xl transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed disabled:bg-transparent',
|
||||
inputValue.trim() && !loading
|
||||
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] hover:bg-[var(--color-accent-hover)]'
|
||||
: 'bg-[var(--color-highlight)] text-[var(--color-highlight-high)] cursor-not-allowed'
|
||||
: 'text-[var(--color-highlight-high)]',
|
||||
loading && 'bg-[var(--color-highlight)] hover:bg-[var(--color-highlight-high)]',
|
||||
]">
|
||||
<Icon v-if="loading" name="svg-spinners:ring-resize" class="w-4 h-4" />
|
||||
<Icon v-else name="mynaui:send-solid" class="w-4 h-4" />
|
||||
<Icon v-if="loading" name="mynaui:stop-solid" class="text-6.5" />
|
||||
<Icon v-else name="mynaui:send-solid" class="text-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user