183 lines
6.3 KiB
Vue
183 lines
6.3 KiB
Vue
<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 { allModels } = await useModels();
|
|
|
|
const inputRef = ref<HTMLTextAreaElement | null>(null);
|
|
let tempInput = '';
|
|
const inputValue = ref('');
|
|
const triplit = useTriplitClient();
|
|
|
|
const emit = defineEmits<{
|
|
submit: [value: string, model: ModelWithProvider | null];
|
|
cancel: [];
|
|
}>();
|
|
|
|
const props = defineProps<{
|
|
loading?: boolean;
|
|
agent?: Entity<typeof schema, 'agents'>;
|
|
providers?: ProviderWithModels[];
|
|
}>();
|
|
|
|
// Model selection state
|
|
const selectedModel = ref<ModelWithProvider | null>(null);
|
|
|
|
// 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);
|
|
}
|
|
};
|
|
|
|
// 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
|
|
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);
|
|
await nextTick();
|
|
handleInput();
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
handleSubmit();
|
|
}
|
|
};
|
|
|
|
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) {
|
|
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;
|
|
nextTick(() => {
|
|
handleInput();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<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 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.`"
|
|
@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 items-center justify-between gap-2">
|
|
<div class="flex-1">
|
|
<ModelSelector v-if="providers !== undefined" v-model="selectedModel" :providers="providers">
|
|
</ModelSelector>
|
|
</div>
|
|
<!-- Send/Stop Button -->
|
|
<button aria-label="Send message" @click="handleSubmit" :disabled="!inputValue.trim() && !loading"
|
|
:class="[
|
|
'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)]'
|
|
: 'text-[var(--color-highlight-high)]',
|
|
loading && 'bg-[var(--color-highlight)] hover:bg-[var(--color-highlight-high)]',
|
|
]">
|
|
<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>
|
|
</div>
|
|
</template>
|