| | <script lang="ts"> |
| | import { prompts } from '$lib/stores'; |
| | import { |
| | findWordIndices, |
| | getUserPosition, |
| | getFormattedDate, |
| | getFormattedTime, |
| | getCurrentDateTime, |
| | getUserTimezone, |
| | getWeekday |
| | } from '$lib/utils'; |
| | import { tick, getContext } from 'svelte'; |
| | import { toast } from 'svelte-sonner'; |
| |
|
| | const i18n = getContext('i18n'); |
| |
|
| | export let files; |
| |
|
| | export let prompt = ''; |
| | export let command = ''; |
| |
|
| | let selectedPromptIdx = 0; |
| | let filteredPrompts = []; |
| |
|
| | $: filteredPrompts = $prompts |
| | .filter((p) => p.command.toLowerCase().includes(command.toLowerCase())) |
| | .sort((a, b) => a.title.localeCompare(b.title)); |
| |
|
| | $: if (command) { |
| | selectedPromptIdx = 0; |
| | } |
| |
|
| | export const selectUp = () => { |
| | selectedPromptIdx = Math.max(0, selectedPromptIdx - 1); |
| | }; |
| |
|
| | export const selectDown = () => { |
| | selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1); |
| | }; |
| |
|
| | const confirmPrompt = async (command) => { |
| | let text = command.content; |
| |
|
| | if (command.content.includes('{{CLIPBOARD}}')) { |
| | const clipboardText = await navigator.clipboard.readText().catch((err) => { |
| | toast.error($i18n.t('Failed to read clipboard contents')); |
| | return '{{CLIPBOARD}}'; |
| | }); |
| |
|
| | const clipboardItems = await navigator.clipboard.read(); |
| |
|
| | let imageUrl = null; |
| | for (const item of clipboardItems) { |
| | |
| | for (const type of item.types) { |
| | if (type.startsWith('image/')) { |
| | const blob = await item.getType(type); |
| | imageUrl = URL.createObjectURL(blob); |
| | } |
| | } |
| | } |
| |
|
| | if (imageUrl) { |
| | files = [ |
| | ...files, |
| | { |
| | type: 'image', |
| | url: imageUrl |
| | } |
| | ]; |
| | } |
| |
|
| | text = text.replaceAll('{{CLIPBOARD}}', clipboardText); |
| | } |
| |
|
| | if (command.content.includes('{{USER_LOCATION}}')) { |
| | const location = await getUserPosition(); |
| | text = text.replaceAll('{{USER_LOCATION}}', String(location)); |
| | } |
| |
|
| | if (command.content.includes('{{USER_LANGUAGE}}')) { |
| | const language = localStorage.getItem('locale') || 'en-US'; |
| | text = text.replaceAll('{{USER_LANGUAGE}}', language); |
| | } |
| |
|
| | if (command.content.includes('{{CURRENT_DATE}}')) { |
| | const date = getFormattedDate(); |
| | text = text.replaceAll('{{CURRENT_DATE}}', date); |
| | } |
| |
|
| | if (command.content.includes('{{CURRENT_TIME}}')) { |
| | const time = getFormattedTime(); |
| | text = text.replaceAll('{{CURRENT_TIME}}', time); |
| | } |
| |
|
| | if (command.content.includes('{{CURRENT_DATETIME}}')) { |
| | const dateTime = getCurrentDateTime(); |
| | text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime); |
| | } |
| |
|
| | if (command.content.includes('{{CURRENT_TIMEZONE}}')) { |
| | const timezone = getUserTimezone(); |
| | text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone); |
| | } |
| |
|
| | if (command.content.includes('{{CURRENT_WEEKDAY}}')) { |
| | const weekday = getWeekday(); |
| | text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday); |
| | } |
| |
|
| | prompt = text; |
| |
|
| | const chatInputElement = document.getElementById('chat-textarea'); |
| |
|
| | await tick(); |
| |
|
| | chatInputElement.style.height = ''; |
| | chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px'; |
| |
|
| | chatInputElement?.focus(); |
| |
|
| | await tick(); |
| |
|
| | const words = findWordIndices(prompt); |
| | if (words.length > 0) { |
| | const word = words.at(0); |
| | chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1); |
| | } |
| | }; |
| | </script> |
| |
|
| | {#if filteredPrompts.length > 0} |
| | <div |
| | id="commands-container" |
| | class="pl-2 pr-14 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10" |
| | > |
| | <div class="flex w-full dark:border dark:border-gray-850 rounded-lg"> |
| | <div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center"> |
| | <div class=" text-lg font-medium mt-2">/</div> |
| | </div> |
| | |
| | <div |
| | class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100" |
| | > |
| | <div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden"> |
| | {#each filteredPrompts as prompt, promptIdx} |
| | <button |
| | class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx |
| | ? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button' |
| | : ''}" |
| | type="button" |
| | on:click={() => { |
| | confirmPrompt(prompt); |
| | }} |
| | on:mousemove={() => { |
| | selectedPromptIdx = promptIdx; |
| | }} |
| | on:focus={() => {}} |
| | > |
| | <div class=" font-medium text-black dark:text-gray-100"> |
| | {prompt.command} |
| | </div> |
| | |
| | <div class=" text-xs text-gray-600 dark:text-gray-100"> |
| | {prompt.title} |
| | </div> |
| | </button> |
| | {/each} |
| | </div> |
| | |
| | <div |
| | class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-br-xl flex items-center space-x-1" |
| | > |
| | <div> |
| | <svg |
| | xmlns="http://www.w3.org/2000/svg" |
| | fill="none" |
| | viewBox="0 0 24 24" |
| | stroke-width="1.5" |
| | stroke="currentColor" |
| | class="w-3 h-3" |
| | > |
| | <path |
| | stroke-linecap="round" |
| | stroke-linejoin="round" |
| | d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" |
| | /> |
| | </svg> |
| | </div> |
| | |
| | <div class="line-clamp-1"> |
| | {$i18n.t( |
| | 'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.' |
| | )} |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | {/if} |
| |
|