Skip to main content
Glama
TextDisplay.vue8.24 kB
<script setup lang="ts"> import { computed, ref, watch } from 'vue'; import MarkdownDisplay from './MarkdownDisplay.vue'; import MultiFieldForm from './MultiFieldForm.vue'; import { isMultiFieldRequest, type AnyInputRequest, type InputRequest, type MultiFieldRequest, } from '../composables/useWebSocket'; const props = defineProps<{ text: string; state: string; contentType: 'text' | 'markdown'; inputRequest?: AnyInputRequest | null; }>(); const emit = defineEmits<{ submitInput: [value: string, requestId: string]; cancelInput: [requestId: string]; submitMultiForm: [values: Record<string, unknown>, requestId: string]; }>(); // Input form state (for single-field) const inputValue = ref(''); // Reset input value when request changes (for single-field) watch(() => props.inputRequest?.requestId, () => { if (props.inputRequest && !isMultiFieldRequest(props.inputRequest)) { inputValue.value = props.inputRequest.defaultValue || ''; } }); // Computed properties const isWaitingForInput = computed(() => props.state === 'waitingForInput' && props.inputRequest); const isMultiField = computed(() => isMultiFieldRequest(props.inputRequest ?? null)); const isSingleField = computed(() => isWaitingForInput.value && !isMultiField.value); const hasInputContent = computed(() => !!props.inputRequest?.content); // Get single-field request (type-safe) const singleFieldRequest = computed(() => { if (props.inputRequest && !isMultiFieldRequest(props.inputRequest)) { return props.inputRequest as InputRequest; } return null; }); // Get multi-field request (type-safe) const multiFieldRequest = computed(() => { if (props.inputRequest && isMultiFieldRequest(props.inputRequest)) { return props.inputRequest as MultiFieldRequest; } return null; }); // Display content: use inputRequest.content when waiting for input, otherwise use text const displayContent = computed(() => { if (isWaitingForInput.value && hasInputContent.value) { return props.inputRequest!.content!; } return props.text; }); const isEmpty = computed(() => !displayContent.value && !isWaitingForInput.value); const isMarkdown = computed(() => { // Always markdown mode when showing input content if (isWaitingForInput.value && hasInputContent.value) return true; return props.contentType === 'markdown'; }); const label = computed(() => { if (isWaitingForInput.value) { return isMultiField.value ? 'Form Required' : 'Input Required'; } return isMarkdown.value ? 'Markdown Content' : 'Displayed Text'; }); // Single-field input handlers function handleSubmit() { if (singleFieldRequest.value) { emit('submitInput', inputValue.value, singleFieldRequest.value.requestId); } } function handleCancel() { if (props.inputRequest) { emit('cancelInput', props.inputRequest.requestId); } } // Multi-field form handlers function handleMultiFormSubmit(values: Record<string, unknown>, requestId: string) { emit('submitMultiForm', values, requestId); } function handleMultiFormCancel(requestId: string) { emit('cancelInput', requestId); } </script> <template> <div class="text-display" :class="{ empty: isEmpty, markdown: isMarkdown, 'waiting-input': isWaitingForInput }"> <div class="label">{{ label }}</div> <!-- Main content area --> <div class="content" :class="{ 'markdown-mode': isMarkdown, 'has-input': isWaitingForInput }"> <template v-if="displayContent"> <!-- Markdown rendering --> <MarkdownDisplay v-if="isMarkdown" :content="displayContent" /> <!-- Plain text --> <div v-else class="plain-text">{{ displayContent }}</div> </template> <template v-else-if="!isWaitingForInput"> <span class="placeholder">No content to display</span> </template> </div> <!-- Single-field input form section --> <div v-if="isSingleField && singleFieldRequest" class="input-section"> <div class="prompt">{{ singleFieldRequest.prompt }}</div> <div class="input-wrapper"> <textarea v-if="singleFieldRequest.inputType === 'textarea'" v-model="inputValue" :placeholder="singleFieldRequest.placeholder" class="input textarea" rows="3" /> <input v-else v-model="inputValue" :type="singleFieldRequest.inputType === 'number' ? 'number' : 'text'" :placeholder="singleFieldRequest.placeholder" class="input" @keyup.enter="handleSubmit" /> </div> <div class="actions"> <button class="btn btn-secondary" @click="handleCancel"> Cancel </button> <button class="btn btn-primary" @click="handleSubmit"> Submit </button> </div> </div> <!-- Multi-field form section --> <div v-else-if="isMultiField && multiFieldRequest" class="multi-form-section"> <MultiFieldForm :request="multiFieldRequest" @submit="handleMultiFormSubmit" @cancel="handleMultiFormCancel" /> </div> </div> </template> <style scoped> .text-display { background: #1a1a1a; border: 2px solid #333; border-radius: 12px; padding: 24px; flex: 1; display: flex; flex-direction: column; transition: border-color 0.3s ease, box-shadow 0.3s ease; } .text-display:not(.empty) { border-color: #4a9eff; box-shadow: 0 0 20px rgba(74, 158, 255, 0.1); } .text-display.markdown:not(.empty) { border-color: #9b59b6; box-shadow: 0 0 20px rgba(155, 89, 182, 0.1); } /* Waiting for input state - orange border */ .text-display.waiting-input { border-color: #ff9f4a; box-shadow: 0 0 20px rgba(255, 159, 74, 0.15); } .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; color: #666; margin-bottom: 16px; } .content { flex: 1; font-size: 2rem; line-height: 1.4; word-wrap: break-word; display: flex; align-items: center; justify-content: center; text-align: center; color: #fff; } /* Markdown mode adjusts layout */ .content.markdown-mode { font-size: 1rem; align-items: flex-start; justify-content: flex-start; text-align: left; overflow: auto; } /* When content has input below, limit height but maximize space */ .content.has-input { max-height: 65vh; overflow-y: auto; margin-bottom: 0; padding-bottom: 16px; padding-right: 12px; } /* Custom scrollbar styling */ .content::-webkit-scrollbar { width: 8px; } .content::-webkit-scrollbar-track { background: transparent; margin: 8px 0; } .content::-webkit-scrollbar-thumb { background: #9b59b6; border-radius: 4px; } .content::-webkit-scrollbar-thumb:hover { background: #a86bc4; } .plain-text { width: 100%; } .placeholder { color: #444; font-style: italic; } .empty .content { color: #444; } /* Input section styles */ .input-section { margin-top: auto; border-top: 2px solid #ff9f4a; background: linear-gradient(180deg, rgba(255, 159, 74, 0.08) 0%, transparent 100%); margin-left: -24px; margin-right: -24px; margin-bottom: -24px; padding: 16px 24px 24px 24px; border-radius: 0 0 10px 10px; } .prompt { font-size: 1.1rem; color: #ff9f4a; margin-bottom: 12px; font-weight: 600; } .input-wrapper { margin-bottom: 12px; } .input { width: 100%; padding: 12px 16px; font-size: 1rem; background: #2a2a2a; border: 1px solid #444; border-radius: 8px; color: #fff; outline: none; transition: border-color 0.2s; box-sizing: border-box; } .input:focus { border-color: #ff9f4a; } .input.textarea { resize: vertical; min-height: 80px; font-family: inherit; } .actions { display: flex; gap: 12px; justify-content: flex-end; } .btn { padding: 10px 20px; font-size: 0.9rem; border-radius: 8px; border: none; cursor: pointer; transition: background 0.2s, transform 0.1s; } .btn:active { transform: scale(0.98); } .btn-primary { background: #ff9f4a; color: #000; font-weight: 600; } .btn-primary:hover { background: #ffb36a; } .btn-secondary { background: #333; color: #aaa; } .btn-secondary:hover { background: #444; } /* Multi-form section */ .multi-form-section { margin-top: auto; padding-top: 16px; } </style>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/uptownhr/pane'

If you have feedback or need assistance with the MCP directory API, please join our Discord server